main
js 175 lines 4.6 KB
Raw
1 let React;
2 let ReactNoop;
3 let Scheduler;
4 let act;
5 let Suspense;
6 let useEffect;
7 let getCacheForType;
8
9 let caches;
10 let seededCache;
11 let assertLog;
12
13 describe('ReactSuspenseWithNoopRenderer', () => {
14 beforeEach(() => {
15 jest.resetModules();
16
17 React = require('react');
18 ReactNoop = require('react-noop-renderer');
19 Scheduler = require('scheduler');
20 act = require('internal-test-utils').act;
21 Suspense = React.Suspense;
22 useEffect = React.useEffect;
23
24 getCacheForType = React.unstable_getCacheForType;
25
26 const InternalTestUtils = require('internal-test-utils');
27 assertLog = InternalTestUtils.assertLog;
28
29 caches = [];
30 seededCache = null;
31 });
32
33 function createTextCache() {
34 if (seededCache !== null) {
35 // Trick to seed a cache before it exists.
36 // TODO: Need a built-in API to seed data before the initial render (i.e.
37 // not a refresh because nothing has mounted yet).
38 const cache = seededCache;
39 seededCache = null;
40 return cache;
41 }
42
43 const data = new Map();
44 const version = caches.length + 1;
45 const cache = {
46 version,
47 data,
48 resolve(text) {
49 const record = data.get(text);
50 if (record === undefined) {
51 const newRecord = {
52 status: 'resolved',
53 value: text,
54 };
55 data.set(text, newRecord);
56 } else if (record.status === 'pending') {
57 const thenable = record.value;
58 record.status = 'resolved';
59 record.value = text;
60 thenable.pings.forEach(t => t());
61 }
62 },
63 reject(text, error) {
64 const record = data.get(text);
65 if (record === undefined) {
66 const newRecord = {
67 status: 'rejected',
68 value: error,
69 };
70 data.set(text, newRecord);
71 } else if (record.status === 'pending') {
72 const thenable = record.value;
73 record.status = 'rejected';
74 record.value = error;
75 thenable.pings.forEach(t => t());
76 }
77 },
78 };
79 caches.push(cache);
80 return cache;
81 }
82
83 function readText(text) {
84 const textCache = getCacheForType(createTextCache);
85 const record = textCache.data.get(text);
86 if (record !== undefined) {
87 switch (record.status) {
88 case 'pending':
89 Scheduler.log(`Suspend! [${text}]`);
90 throw record.value;
91 case 'rejected':
92 Scheduler.log(`Error! [${text}]`);
93 throw record.value;
94 case 'resolved':
95 return textCache.version;
96 }
97 } else {
98 Scheduler.log(`Suspend! [${text}]`);
99
100 const thenable = {
101 pings: [],
102 then(resolve) {
103 if (newRecord.status === 'pending') {
104 thenable.pings.push(resolve);
105 } else {
106 Promise.resolve().then(() => resolve(newRecord.value));
107 }
108 },
109 };
110
111 const newRecord = {
112 status: 'pending',
113 value: thenable,
114 };
115 textCache.data.set(text, newRecord);
116
117 throw thenable;
118 }
119 }
120
121 function resolveMostRecentTextCache(text) {
122 if (caches.length === 0) {
123 throw Error('Cache does not exist.');
124 } else {
125 // Resolve the most recently created cache. An older cache can by
126 // resolved with `caches[index].resolve(text)`.
127 caches[caches.length - 1].resolve(text);
128 }
129 }
130
131 const resolveText = resolveMostRecentTextCache;
132
133 // @gate enableLegacyCache && !disableLegacyMode
134 it('regression: false positive for legacy suspense', async () => {
135 const Child = ({text}) => {
136 // If text hasn't resolved, this will throw and exit before the passive
137 // static effect flag is added by the useEffect call below.
138 readText(text);
139
140 useEffect(() => {
141 Scheduler.log('Effect');
142 }, []);
143
144 Scheduler.log(text);
145 return text;
146 };
147
148 function App() {
149 return (
150 <Suspense fallback="Loading...">
151 <Child text="Async" />
152 </Suspense>
153 );
154 }
155
156 const root = ReactNoop.createLegacyRoot(null);
157
158 // On initial mount, the suspended component is committed in an incomplete
159 // state, without a passive static effect flag.
160 await act(() => {
161 root.render(<App />);
162 });
163 assertLog(['Suspend! [Async]']);
164 expect(root).toMatchRenderedOutput('Loading...');
165
166 // When the promise resolves, a passive static effect flag is added. In the
167 // regression, the "missing expected static flag" would fire, because the
168 // previous fiber did not have one.
169 await act(() => {
170 resolveText('Async');
171 });
172 assertLog(['Async', 'Effect']);
173 expect(root).toMatchRenderedOutput('Async');
174 });
175 });