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');