main
js 241 lines 5.87 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9 let React;
10 let ReactNoop;
11 let Scheduler;
12 let Suspense;
13 let getCacheForType;
14 let caches;
15 let seededCache;
16 let waitForAll;
17
18 describe('ReactSuspenseFallback', () => {
19 beforeEach(() => {
20 jest.resetModules();
21
22 React = require('react');
23 ReactNoop = require('react-noop-renderer');
24 Scheduler = require('scheduler');
25 Suspense = React.Suspense;
26 getCacheForType = React.unstable_getCacheForType;
27 caches = [];
28 seededCache = null;
29
30 const InternalTestUtils = require('internal-test-utils');
31 waitForAll = InternalTestUtils.waitForAll;
32 });
33
34 function createTextCache() {
35 if (seededCache !== null) {
36 // Trick to seed a cache before it exists.
37 // TODO: Need a built-in API to seed data before the initial render (i.e.
38 // not a refresh because nothing has mounted yet).
39 const cache = seededCache;
40 seededCache = null;
41 return cache;
42 }
43
44 const data = new Map();
45 const version = caches.length + 1;
46 const cache = {
47 version,
48 data,
49 resolve(text) {
50 const record = data.get(text);
51 if (record === undefined) {
52 const newRecord = {
53 status: 'resolved',
54 value: text,
55 };
56 data.set(text, newRecord);
57 } else if (record.status === 'pending') {
58 const thenable = record.value;
59 record.status = 'resolved';
60 record.value = text;
61 thenable.pings.forEach(t => t());
62 }
63 },
64 reject(text, error) {
65 const record = data.get(text);
66 if (record === undefined) {
67 const newRecord = {
68 status: 'rejected',
69 value: error,
70 };
71 data.set(text, newRecord);
72 } else if (record.status === 'pending') {
73 const thenable = record.value;
74 record.status = 'rejected';
75 record.value = error;
76 thenable.pings.forEach(t => t());
77 }
78 },
79 };
80 caches.push(cache);
81 return cache;
82 }
83
84 function readText(text) {
85 const textCache = getCacheForType(createTextCache);
86 const record = textCache.data.get(text);
87 if (record !== undefined) {
88 switch (record.status) {
89 case 'pending':
90 Scheduler.log(`Suspend! [${text}]`);
91 throw record.value;
92 case 'rejected':
93 Scheduler.log(`Error! [${text}]`);
94 throw record.value;
95 case 'resolved':
96 return textCache.version;
97 }
98 } else {
99 Scheduler.log(`Suspend! [${text}]`);
100
101 const thenable = {
102 pings: [],
103 then(resolve) {
104 if (newRecord.status === 'pending') {
105 thenable.pings.push(resolve);
106 } else {
107 Promise.resolve().then(() => resolve(newRecord.value));
108 }
109 },
110 };
111
112 const newRecord = {
113 status: 'pending',
114 value: thenable,
115 };
116 textCache.data.set(text, newRecord);
117
118 throw thenable;
119 }
120 }
121
122 function Text({text}) {
123 Scheduler.log(text);
124 return <span prop={text} />;
125 }
126
127 function AsyncText({text, showVersion}) {
128 const version = readText(text);
129 const fullText = showVersion ? `${text} [v${version}]` : text;
130 Scheduler.log(fullText);
131 return <span prop={fullText} />;
132 }
133
134 // @gate enableLegacyCache
135 it('suspends and shows fallback', async () => {
136 ReactNoop.render(
137 <Suspense fallback={<Text text="Loading..." />}>
138 <AsyncText text="A" />
139 </Suspense>,
140 );
141
142 await waitForAll([
143 'Suspend! [A]',
144 'Loading...',
145 // pre-warming
146 'Suspend! [A]',
147 ]);
148 expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
149 });
150
151 // @gate enableLegacyCache
152 it('suspends and shows null fallback', async () => {
153 ReactNoop.render(
154 <Suspense fallback={null}>
155 <AsyncText text="A" />
156 </Suspense>,
157 );
158
159 await waitForAll([
160 'Suspend! [A]',
161 // null
162 // pre-warming
163 'Suspend! [A]',
164 ]);
165 expect(ReactNoop).toMatchRenderedOutput(null);
166 });
167
168 // @gate enableLegacyCache
169 it('suspends and shows undefined fallback', async () => {
170 ReactNoop.render(
171 <Suspense>
172 <AsyncText text="A" />
173 </Suspense>,
174 );
175
176 await waitForAll([
177 'Suspend! [A]',
178 // null
179 // pre-warming
180 'Suspend! [A]',
181 ]);
182 expect(ReactNoop).toMatchRenderedOutput(null);
183 });
184
185 // @gate enableLegacyCache
186 it('suspends and shows inner fallback', async () => {
187 ReactNoop.render(
188 <Suspense fallback={<Text text="Should not show..." />}>
189 <Suspense fallback={<Text text="Loading..." />}>
190 <AsyncText text="A" />
191 </Suspense>
192 </Suspense>,
193 );
194
195 await waitForAll([
196 'Suspend! [A]',
197 'Loading...',
198 // pre-warming
199 'Suspend! [A]',
200 ]);
201 expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
202 });
203
204 // @gate enableLegacyCache
205 it('suspends and shows inner undefined fallback', async () => {
206 ReactNoop.render(
207 <Suspense fallback={<Text text="Should not show..." />}>
208 <Suspense>
209 <AsyncText text="A" />
210 </Suspense>
211 </Suspense>,
212 );
213
214 await waitForAll([
215 'Suspend! [A]',
216 // null
217 // pre-warming
218 'Suspend! [A]',
219 ]);
220 expect(ReactNoop).toMatchRenderedOutput(null);
221 });
222
223 // @gate enableLegacyCache
224 it('suspends and shows inner null fallback', async () => {
225 ReactNoop.render(
226 <Suspense fallback={<Text text="Should not show..." />}>
227 <Suspense fallback={null}>
228 <AsyncText text="A" />
229 </Suspense>
230 </Suspense>,
231 );
232
233 await waitForAll([
234 'Suspend! [A]',
235 // null
236 // pre-warming
237 'Suspend! [A]',
238 ]);
239 expect(ReactNoop).toMatchRenderedOutput(null);
240 });
241 });