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