main
js 307 lines 8.2 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 * @emails react-core
8 * @jest-environment node
9 */
10
11 'use strict';
12
13 let React;
14 let ReactNoopFlightServer;
15 let ReactNoopFlightClient;
16 let cache;
17 let cacheSignal;
18
19 describe('ReactCache', () => {
20 beforeEach(() => {
21 jest.resetModules();
22 jest.mock('react', () => require('react/react.react-server'));
23 React = require('react');
24
25 ReactNoopFlightServer = require('react-noop-renderer/flight-server');
26 ReactNoopFlightClient = require('react-noop-renderer/flight-client');
27
28 cache = React.cache;
29 cacheSignal = React.cacheSignal;
30
31 jest.resetModules();
32 __unmockReact();
33 });
34
35 it('cache objects and primitive arguments and a mix of them', async () => {
36 const types = cache((a, b) => ({a: typeof a, b: typeof b}));
37 function Print({a, b}) {
38 return types(a, b).a + ' ' + types(a, b).b + ' ';
39 }
40 function Same({a, b}) {
41 const x = types(a, b);
42 const y = types(a, b);
43 return (x === y).toString() + ' ';
44 }
45 function FlippedOrder({a, b}) {
46 return (types(a, b) === types(b, a)).toString() + ' ';
47 }
48 function FewerArgs({a, b}) {
49 return (types(a, b) === types(a)).toString() + ' ';
50 }
51 function MoreArgs({a, b}) {
52 return (types(a) === types(a, b)).toString() + ' ';
53 }
54
55 expect(
56 (
57 await ReactNoopFlightClient.read(
58 ReactNoopFlightServer.render(
59 <>
60 <Print a="e" b="f" />
61 <Same a="a" b="b" />
62 <FlippedOrder a="c" b="d" />
63 <FewerArgs a="e" b="f" />
64 <MoreArgs a="g" b="h" />
65 </>,
66 ),
67 )
68 ).join(''),
69 ).toEqual('string string true false false false ');
70
71 expect(
72 (
73 await ReactNoopFlightClient.read(
74 ReactNoopFlightServer.render(
75 <>
76 <Print a="e" b={null} />
77 <Same a="a" b={null} />
78 <FlippedOrder a="c" b={null} />
79 <FewerArgs a="e" b={null} />
80 <MoreArgs a="g" b={null} />
81 </>,
82 ),
83 )
84 ).join(''),
85 ).toEqual('string object true false false false ');
86
87 const obj = {};
88 expect(
89 (
90 await ReactNoopFlightClient.read(
91 ReactNoopFlightServer.render(
92 <>
93 <Print a="e" b={obj} />
94 <Same a="a" b={obj} />
95 <FlippedOrder a="c" b={obj} />
96 <FewerArgs a="e" b={obj} />
97 <MoreArgs a="g" b={obj} />
98 </>,
99 ),
100 )
101 ).join(''),
102 ).toEqual('string object true false false false ');
103
104 const sameObj = {};
105 expect(
106 (
107 await ReactNoopFlightClient.read(
108 ReactNoopFlightServer.render(
109 <>
110 <Print a={sameObj} b={sameObj} />
111 <Same a={sameObj} b={sameObj} />
112 <FlippedOrder a={sameObj} b={sameObj} />
113 <FewerArgs a={sameObj} b={sameObj} />
114 <MoreArgs a={sameObj} b={sameObj} />
115 </>,
116 ),
117 )
118 ).join(''),
119 ).toEqual('object object true true false false ');
120
121 const objA = {};
122 const objB = {};
123 expect(
124 (
125 await ReactNoopFlightClient.read(
126 ReactNoopFlightServer.render(
127 <>
128 <Print a={objA} b={objB} />
129 <Same a={objA} b={objB} />
130 <FlippedOrder a={objA} b={objB} />
131 <FewerArgs a={objA} b={objB} />
132 <MoreArgs a={objA} b={objB} />
133 </>,
134 ),
135 )
136 ).join(''),
137 ).toEqual('object object true false false false ');
138
139 const sameSymbol = Symbol();
140 expect(
141 (
142 await ReactNoopFlightClient.read(
143 ReactNoopFlightServer.render(
144 <>
145 <Print a={sameSymbol} b={sameSymbol} />
146 <Same a={sameSymbol} b={sameSymbol} />
147 <FlippedOrder a={sameSymbol} b={sameSymbol} />
148 <FewerArgs a={sameSymbol} b={sameSymbol} />
149 <MoreArgs a={sameSymbol} b={sameSymbol} />
150 </>,
151 ),
152 )
153 ).join(''),
154 ).toEqual('symbol symbol true true false false ');
155
156 const notANumber = +'nan';
157 expect(
158 (
159 await ReactNoopFlightClient.read(
160 ReactNoopFlightServer.render(
161 <>
162 <Print a={1} b={notANumber} />
163 <Same a={1} b={notANumber} />
164 <FlippedOrder a={1} b={notANumber} />
165 <FewerArgs a={1} b={notANumber} />
166 <MoreArgs a={1} b={notANumber} />
167 </>,
168 ),
169 )
170 ).join(''),
171 ).toEqual('number number true false false false ');
172 });
173
174 it('cached functions that throw should cache the error', async () => {
175 const throws = cache(v => {
176 throw new Error(v);
177 });
178 let x;
179 let y;
180 let z;
181 function Test() {
182 try {
183 throws(1);
184 } catch (e) {
185 x = e;
186 }
187 try {
188 throws(1);
189 } catch (e) {
190 y = e;
191 }
192 try {
193 throws(2);
194 } catch (e) {
195 z = e;
196 }
197
198 return 'Blank';
199 }
200
201 ReactNoopFlightServer.render(<Test />);
202 expect(x).toBe(y);
203 expect(z).not.toBe(x);
204 });
205
206 it('introspection of returned wrapper function is same on client and server', async () => {
207 // When the variant flag is true, test the client version of `cache`.
208 if (gate(flags => flags.variant)) {
209 jest.resetModules();
210 jest.mock('react', () => jest.requireActual('react'));
211 const ClientReact = require('react');
212 cache = ClientReact.cache;
213 }
214
215 function foo(a, b, c) {
216 return a + b + c;
217 }
218 foo.displayName = 'Custom display name';
219
220 const cachedFoo = cache(foo);
221 expect(cachedFoo).not.toBe(foo);
222 expect(cachedFoo.length).toBe(0);
223 expect(cachedFoo.displayName).toBe(undefined);
224 });
225
226 it('cacheSignal() returns null outside a render', async () => {
227 expect(cacheSignal()).toBe(null);
228 });
229
230 it('cacheSignal() aborts when the render finishes normally', async () => {
231 let renderedCacheSignal = null;
232
233 let resolve;
234 const promise = new Promise(r => (resolve = r));
235
236 async function Test() {
237 renderedCacheSignal = cacheSignal();
238 await promise;
239 return 'Hi';
240 }
241
242 const controller = new AbortController();
243 const errors = [];
244 const result = ReactNoopFlightServer.render(<Test />, {
245 signal: controller.signal,
246 onError(x) {
247 errors.push(x);
248 },
249 });
250 expect(errors).toEqual([]);
251 expect(renderedCacheSignal).not.toBe(controller.signal); // In the future we might make these the same
252 expect(renderedCacheSignal.aborted).toBe(false);
253 await resolve();
254 await 0;
255 await 0;
256
257 expect(await ReactNoopFlightClient.read(result)).toBe('Hi');
258
259 expect(errors).toEqual([]);
260 expect(renderedCacheSignal.aborted).toBe(true);
261 expect(renderedCacheSignal.reason.message).toContain(
262 'This render completed successfully.',
263 );
264 });
265
266 it('cacheSignal() aborts when the render is aborted', async () => {
267 let renderedCacheSignal = null;
268
269 const promise = new Promise(() => {});
270
271 async function Test() {
272 renderedCacheSignal = cacheSignal();
273 await promise;
274 return 'Hi';
275 }
276
277 const controller = new AbortController();
278 const errors = [];
279 const result = ReactNoopFlightServer.render(<Test />, {
280 signal: controller.signal,
281 onError(x) {
282 errors.push(x);
283 return 'hi';
284 },
285 });
286 expect(errors).toEqual([]);
287 expect(renderedCacheSignal).not.toBe(controller.signal); // In the future we might make these the same
288 expect(renderedCacheSignal.aborted).toBe(false);
289 const reason = new Error('Timed out');
290 controller.abort(reason);
291 expect(errors).toEqual([reason]);
292 expect(renderedCacheSignal.aborted).toBe(true);
293 expect(renderedCacheSignal.reason).toBe(reason);
294
295 let clientError = null;
296 try {
297 await ReactNoopFlightClient.read(result);
298 } catch (x) {
299 clientError = x;
300 }
301 expect(clientError).not.toBe(null);
302 if (__DEV__) {
303 expect(clientError.message).toBe('Timed out');
304 }
305 expect(clientError.digest).toBe('hi');
306 });
307 });