@samitouri / QOS-React-2 / commits / 9043626f09

Cache tests: Make it easier to test many caches (#20600)

Some rearranging to make it easier to write tests that assert on the output of multiple caches.

Andrew Clark committed Jan 19, 2021 at 15:54 UTC 9043626f09c7d02a81b1ed89236e8f88d44fdc7e
1 file changed +122 -126
packages/react-reconciler/src/__tests__/ReactCache-test.js
+122 -126
@@ -8,8 +8,8 @@ let useCacheRefresh;
8 let startTransition;
9 let useState;
10
11 -let textService;
12 -let textServiceVersion;
11 +let caches;
12 +let seededCache;
13
14 describe('ReactCache', () => {
15 beforeEach(() => {
@@ -25,20 +25,57 @@ describe('ReactCache', () => {
25 startTransition = React.unstable_startTransition;
26 useState = React.useState;
27
28 - // Represents some data service that returns text. It likely has additional
29 - // caching layers, like a CDN or the local browser cache. It can be mutated
30 - // or emptied independently of the React cache.
31 - textService = new Map();
32 - textServiceVersion = 1;
28 + caches = [];
29 + seededCache = null;
30 });
31
32 function createTextCache() {
36 - return new Map();
33 + if (seededCache !== null) {
34 + // Trick to seed a cache before it exists.
35 + // TODO: Need a built-in API to seed data before the initial render (i.e.
36 + // not a refresh because nothing has mounted yet).
37 + const cache = seededCache;
38 + seededCache = null;
39 + return cache;
40 + }
41 +
42 + const data = new Map();
43 + const version = caches.length + 1;
44 + const cache = {
45 + version,
46 + data,
47 + resolve(text) {
48 + const record = data.get(text);
49 + if (record === undefined) {
50 + const newRecord = {
51 + status: 'resolved',
52 + value: text,
53 + };
54 + data.set(text, newRecord);
55 + } else if (record.status === 'pending') {
56 + record.value.resolve();
57 + }
58 + },
59 + reject(text, error) {
60 + const record = data.get(text);
61 + if (record === undefined) {
62 + const newRecord = {
63 + status: 'rejected',
64 + value: error,
65 + };
66 + data.set(text, newRecord);
67 + } else if (record.status === 'pending') {
68 + record.value.reject();
69 + }
70 + },
71 + };
72 + caches.push(cache);
73 + return cache;
74 }
75
76 function readText(text) {
77 const textCache = getCacheForType(createTextCache);
41 - const record = textCache.get(text);
78 + const record = textCache.data.get(text);
79 if (record !== undefined) {
80 switch (record.status) {
81 case 'pending':
@@ -46,28 +83,17 @@ describe('ReactCache', () => {
83 case 'rejected':
84 throw record.value;
85 case 'resolved':
49 - return record.value;
86 + return textCache.version;
87 }
88 } else {
89 Scheduler.unstable_yieldValue(`Cache miss! [${text}]`);
90
54 - let request = textService.get(text);
55 - if (request === undefined) {
56 - let resolve;
57 - let reject;
58 - request = new Promise((res, rej) => {
59 - resolve = res;
60 - reject = rej;
61 - });
62 - request.resolve = resolve;
63 - request.reject = reject;
64 -
65 - // Add the request to a backing cache. This may outlive the lifetime
66 - // of the component that is currently reading the data.
67 - textService.set(text, request);
68 - }
69 -
70 - const thenable = request.then(
91 + let resolve;
92 + let reject;
93 + const thenable = new Promise((res, rej) => {
94 + resolve = res;
95 + reject = rej;
96 + }).then(
97 value => {
98 if (newRecord.status === 'pending') {
99 newRecord.status = 'resolved';
@@ -81,36 +107,19 @@ describe('ReactCache', () => {
107 }
108 },
109 );
110 + thenable.resolve = resolve;
111 + thenable.reject = reject;
112
113 const newRecord = {
86 - ping: null,
114 status: 'pending',
115 value: thenable,
116 };
90 - textCache.set(text, newRecord);
117 + textCache.data.set(text, newRecord);
118
119 throw thenable;
120 }
121 }
122
96 - function mutateRemoteTextService() {
97 - textService = new Map();
98 - textServiceVersion++;
99 - }
100 -
101 - function resolveText(text) {
102 - const request = textService.get(text);
103 - if (request !== undefined) {
104 - request.resolve(textServiceVersion);
105 - return request;
106 - } else {
107 - const newRequest = Promise.resolve(textServiceVersion);
108 - newRequest.resolve = newRequest.reject = () => {};
109 - textService.set(text, newRequest);
110 - return newRequest;
111 - }
112 - }
113 -
123 function Text({text}) {
124 Scheduler.unstable_yieldValue(text);
125 return text;
@@ -123,6 +132,23 @@ describe('ReactCache', () => {
132 return fullText;
133 }
134
135 + function seedNextTextCache(text) {
136 + if (seededCache === null) {
137 + seededCache = createTextCache();
138 + }
139 + seededCache.resolve(text);
140 + }
141 +
142 + function resolveMostRecentTextCache(text) {
143 + if (caches.length === 0) {
144 + throw Error('Cache does not exist.');
145 + } else {
146 + // Resolve the most recently created cache. An older cache can by
147 + // resolved with `caches[index].resolve(text)`.
148 + caches[caches.length - 1].resolve(text);
149 + }
150 + }
151 +
152 // @gate experimental
153 test('render Cache component', async () => {
154 const root = ReactNoop.createRoot();
@@ -148,7 +174,7 @@ describe('ReactCache', () => {
174 expect(root).toMatchRenderedOutput('Loading...');
175
176 await ReactNoop.act(async () => {
151 - await resolveText('A');
177 + resolveMostRecentTextCache('A');
178 });
179 expect(Scheduler).toHaveYielded(['A']);
180 expect(root).toMatchRenderedOutput('A');
@@ -168,7 +194,7 @@ describe('ReactCache', () => {
194 expect(root).toMatchRenderedOutput('Loading...');
195
196 await ReactNoop.act(async () => {
171 - await resolveText('A');
197 + resolveMostRecentTextCache('A');
198 });
199 expect(Scheduler).toHaveYielded(['A']);
200 expect(root).toMatchRenderedOutput('A');
@@ -207,7 +233,7 @@ describe('ReactCache', () => {
233 expect(root).toMatchRenderedOutput('Loading...Loading...');
234
235 await ReactNoop.act(async () => {
210 - await resolveText('A');
236 + resolveMostRecentTextCache('A');
237 });
238 expect(Scheduler).toHaveYielded(['A', 'A']);
239 expect(root).toMatchRenderedOutput('AA');
@@ -239,7 +265,7 @@ describe('ReactCache', () => {
265 expect(root).toMatchRenderedOutput('Loading...');
266
267 await ReactNoop.act(async () => {
242 - await resolveText('A');
268 + resolveMostRecentTextCache('A');
269 });
270 expect(Scheduler).toHaveYielded(['A', 'A']);
271 expect(root).toMatchRenderedOutput('AA');
@@ -265,22 +291,14 @@ describe('ReactCache', () => {
291
292 const root = ReactNoop.createRoot();
293 await ReactNoop.act(async () => {
268 - await resolveText('A');
294 + seedNextTextCache('A');
295 root.render(<App showMore={false} />);
296 });
271 - expect(Scheduler).toHaveYielded([
272 - 'Cache miss! [A]',
273 - 'Loading...',
274 - 'A [v1]',
275 - ]);
297 + expect(Scheduler).toHaveYielded(['A [v1]']);
298 expect(root).toMatchRenderedOutput('A [v1]');
299
278 - // Simulate a server mutation.
279 - mutateRemoteTextService();
280 -
300 // Add a new cache boundary
301 await ReactNoop.act(async () => {
283 - await resolveText('A');
302 root.render(<App showMore={true} />);
303 });
304 expect(Scheduler).toHaveYielded([
@@ -314,22 +332,14 @@ describe('ReactCache', () => {
332
333 const root = ReactNoop.createRoot();
334 await ReactNoop.act(async () => {
317 - await resolveText('A');
335 + seedNextTextCache('A');
336 root.render(<App showMore={false} />);
337 });
320 - expect(Scheduler).toHaveYielded([
321 - 'Cache miss! [A]',
322 - 'Loading...',
323 - 'A [v1]',
324 - ]);
338 + expect(Scheduler).toHaveYielded(['A [v1]']);
339 expect(root).toMatchRenderedOutput('A [v1]');
340
327 - // Simulate a server mutation.
328 - mutateRemoteTextService();
329 -
341 // Add a new cache boundary
342 await ReactNoop.act(async () => {
332 - await resolveText('A');
343 root.render(<App showMore={true} />);
344 });
345 expect(Scheduler).toHaveYielded([
@@ -337,8 +347,12 @@ describe('ReactCache', () => {
347 // New tree should load fresh data.
348 'Cache miss! [A]',
349 'Loading...',
340 - 'A [v2]',
350 ]);
351 + expect(root).toMatchRenderedOutput('A [v1]Loading...');
352 + await ReactNoop.act(async () => {
353 + resolveMostRecentTextCache('A');
354 + });
355 + expect(Scheduler).toHaveYielded(['A [v2]']);
356 expect(root).toMatchRenderedOutput('A [v1]A [v2]');
357 });
358
@@ -389,7 +403,7 @@ describe('ReactCache', () => {
403 expect(root).toMatchRenderedOutput('Loading shell...');
404
405 await ReactNoop.act(async () => {
392 - await resolveText('A');
406 + resolveMostRecentTextCache('A');
407 });
408 expect(Scheduler).toHaveYielded([
409 'Shell',
@@ -406,7 +420,7 @@ describe('ReactCache', () => {
420 );
421
422 await ReactNoop.act(async () => {
409 - await resolveText('B');
423 + resolveMostRecentTextCache('B');
424 });
425 expect(Scheduler).toHaveYielded(['Content']);
426 expect(root).toMatchRenderedOutput(
@@ -440,13 +454,12 @@ describe('ReactCache', () => {
454 expect(root).toMatchRenderedOutput('Loading...');
455
456 await ReactNoop.act(async () => {
443 - await resolveText('A');
457 + resolveMostRecentTextCache('A');
458 });
459 expect(Scheduler).toHaveYielded(['A [v1]']);
460 expect(root).toMatchRenderedOutput('A [v1]');
461
448 - // Mutate the text service, then refresh for new data.
449 - mutateRemoteTextService();
462 + // Fefresh for new data.
463 await ReactNoop.act(async () => {
464 startTransition(() => refresh());
465 });
@@ -454,7 +467,7 @@ describe('ReactCache', () => {
467 expect(root).toMatchRenderedOutput('A [v1]');
468
469 await ReactNoop.act(async () => {
457 - await resolveText('A');
470 + resolveMostRecentTextCache('A');
471 });
472 // Note that the version has updated
473 expect(Scheduler).toHaveYielded(['A [v2]']);
@@ -482,13 +495,12 @@ describe('ReactCache', () => {
495 expect(root).toMatchRenderedOutput('Loading...');
496
497 await ReactNoop.act(async () => {
485 - await resolveText('A');
498 + resolveMostRecentTextCache('A');
499 });
500 expect(Scheduler).toHaveYielded(['A [v1]']);
501 expect(root).toMatchRenderedOutput('A [v1]');
502
490 - // Mutate the text service, then refresh for new data.
491 - mutateRemoteTextService();
503 + // Refresh for new data.
504 await ReactNoop.act(async () => {
505 startTransition(() => refresh());
506 });
@@ -496,7 +508,7 @@ describe('ReactCache', () => {
508 expect(root).toMatchRenderedOutput('A [v1]');
509
510 await ReactNoop.act(async () => {
499 - await resolveText('A');
511 + resolveMostRecentTextCache('A');
512 });
513 // Note that the version has updated
514 expect(Scheduler).toHaveYielded(['A [v2]']);
@@ -526,25 +538,20 @@ describe('ReactCache', () => {
538 expect(root).toMatchRenderedOutput('Loading...');
539
540 await ReactNoop.act(async () => {
529 - await resolveText('A');
541 + resolveMostRecentTextCache('A');
542 });
543 expect(Scheduler).toHaveYielded(['A [v1]']);
544 expect(root).toMatchRenderedOutput('A [v1]');
545
534 - // Mutate the text service, then refresh for new data.
535 - mutateRemoteTextService();
546 + // Refresh for new data.
547 await ReactNoop.act(async () => {
548 // Refresh the cache with seeded data, like you would receive from a
549 // server mutation.
550 // TODO: Seeding multiple typed caches. Should work by calling `refresh`
551 // multiple times with different key/value pairs
541 - const seededCache = new Map();
542 - seededCache.set('A', {
543 - ping: null,
544 - status: 'resolved',
545 - value: textServiceVersion,
546 - });
547 - startTransition(() => refresh(createTextCache, seededCache));
552 + const cache = createTextCache();
553 + cache.resolve('A');
554 + startTransition(() => refresh(createTextCache, cache));
555 });
556 // The root should re-render without a cache miss.
557 expect(Scheduler).toHaveYielded(['A [v2]']);
@@ -579,36 +586,26 @@ describe('ReactCache', () => {
586
587 const root = ReactNoop.createRoot();
588 await ReactNoop.act(async () => {
582 - await resolveText('A');
589 + seedNextTextCache('A');
590 root.render(<App showMore={false} />);
591 });
585 - expect(Scheduler).toHaveYielded([
586 - 'Cache miss! [A]',
587 - 'Loading...',
588 - 'A [v1]',
589 - ]);
592 + expect(Scheduler).toHaveYielded(['A [v1]']);
593 expect(root).toMatchRenderedOutput('A [v1]');
594
592 - // Simulate a server mutation.
593 - mutateRemoteTextService();
594 -
595 // Add a new cache boundary
596 await ReactNoop.act(async () => {
597 - await resolveText('A');
597 + seedNextTextCache('A');
598 root.render(<App showMore={true} />);
599 });
600 expect(Scheduler).toHaveYielded([
601 'A [v1]',
602 // New tree should load fresh data.
603 - 'Cache miss! [A]',
604 - 'Loading...',
603 'A [v2]',
604 ]);
605 expect(root).toMatchRenderedOutput('A [v1]A [v2]');
606
607 // Now refresh the shell. This should also cause the "Show More" contents to
608 // refresh, since its cache is nested inside the outer one.
611 - mutateRemoteTextService();
609 await ReactNoop.act(async () => {
610 startTransition(() => refreshShell());
611 });
@@ -620,7 +617,7 @@ describe('ReactCache', () => {
617 expect(root).toMatchRenderedOutput('A [v1]A [v2]');
618
619 await ReactNoop.act(async () => {
623 - await resolveText('A');
620 + resolveMostRecentTextCache('A');
621 });
622 expect(Scheduler).toHaveYielded(['A [v3]', 'A [v3]']);
623 expect(root).toMatchRenderedOutput('A [v3]A [v3]');
@@ -679,21 +676,20 @@ describe('ReactCache', () => {
676 expect(root).toMatchRenderedOutput('Loading...Loading...');
677
678 await ReactNoop.act(async () => {
682 - await resolveText('A');
679 + resolveMostRecentTextCache('A');
680 });
681 expect(Scheduler).toHaveYielded(['A [v1]', 'A [v1]']);
682 expect(root).toMatchRenderedOutput('A [v1]A [v1]');
683
684 // Refresh the first boundary. It should not refresh the second boundary,
685 // even though they previously shared the same underlying cache.
689 - mutateRemoteTextService();
686 await ReactNoop.act(async () => {
687 await refreshFirstBoundary();
688 });
689 expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
690
691 await ReactNoop.act(async () => {
696 - await resolveText('A');
692 + resolveMostRecentTextCache('A');
693 });
694 expect(Scheduler).toHaveYielded(['A [v2]']);
695 expect(root).toMatchRenderedOutput('A [v2]A [v1]');
@@ -738,10 +734,8 @@ describe('ReactCache', () => {
734
735 await ReactNoop.act(async () => {
736 // This will resolve the content in the first cache
741 - resolveText('A');
742 - resolveText('B');
743 - // Now let's simulate a mutation
744 - mutateRemoteTextService();
737 + resolveMostRecentTextCache('A');
738 + resolveMostRecentTextCache('B');
739 // And mount the second tree, which includes new content
740 root.render(<App showMore={true} />);
741 });
@@ -757,7 +751,7 @@ describe('ReactCache', () => {
751
752 // Now resolve the second tree
753 await ReactNoop.act(async () => {
760 - resolveText('A');
754 + resolveMostRecentTextCache('A');
755 });
756 expect(Scheduler).toHaveYielded(['A [v2]']);
757 expect(root).toMatchRenderedOutput('A [v2] A [v1] B [v1]');
@@ -769,7 +763,7 @@ describe('ReactCache', () => {
763 function Child({text}) {
764 return (
765 <Cache>
772 - <AsyncText text={text} />
766 + <AsyncText showVersion={true} text={text} />
767 </Cache>
768 );
769 }
@@ -813,10 +807,10 @@ describe('ReactCache', () => {
807
808 // Resolve the request
809 await ReactNoop.act(async () => {
816 - await resolveText('A');
810 + resolveMostRecentTextCache('A');
811 });
818 - expect(Scheduler).toHaveYielded(['A', 'A']);
819 - expect(root).toMatchRenderedOutput('AA');
812 + expect(Scheduler).toHaveYielded(['A [v1]', 'A [v1]']);
813 + expect(root).toMatchRenderedOutput('A [v1]A [v1]');
814
815 // Now do another transition
816 await ReactNoop.act(async () => {
@@ -832,16 +826,19 @@ describe('ReactCache', () => {
826 });
827 expect(Scheduler).toHaveYielded([
828 // First two children use the old cache because they already finished
835 - 'A',
836 - 'A',
829 + 'A [v1]',
830 + 'A [v1]',
831 // The new child uses a fresh cache
832 'Cache miss! [A]',
833 'Loading...',
840 - 'A',
841 - 'A',
842 - 'A',
834 ]);
844 - expect(root).toMatchRenderedOutput('AAA');
835 + expect(root).toMatchRenderedOutput('A [v1]A [v1]');
836 +
837 + await ReactNoop.act(async () => {
838 + resolveMostRecentTextCache('A');
839 + });
840 + expect(Scheduler).toHaveYielded(['A [v1]', 'A [v1]', 'A [v2]']);
841 + expect(root).toMatchRenderedOutput('A [v1]A [v1]A [v2]');
842 });
843
844 // @gate experimental
@@ -907,8 +904,7 @@ describe('ReactCache', () => {
904 expect(root).toMatchRenderedOutput('1');
905
906 await ReactNoop.act(async () => {
910 - resolveText('A');
911 - mutateRemoteTextService();
907 + resolveMostRecentTextCache('A');
908 });
909 expect(Scheduler).toHaveYielded(['A']);
910 expect(root).toMatchRenderedOutput('A1');