main
js 4,505 lines 136 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 if (typeof Blob === 'undefined') {
14 global.Blob = require('buffer').Blob;
15 }
16 if (typeof File === 'undefined' || typeof FormData === 'undefined') {
17 global.File = require('undici').File;
18 global.FormData = require('undici').FormData;
19 }
20
21 function normalizeCodeLocInfo(str) {
22 return (
23 str &&
24 str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) {
25 const dot = name.lastIndexOf('.');
26 if (dot !== -1) {
27 name = name.slice(dot + 1);
28 }
29 return ' in ' + name + (/\d/.test(m) ? ' (at **)' : '');
30 })
31 );
32 }
33
34 function normalizeReactCodeLocInfo(str) {
35 const repoRootForRegexp = __REACT_ROOT_PATH_TEST__.replace(/\//g, '\\/');
36 const repoFileLocMatch = new RegExp(`${repoRootForRegexp}.+?:\\d+:\\d+`, 'g');
37 return str && str.replace(repoFileLocMatch, '**');
38 }
39
40 // If we just use the original Error prototype, Jest will only display the error message if assertions fail.
41 // But we usually want to also assert on our expando properties or even the stack.
42 // By hiding the fact from Jest that this is an error, it will show all enumerable properties on mismatch.
43
44 function getErrorForJestMatcher(error) {
45 return {
46 ...error,
47 // non-enumerable properties that are still relevant for testing
48 message: error.message,
49 stack: normalizeReactCodeLocInfo(error.stack),
50 };
51 }
52
53 const finalizationRegistries = [];
54 function FinalizationRegistryMock(callback) {
55 this._heldValues = [];
56 this._callback = callback;
57 finalizationRegistries.push(this);
58 }
59 FinalizationRegistryMock.prototype.register = function (target, heldValue) {
60 this._heldValues.push(heldValue);
61 };
62 global.FinalizationRegistry = FinalizationRegistryMock;
63
64 function gc() {
65 for (let i = 0; i < finalizationRegistries.length; i++) {
66 const registry = finalizationRegistries[i];
67 const callback = registry._callback;
68 const heldValues = registry._heldValues;
69 for (let j = 0; j < heldValues.length; j++) {
70 callback(heldValues[j]);
71 }
72 heldValues.length = 0;
73 }
74 }
75
76 let act;
77 let use;
78 let startTransition;
79 let React;
80 let ReactServer;
81 let ReactNoop;
82 let ReactNoopFlightServer;
83 let ReactNoopFlightClient;
84 let ErrorBoundary;
85 let NoErrorExpected;
86 let Scheduler;
87 let assertLog;
88 let assertConsoleErrorDev;
89 let getDebugInfo;
90
91 describe('ReactFlight', () => {
92 beforeEach(() => {
93 // Mock performance.now for timing tests
94 let time = 10;
95 jest.spyOn(performance, 'timeOrigin', 'get').mockReturnValue(time);
96 jest.spyOn(performance, 'now').mockImplementation(() => {
97 return time++;
98 });
99
100 jest.resetModules();
101 jest.mock('react', () => require('react/react.react-server'));
102 ReactServer = require('react');
103 ReactNoopFlightServer = require('react-noop-renderer/flight-server');
104 // This stores the state so we need to preserve it
105 const flightModules = require('react-noop-renderer/flight-modules');
106 jest.resetModules();
107 __unmockReact();
108 jest.mock('react-noop-renderer/flight-modules', () => flightModules);
109 React = require('react');
110 startTransition = React.startTransition;
111 use = React.use;
112 ReactNoop = require('react-noop-renderer');
113 ReactNoopFlightClient = require('react-noop-renderer/flight-client');
114 act = require('internal-test-utils').act;
115 Scheduler = require('scheduler');
116 const InternalTestUtils = require('internal-test-utils');
117 assertLog = InternalTestUtils.assertLog;
118 assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
119
120 getDebugInfo = InternalTestUtils.getDebugInfo.bind(null, {
121 useV8Stack: true,
122 ignoreRscStreamInfo: true,
123 });
124
125 ErrorBoundary = class extends React.Component {
126 state = {hasError: false, error: null};
127 static getDerivedStateFromError(error) {
128 return {
129 hasError: true,
130 error,
131 };
132 }
133 componentDidCatch(error, errorInfo) {
134 expect(error).toBe(this.state.error);
135 if (this.props.expectedStack !== undefined) {
136 expect(normalizeCodeLocInfo(errorInfo.componentStack)).toBe(
137 this.props.expectedStack,
138 );
139 }
140 }
141 componentDidMount() {
142 expect(this.state.hasError).toBe(true);
143 expect(this.state.error).toBeTruthy();
144 if (__DEV__) {
145 expect(this.state.error.message).toContain(
146 this.props.expectedMessage,
147 );
148 expect(this.state.error.digest).toBe('a dev digest');
149 expect(this.state.error.environmentName).toBe(
150 this.props.expectedEnviromentName || 'Server',
151 );
152 if (this.props.expectedErrorStack !== undefined) {
153 expect(this.state.error.stack).toContain(
154 this.props.expectedErrorStack,
155 );
156 }
157 } else {
158 expect(this.state.error.message).toBe(
159 'An error occurred in the Server Components render. The specific message is omitted in production' +
160 ' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
161 ' may provide additional details about the nature of the error.',
162 );
163 let expectedDigest = this.props.expectedMessage;
164 if (
165 expectedDigest.startsWith('{') ||
166 expectedDigest.startsWith('<')
167 ) {
168 expectedDigest = '{}';
169 } else if (expectedDigest.startsWith('[')) {
170 expectedDigest = '[]';
171 }
172 expect(this.state.error.digest).toContain(expectedDigest);
173 expect(this.state.error.environmentName).toBe(undefined);
174 expect(this.state.error.stack).toBe(
175 'Error: ' + this.state.error.message,
176 );
177 }
178 }
179 render() {
180 if (this.state.hasError) {
181 return this.state.error.message;
182 }
183 return this.props.children;
184 }
185 };
186
187 NoErrorExpected = class extends React.Component {
188 state = {hasError: false, error: null};
189 static getDerivedStateFromError(error) {
190 return {
191 hasError: true,
192 error,
193 };
194 }
195 componentDidMount() {
196 expect(this.state.error).toBe(null);
197 expect(this.state.hasError).toBe(false);
198 }
199 render() {
200 if (this.state.hasError) {
201 return this.state.error.message;
202 }
203 return this.props.children;
204 }
205 };
206 });
207
208 afterEach(() => {
209 jest.restoreAllMocks();
210 });
211
212 function clientReference(value) {
213 return Object.defineProperties(
214 function () {
215 throw new Error('Cannot call a client function from the server.');
216 },
217 {
218 $$typeof: {value: Symbol.for('react.client.reference')},
219 value: {value: value},
220 },
221 );
222 }
223
224 it('can render a Server Component', async () => {
225 function Bar({text}) {
226 return text.toUpperCase();
227 }
228 function Foo() {
229 return {
230 bar: (
231 <div>
232 <Bar text="a" />, <Bar text="b" />
233 </div>
234 ),
235 };
236 }
237 const transport = ReactNoopFlightServer.render({
238 foo: <Foo />,
239 });
240 const model = await ReactNoopFlightClient.read(transport);
241 expect(model).toEqual({
242 foo: {
243 bar: (
244 <div>
245 {'A'}
246 {', '}
247 {'B'}
248 </div>
249 ),
250 },
251 });
252 });
253
254 // @gate !__DEV__ || enableComponentPerformanceTrack
255 it('can render a Client Component using a module reference and render there', async () => {
256 function UserClient(props) {
257 return (
258 <span>
259 {props.greeting}, {props.name}
260 </span>
261 );
262 }
263 const User = clientReference(UserClient);
264
265 function Greeting({firstName, lastName}) {
266 return <User greeting="Hello" name={firstName + ' ' + lastName} />;
267 }
268
269 const model = {
270 greeting: <Greeting firstName="Seb" lastName="Smith" />,
271 };
272
273 const transport = ReactNoopFlightServer.render(model);
274
275 await act(async () => {
276 const rootModel = await ReactNoopFlightClient.read(transport);
277 const greeting = rootModel.greeting;
278 expect(getDebugInfo(greeting)).toEqual(
279 __DEV__
280 ? [
281 {time: 12},
282 {
283 name: 'Greeting',
284 env: 'Server',
285 key: null,
286 stack: ' in Object.<anonymous> (at **)',
287 props: {
288 firstName: 'Seb',
289 lastName: 'Smith',
290 },
291 },
292 {time: 13},
293 ]
294 : undefined,
295 );
296 ReactNoop.render(greeting);
297 });
298
299 expect(ReactNoop).toMatchRenderedOutput(<span>Hello, Seb Smith</span>);
300 });
301
302 // @gate !__DEV__ || enableComponentPerformanceTrack
303 it('can render a shared forwardRef Component', async () => {
304 const Greeting = React.forwardRef(function Greeting(
305 {firstName, lastName},
306 ref,
307 ) {
308 return (
309 <span ref={ref}>
310 Hello, {firstName} {lastName}
311 </span>
312 );
313 });
314
315 const root = <Greeting firstName="Seb" lastName="Smith" />;
316
317 const transport = ReactNoopFlightServer.render(root);
318
319 await act(async () => {
320 const result = await ReactNoopFlightClient.read(transport);
321 expect(getDebugInfo(result)).toEqual(
322 __DEV__
323 ? [
324 {time: 12},
325 {
326 name: 'Greeting',
327 env: 'Server',
328 key: null,
329 stack: ' in Object.<anonymous> (at **)',
330 props: {
331 firstName: 'Seb',
332 lastName: 'Smith',
333 },
334 },
335 {time: 13},
336 ]
337 : undefined,
338 );
339 ReactNoop.render(result);
340 });
341
342 expect(ReactNoop).toMatchRenderedOutput(<span>Hello, Seb Smith</span>);
343 });
344
345 it('can render an iterable as an array', async () => {
346 function ItemListClient(props) {
347 return <span>{props.items}</span>;
348 }
349 const ItemList = clientReference(ItemListClient);
350
351 function Items() {
352 const iterable = {
353 [Symbol.iterator]: function* () {
354 yield 'A';
355 yield 'B';
356 yield 'C';
357 },
358 };
359 return <ItemList items={iterable} />;
360 }
361
362 const model = <Items />;
363
364 const transport = ReactNoopFlightServer.render(model);
365
366 await act(async () => {
367 ReactNoop.render(await ReactNoopFlightClient.read(transport));
368 });
369
370 expect(ReactNoop).toMatchRenderedOutput(<span>ABC</span>);
371 });
372
373 it('can render an iterator as a single shot iterator', async () => {
374 const iterator = (function* () {
375 yield 'A';
376 yield 'B';
377 yield 'C';
378 })();
379
380 const transport = ReactNoopFlightServer.render(iterator);
381 const result = await ReactNoopFlightClient.read(transport);
382
383 // The iterator should be the same as itself.
384 expect(result[Symbol.iterator]()).toBe(result);
385
386 expect(Array.from(result)).toEqual(['A', 'B', 'C']);
387 // We've already consumed this iterator.
388 expect(Array.from(result)).toEqual([]);
389 });
390
391 it('can render a Generator Server Component as a fragment', async () => {
392 function ItemListClient(props) {
393 return <span>{props.children}</span>;
394 }
395 const ItemList = clientReference(ItemListClient);
396
397 function* Items() {
398 yield 'A';
399 yield 'B';
400 yield 'C';
401 }
402
403 const model = (
404 <ItemList>
405 <Items />
406 </ItemList>
407 );
408
409 const transport = ReactNoopFlightServer.render(model);
410
411 await act(async () => {
412 ReactNoop.render(await ReactNoopFlightClient.read(transport));
413 });
414
415 expect(ReactNoop).toMatchRenderedOutput(<span>ABC</span>);
416 });
417
418 it('can render undefined', async () => {
419 function Undefined() {
420 return undefined;
421 }
422
423 const model = <Undefined />;
424
425 const transport = ReactNoopFlightServer.render(model);
426
427 await act(async () => {
428 ReactNoop.render(await ReactNoopFlightClient.read(transport));
429 });
430
431 expect(ReactNoop).toMatchRenderedOutput(null);
432 });
433
434 // @gate FIXME
435 it('should transport undefined object values', async () => {
436 function ServerComponent(props) {
437 return 'prop' in props
438 ? `\`prop\` in props as '${props.prop}'`
439 : '`prop` not in props';
440 }
441 const ClientComponent = clientReference(ServerComponent);
442
443 const model = (
444 <>
445 <div>
446 Server: <ServerComponent prop={undefined} />
447 </div>
448 <div>
449 Client: <ClientComponent prop={undefined} />
450 </div>
451 </>
452 );
453
454 const transport = ReactNoopFlightServer.render(model);
455
456 await act(async () => {
457 ReactNoop.render(await ReactNoopFlightClient.read(transport));
458 });
459
460 expect(ReactNoop).toMatchRenderedOutput(
461 <>
462 <div>Server: `prop` in props as 'undefined'</div>
463 <div>Client: `prop` in props as 'undefined'</div>
464 </>,
465 );
466 });
467
468 it('can render an empty fragment', async () => {
469 function Empty() {
470 return <React.Fragment />;
471 }
472
473 const model = <Empty />;
474
475 const transport = ReactNoopFlightServer.render(model);
476
477 await act(async () => {
478 ReactNoop.render(await ReactNoopFlightClient.read(transport));
479 });
480
481 expect(ReactNoop).toMatchRenderedOutput(null);
482 });
483
484 it('can transport weird numbers', async () => {
485 const nums = [0, -0, Infinity, -Infinity, NaN];
486 function ComponentClient({prop}) {
487 expect(prop).not.toBe(nums);
488 expect(prop).toEqual(nums);
489 expect(prop.every((p, i) => Object.is(p, nums[i]))).toBe(true);
490 return `prop: ${prop}`;
491 }
492 const Component = clientReference(ComponentClient);
493
494 const model = <Component prop={nums} />;
495
496 const transport = ReactNoopFlightServer.render(model);
497
498 await act(async () => {
499 ReactNoop.render(await ReactNoopFlightClient.read(transport));
500 });
501
502 expect(ReactNoop).toMatchRenderedOutput(
503 // already checked -0 with expects above
504 'prop: 0,0,Infinity,-Infinity,NaN',
505 );
506 });
507
508 it('can transport BigInt', async () => {
509 function ComponentClient({prop}) {
510 return `prop: ${prop} (${typeof prop})`;
511 }
512 const Component = clientReference(ComponentClient);
513
514 const model = <Component prop={90071992547409910000n} />;
515
516 const transport = ReactNoopFlightServer.render(model);
517
518 await act(async () => {
519 ReactNoop.render(await ReactNoopFlightClient.read(transport));
520 });
521
522 expect(ReactNoop).toMatchRenderedOutput(
523 'prop: 90071992547409910000 (bigint)',
524 );
525 });
526
527 it('can transport Date', async () => {
528 function ComponentClient({prop}) {
529 return `prop: ${prop.toISOString()}`;
530 }
531 const Component = clientReference(ComponentClient);
532
533 const model = <Component prop={new Date(1234567890123)} />;
534
535 const transport = ReactNoopFlightServer.render(model);
536
537 await act(async () => {
538 ReactNoop.render(await ReactNoopFlightClient.read(transport));
539 });
540
541 expect(ReactNoop).toMatchRenderedOutput('prop: 2009-02-13T23:31:30.123Z');
542 });
543
544 it('can transport Map', async () => {
545 function ComponentClient({prop, selected}) {
546 return `
547 map: ${prop instanceof Map}
548 size: ${prop.size}
549 greet: ${prop.get('hi').greet}
550 content: ${JSON.stringify(Array.from(prop))}
551 selected: ${prop.get(selected)}
552 `;
553 }
554 const Component = clientReference(ComponentClient);
555
556 const objKey = {obj: 'key'};
557 const map = new Map([
558 ['hi', {greet: 'world'}],
559 [objKey, 123],
560 ]);
561 const model = <Component prop={map} selected={objKey} />;
562
563 const transport = ReactNoopFlightServer.render(model);
564
565 await act(async () => {
566 ReactNoop.render(await ReactNoopFlightClient.read(transport));
567 });
568
569 expect(ReactNoop).toMatchRenderedOutput(`
570 map: true
571 size: 2
572 greet: world
573 content: [["hi",{"greet":"world"}],[{"obj":"key"},123]]
574 selected: 123
575 `);
576 });
577
578 it('can transport Set', async () => {
579 function ComponentClient({prop, selected}) {
580 return `
581 set: ${prop instanceof Set}
582 size: ${prop.size}
583 hi: ${prop.has('hi')}
584 content: ${JSON.stringify(Array.from(prop))}
585 selected: ${prop.has(selected)}
586 `;
587 }
588 const Component = clientReference(ComponentClient);
589
590 const objKey = {obj: 'key'};
591 const set = new Set(['hi', objKey]);
592 const model = <Component prop={set} selected={objKey} />;
593
594 const transport = ReactNoopFlightServer.render(model);
595
596 await act(async () => {
597 ReactNoop.render(await ReactNoopFlightClient.read(transport));
598 });
599
600 expect(ReactNoop).toMatchRenderedOutput(`
601 set: true
602 size: 2
603 hi: true
604 content: ["hi",{"obj":"key"}]
605 selected: true
606 `);
607 });
608
609 it('can transport FormData (no blobs)', async () => {
610 function ComponentClient({prop}) {
611 return `
612 formData: ${prop instanceof FormData}
613 hi: ${prop.get('hi')}
614 multiple: ${prop.getAll('multiple')}
615 content: ${JSON.stringify(Array.from(prop))}
616 `;
617 }
618 const Component = clientReference(ComponentClient);
619
620 const formData = new FormData();
621 formData.append('hi', 'world');
622 formData.append('multiple', 1);
623 formData.append('multiple', 2);
624
625 const model = <Component prop={formData} />;
626
627 const transport = ReactNoopFlightServer.render(model);
628
629 await act(async () => {
630 ReactNoop.render(await ReactNoopFlightClient.read(transport));
631 });
632
633 expect(ReactNoop).toMatchRenderedOutput(`
634 formData: true
635 hi: world
636 multiple: 1,2
637 content: [["hi","world"],["multiple","1"],["multiple","2"]]
638 `);
639 });
640
641 it('can transport Date as a top-level value', async () => {
642 const date = new Date(0);
643 const transport = ReactNoopFlightServer.render(date);
644
645 let readValue;
646 await act(async () => {
647 readValue = await ReactNoopFlightClient.read(transport);
648 });
649
650 expect(readValue).toEqual(date);
651 });
652
653 it('can transport Error objects as values', async () => {
654 class CustomError extends Error {
655 constructor(message) {
656 super(message);
657 this.name = 'Custom';
658 }
659 }
660
661 function ComponentClient({prop}) {
662 return `
663 is error: ${prop instanceof Error}
664 name: ${prop.name}
665 message: ${prop.message}
666 stack: ${normalizeCodeLocInfo(prop.stack).split('\n').slice(0, 2).join('\n')}
667 environmentName: ${prop.environmentName}
668 `;
669 }
670 const Component = clientReference(ComponentClient);
671
672 function ServerComponent() {
673 const error = new CustomError('hello');
674 return <Component prop={error} />;
675 }
676
677 const transport = ReactNoopFlightServer.render(<ServerComponent />);
678
679 await act(async () => {
680 ReactNoop.render(await ReactNoopFlightClient.read(transport));
681 });
682
683 if (__DEV__) {
684 expect(ReactNoop).toMatchRenderedOutput(`
685 is error: true
686 name: Custom
687 message: hello
688 stack: Custom: hello
689 in ServerComponent (at **)
690 environmentName: Server
691 `);
692 } else {
693 expect(ReactNoop).toMatchRenderedOutput(`
694 is error: true
695 name: Error
696 message: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.
697 stack: Error: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.
698 environmentName: undefined
699 `);
700 }
701 });
702
703 it('can transport Error.cause', async () => {
704 function renderError(error) {
705 if (!(error instanceof Error)) {
706 return `${JSON.stringify(error)}`;
707 }
708 return `
709 is error: ${error instanceof Error}
710 name: ${error.name}
711 message: ${error.message}
712 stack: ${normalizeCodeLocInfo(error.stack).split('\n').slice(0, 2).join('\n')}
713 environmentName: ${error.environmentName}
714 cause: ${'cause' in error ? renderError(error.cause) : 'no cause'}`;
715 }
716 function ComponentClient({error}) {
717 return renderError(error);
718 }
719 const Component = clientReference(ComponentClient);
720
721 function ServerComponent() {
722 const cause = new TypeError('root cause', {
723 cause: {type: 'object cause'},
724 });
725 const error = new Error('hello', {cause});
726 return <Component error={error} />;
727 }
728
729 const transport = ReactNoopFlightServer.render(<ServerComponent />, {
730 onError(x) {
731 if (__DEV__) {
732 return 'a dev digest';
733 }
734 return `digest("${x.message}")`;
735 },
736 });
737
738 await act(() => {
739 ReactNoop.render(ReactNoopFlightClient.read(transport));
740 });
741
742 if (__DEV__) {
743 expect(ReactNoop).toMatchRenderedOutput(`
744 is error: true
745 name: Error
746 message: hello
747 stack: Error: hello
748 in ServerComponent (at **)
749 environmentName: Server
750 cause:
751 is error: true
752 name: TypeError
753 message: root cause
754 stack: TypeError: root cause
755 in ServerComponent (at **)
756 environmentName: Server
757 cause: {"type":"object cause"}`);
758 } else {
759 expect(ReactNoop).toMatchRenderedOutput(`
760 is error: true
761 name: Error
762 message: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.
763 stack: Error: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.
764 environmentName: undefined
765 cause: no cause`);
766 }
767 });
768
769 it('includes Error.cause in thrown errors', async () => {
770 function renderError(error) {
771 if (!(error instanceof Error)) {
772 return `${JSON.stringify(error)}`;
773 }
774 return `
775 is error: true
776 name: ${error.name}
777 message: ${error.message}
778 stack: ${normalizeCodeLocInfo(error.stack).split('\n').slice(0, 2).join('\n')}
779 environmentName: ${error.environmentName}
780 cause: ${'cause' in error ? renderError(error.cause) : 'no cause'}`;
781 }
782
783 function ServerComponent() {
784 const cause = new TypeError('root cause', {
785 cause: {type: 'object cause'},
786 });
787 const error = new Error('hello', {cause});
788 throw error;
789 }
790
791 const transport = ReactNoopFlightServer.render(<ServerComponent />, {
792 onError(x) {
793 if (__DEV__) {
794 return 'a dev digest';
795 }
796 return `digest("${x.message}")`;
797 },
798 });
799
800 let error;
801 try {
802 await act(() => {
803 ReactNoop.render(ReactNoopFlightClient.read(transport));
804 });
805 } catch (x) {
806 error = x;
807 }
808
809 if (__DEV__) {
810 expect(renderError(error)).toEqual(`
811 is error: true
812 name: Error
813 message: hello
814 stack: Error: hello
815 in ServerComponent (at **)
816 environmentName: Server
817 cause:
818 is error: true
819 name: TypeError
820 message: root cause
821 stack: TypeError: root cause
822 in ServerComponent (at **)
823 environmentName: Server
824 cause: {"type":"object cause"}`);
825 } else {
826 expect(renderError(error)).toEqual(`
827 is error: true
828 name: Error
829 message: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.
830 stack: Error: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.
831 environmentName: undefined
832 cause: no cause`);
833 }
834 });
835
836 it('can transport AggregateError', async () => {
837 function renderError(error) {
838 if (!(error instanceof Error)) {
839 return `${JSON.stringify(error)}`;
840 }
841 let result = `
842 is error: ${error instanceof AggregateError ? 'AggregateError' : 'Error'}
843 name: ${error.name}
844 message: ${error.message}
845 stack: ${normalizeCodeLocInfo(error.stack).split('\n').slice(0, 2).join('\n')}
846 environmentName: ${error.environmentName}
847 cause: ${'cause' in error ? renderError(error.cause) : 'no cause'}`;
848 if ('errors' in error) {
849 result += `
850 errors: [${error.errors.map(e => renderError(e)).join(',\n')}]`;
851 }
852 return result;
853 }
854 function ComponentClient({error}) {
855 return renderError(error);
856 }
857 const Component = clientReference(ComponentClient);
858
859 function ServerComponent() {
860 const error1 = new TypeError('first error');
861 const error2 = new RangeError('second error');
862 const error = new AggregateError([error1, error2], 'aggregate');
863 return <Component error={error} />;
864 }
865
866 const transport = ReactNoopFlightServer.render(<ServerComponent />, {
867 onError(x) {
868 if (__DEV__) {
869 return 'a dev digest';
870 }
871 return `digest("${x.message}")`;
872 },
873 });
874
875 await act(() => {
876 ReactNoop.render(ReactNoopFlightClient.read(transport));
877 });
878
879 if (__DEV__) {
880 expect(ReactNoop).toMatchRenderedOutput(`
881 is error: AggregateError
882 name: AggregateError
883 message: aggregate
884 stack: AggregateError: aggregate
885 in ServerComponent (at **)
886 environmentName: Server
887 cause: no cause
888 errors: [
889 is error: Error
890 name: TypeError
891 message: first error
892 stack: TypeError: first error
893 in ServerComponent (at **)
894 environmentName: Server
895 cause: no cause,
896
897 is error: Error
898 name: RangeError
899 message: second error
900 stack: RangeError: second error
901 in ServerComponent (at **)
902 environmentName: Server
903 cause: no cause]`);
904 } else {
905 expect(ReactNoop).toMatchRenderedOutput(`
906 is error: Error
907 name: Error
908 message: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.
909 stack: Error: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.
910 environmentName: undefined
911 cause: no cause`);
912 }
913 });
914
915 it('includes AggregateError.errors in thrown errors', async () => {
916 function renderError(error) {
917 if (!(error instanceof Error)) {
918 return `${JSON.stringify(error)}`;
919 }
920 let result = `
921 is error: ${error instanceof AggregateError ? 'AggregateError' : 'Error'}
922 name: ${error.name}
923 message: ${error.message}
924 stack: ${normalizeCodeLocInfo(error.stack).split('\n').slice(0, 2).join('\n')}
925 environmentName: ${error.environmentName}
926 cause: ${'cause' in error ? renderError(error.cause) : 'no cause'}`;
927 if ('errors' in error) {
928 result += `
929 errors: [${error.errors.map(e => renderError(e)).join(',\n')}]`;
930 }
931 return result;
932 }
933
934 function ServerComponent() {
935 const error1 = new TypeError('first error');
936 const error2 = new RangeError('second error');
937 const error3 = new Error('third error');
938 const error4 = new Error('fourth error');
939 const error5 = new Error('fifth error');
940 const error6 = new Error('sixth error');
941 const error = new AggregateError(
942 [error1, error2, error3, error4, error5, error6],
943 'aggregate',
944 );
945 throw error;
946 }
947
948 const transport = ReactNoopFlightServer.render(<ServerComponent />, {
949 onError(x) {
950 if (__DEV__) {
951 return 'a dev digest';
952 }
953 return `digest("${x.message}")`;
954 },
955 });
956
957 let error;
958 try {
959 await act(() => {
960 ReactNoop.render(ReactNoopFlightClient.read(transport));
961 });
962 } catch (x) {
963 error = x;
964 }
965
966 if (__DEV__) {
967 expect(renderError(error)).toEqual(`
968 is error: AggregateError
969 name: AggregateError
970 message: aggregate
971 stack: AggregateError: aggregate
972 in ServerComponent (at **)
973 environmentName: Server
974 cause: no cause
975 errors: [
976 is error: Error
977 name: TypeError
978 message: first error
979 stack: TypeError: first error
980 in ServerComponent (at **)
981 environmentName: Server
982 cause: no cause,
983
984 is error: Error
985 name: RangeError
986 message: second error
987 stack: RangeError: second error
988 in ServerComponent (at **)
989 environmentName: Server
990 cause: no cause,
991
992 is error: Error
993 name: Error
994 message: third error
995 stack: Error: third error
996 in ServerComponent (at **)
997 environmentName: Server
998 cause: no cause,
999
1000 is error: Error
1001 name: Error
1002 message: fourth error
1003 stack: Error: fourth error
1004 in ServerComponent (at **)
1005 environmentName: Server
1006 cause: no cause,
1007
1008 is error: Error
1009 name: Error
1010 message: fifth error
1011 stack: Error: fifth error
1012 in ServerComponent (at **)
1013 environmentName: Server
1014 cause: no cause,
1015
1016 is error: Error
1017 name: Error
1018 message: sixth error
1019 stack: Error: sixth error
1020 in ServerComponent (at **)
1021 environmentName: Server
1022 cause: no cause]`);
1023 } else {
1024 expect(renderError(error)).toEqual(`
1025 is error: Error
1026 name: Error
1027 message: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.
1028 stack: Error: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.
1029 environmentName: undefined
1030 cause: no cause`);
1031 }
1032 });
1033
1034 it('can transport cyclic objects', async () => {
1035 function ComponentClient({prop}) {
1036 expect(prop.obj.obj.obj).toBe(prop.obj.obj);
1037 }
1038 const Component = clientReference(ComponentClient);
1039
1040 const cyclic = {obj: null};
1041 cyclic.obj = cyclic;
1042 const model = <Component prop={cyclic} />;
1043
1044 const transport = ReactNoopFlightServer.render(model);
1045
1046 await act(async () => {
1047 ReactNoop.render(await ReactNoopFlightClient.read(transport));
1048 });
1049 });
1050
1051 it('can transport cyclic arrays', async () => {
1052 function ComponentClient({prop, obj}) {
1053 expect(prop[1]).toBe(prop);
1054 expect(prop[0]).toBe(obj);
1055 }
1056 const Component = clientReference(ComponentClient);
1057
1058 const obj = {};
1059 const cyclic = [obj];
1060 cyclic[1] = cyclic;
1061 const model = <Component prop={cyclic} obj={obj} />;
1062
1063 const transport = ReactNoopFlightServer.render(model);
1064
1065 await act(async () => {
1066 ReactNoop.render(await ReactNoopFlightClient.read(transport));
1067 });
1068 });
1069
1070 it('can render a lazy component as a shared component on the server', async () => {
1071 function SharedComponent({text}) {
1072 return (
1073 <div>
1074 shared<span>{text}</span>
1075 </div>
1076 );
1077 }
1078
1079 let load = null;
1080 const loadSharedComponent = () => {
1081 return new Promise(res => {
1082 load = () => res({default: SharedComponent});
1083 });
1084 };
1085
1086 const LazySharedComponent = React.lazy(loadSharedComponent);
1087
1088 function ServerComponent() {
1089 return (
1090 <React.Suspense fallback={'Loading...'}>
1091 <LazySharedComponent text={'a'} />
1092 </React.Suspense>
1093 );
1094 }
1095
1096 const transport = ReactNoopFlightServer.render(<ServerComponent />);
1097
1098 await act(async () => {
1099 const rootModel = await ReactNoopFlightClient.read(transport);
1100 ReactNoop.render(rootModel);
1101 });
1102 expect(ReactNoop).toMatchRenderedOutput('Loading...');
1103 await load();
1104
1105 await act(async () => {
1106 const rootModel = await ReactNoopFlightClient.read(transport);
1107 ReactNoop.render(rootModel);
1108 });
1109 expect(ReactNoop).toMatchRenderedOutput(
1110 <div>
1111 shared<span>a</span>
1112 </div>,
1113 );
1114 });
1115
1116 it('errors on a Lazy element being used in Component position', async () => {
1117 function SharedComponent({text}) {
1118 return (
1119 <div>
1120 shared<span>{text}</span>
1121 </div>
1122 );
1123 }
1124
1125 let load = null;
1126
1127 const LazyElementDisguisedAsComponent = React.lazy(() => {
1128 return new Promise(res => {
1129 load = () => res({default: <SharedComponent text={'a'} />});
1130 });
1131 });
1132
1133 function ServerComponent() {
1134 return (
1135 <React.Suspense fallback={'Loading...'}>
1136 <LazyElementDisguisedAsComponent text={'b'} />
1137 </React.Suspense>
1138 );
1139 }
1140
1141 const transport = ReactNoopFlightServer.render(<ServerComponent />);
1142
1143 await load();
1144
1145 await expect(async () => {
1146 await act(async () => {
1147 const rootModel = await ReactNoopFlightClient.read(transport);
1148 ReactNoop.render(rootModel);
1149 });
1150 }).rejects.toThrow(
1151 __DEV__
1152 ? 'Element type is invalid: expected a string (for built-in components) or a class/function ' +
1153 '(for composite components) but got: <div />. ' +
1154 'Did you accidentally export a JSX literal instead of a component?'
1155 : 'Element type is invalid: expected a string (for built-in components) or a class/function ' +
1156 '(for composite components) but got: object.',
1157 );
1158 expect(ReactNoop).toMatchRenderedOutput(null);
1159 });
1160
1161 it('can render a lazy element', async () => {
1162 function SharedComponent({text}) {
1163 return (
1164 <div>
1165 shared<span>{text}</span>
1166 </div>
1167 );
1168 }
1169
1170 let load = null;
1171
1172 const lazySharedElement = React.lazy(() => {
1173 return new Promise(res => {
1174 load = () => res({default: <SharedComponent text={'a'} />});
1175 });
1176 });
1177
1178 function ServerComponent() {
1179 return (
1180 <React.Suspense fallback={'Loading...'}>
1181 {lazySharedElement}
1182 </React.Suspense>
1183 );
1184 }
1185
1186 const transport = ReactNoopFlightServer.render(<ServerComponent />);
1187
1188 await act(async () => {
1189 const rootModel = await ReactNoopFlightClient.read(transport);
1190 ReactNoop.render(rootModel);
1191 });
1192 expect(ReactNoop).toMatchRenderedOutput('Loading...');
1193 await load();
1194
1195 await act(async () => {
1196 const rootModel = await ReactNoopFlightClient.read(transport);
1197 ReactNoop.render(rootModel);
1198 });
1199 expect(ReactNoop).toMatchRenderedOutput(
1200 <div>
1201 shared<span>a</span>
1202 </div>,
1203 );
1204 });
1205
1206 it('errors with lazy value in element position that resolves to Component', async () => {
1207 function SharedComponent({text}) {
1208 return (
1209 <div>
1210 shared<span>{text}</span>
1211 </div>
1212 );
1213 }
1214
1215 let load = null;
1216
1217 const componentDisguisedAsElement = React.lazy(() => {
1218 return new Promise(res => {
1219 load = () => res({default: SharedComponent});
1220 });
1221 });
1222
1223 function ServerComponent() {
1224 return (
1225 <React.Suspense fallback={'Loading...'}>
1226 {componentDisguisedAsElement}
1227 </React.Suspense>
1228 );
1229 }
1230
1231 const transport = ReactNoopFlightServer.render(<ServerComponent />);
1232
1233 await act(async () => {
1234 const rootModel = await ReactNoopFlightClient.read(transport);
1235 ReactNoop.render(rootModel);
1236 });
1237 expect(ReactNoop).toMatchRenderedOutput('Loading...');
1238 spyOnDevAndProd(console, 'error').mockImplementation(() => {});
1239 await load();
1240 expect(console.error).toHaveBeenCalledTimes(1);
1241 });
1242
1243 it('can render a lazy module reference', async () => {
1244 function ClientComponent() {
1245 return <div>I am client</div>;
1246 }
1247
1248 const ClientComponentReference = clientReference(ClientComponent);
1249
1250 let load = null;
1251 const loadClientComponentReference = () => {
1252 return new Promise(res => {
1253 load = () => res({default: ClientComponentReference});
1254 });
1255 };
1256
1257 const LazyClientComponentReference = React.lazy(
1258 loadClientComponentReference,
1259 );
1260
1261 function ServerComponent() {
1262 return (
1263 <React.Suspense fallback={'Loading...'}>
1264 <LazyClientComponentReference />
1265 </React.Suspense>
1266 );
1267 }
1268
1269 const transport = ReactNoopFlightServer.render(<ServerComponent />);
1270
1271 await act(async () => {
1272 const rootModel = await ReactNoopFlightClient.read(transport);
1273 ReactNoop.render(rootModel);
1274 });
1275 expect(ReactNoop).toMatchRenderedOutput('Loading...');
1276 await load();
1277
1278 await act(async () => {
1279 const rootModel = await ReactNoopFlightClient.read(transport);
1280 ReactNoop.render(rootModel);
1281 });
1282 expect(ReactNoop).toMatchRenderedOutput(<div>I am client</div>);
1283 });
1284
1285 it('should error if a non-serializable value is passed to a host component', async () => {
1286 function ClientImpl({children}) {
1287 return children;
1288 }
1289 const Client = clientReference(ClientImpl);
1290
1291 function EventHandlerProp() {
1292 return (
1293 <div className="foo" onClick={function () {}}>
1294 Test
1295 </div>
1296 );
1297 }
1298 function FunctionProp() {
1299 return <div>{function fn() {}}</div>;
1300 }
1301 function SymbolProp() {
1302 return <div foo={Symbol('foo')} />;
1303 }
1304
1305 const ref = React.createRef();
1306 function RefProp() {
1307 return <div ref={ref} />;
1308 }
1309
1310 function EventHandlerPropClient() {
1311 return (
1312 <Client className="foo" onClick={function () {}}>
1313 Test
1314 </Client>
1315 );
1316 }
1317 function FunctionChildrenClient() {
1318 return <Client>{function Component() {}}</Client>;
1319 }
1320 function FunctionPropClient() {
1321 return <Client foo={() => {}} />;
1322 }
1323 function SymbolPropClient() {
1324 return <Client foo={Symbol('foo')} />;
1325 }
1326
1327 function RefPropClient() {
1328 return <Client ref={ref} />;
1329 }
1330
1331 const options = {
1332 onError(x) {
1333 return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
1334 },
1335 };
1336 const event = ReactNoopFlightServer.render(<EventHandlerProp />, options);
1337 const fn = ReactNoopFlightServer.render(<FunctionProp />, options);
1338 const symbol = ReactNoopFlightServer.render(<SymbolProp />, options);
1339 const refs = ReactNoopFlightServer.render(<RefProp />, options);
1340 const eventClient = ReactNoopFlightServer.render(
1341 <EventHandlerPropClient />,
1342 options,
1343 );
1344 const fnChildrenClient = ReactNoopFlightServer.render(
1345 <FunctionChildrenClient />,
1346 options,
1347 );
1348 const fnClient = ReactNoopFlightServer.render(
1349 <FunctionPropClient />,
1350 options,
1351 );
1352 const symbolClient = ReactNoopFlightServer.render(
1353 <SymbolPropClient />,
1354 options,
1355 );
1356 const refsClient = ReactNoopFlightServer.render(<RefPropClient />, options);
1357
1358 function Render({promise}) {
1359 return use(promise);
1360 }
1361
1362 await act(() => {
1363 startTransition(() => {
1364 ReactNoop.render(
1365 <>
1366 <ErrorBoundary expectedMessage="Event handlers cannot be passed to Client Component props.">
1367 <Render promise={ReactNoopFlightClient.read(event)} />
1368 </ErrorBoundary>
1369 <ErrorBoundary
1370 expectedMessage={
1371 __DEV__
1372 ? 'Functions are not valid as a child of Client Components. This may happen if you return fn instead of <fn /> from render. Or maybe you meant to call this function rather than return it.'
1373 : 'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".'
1374 }>
1375 <Render promise={ReactNoopFlightClient.read(fn)} />
1376 </ErrorBoundary>
1377 <ErrorBoundary expectedMessage="Only global symbols received from Symbol.for(...) can be passed to Client Components.">
1378 <Render promise={ReactNoopFlightClient.read(symbol)} />
1379 </ErrorBoundary>
1380 <ErrorBoundary expectedMessage="Refs cannot be used in Server Components, nor passed to Client Components.">
1381 <Render promise={ReactNoopFlightClient.read(refs)} />
1382 </ErrorBoundary>
1383 <ErrorBoundary expectedMessage="Event handlers cannot be passed to Client Component props.">
1384 <Render promise={ReactNoopFlightClient.read(eventClient)} />
1385 </ErrorBoundary>
1386 <ErrorBoundary
1387 expectedMessage={
1388 __DEV__
1389 ? 'Functions are not valid as a child of Client Components. This may happen if you return Component instead of <Component /> from render. Or maybe you meant to call this function rather than return it.'
1390 : 'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".'
1391 }>
1392 <Render promise={ReactNoopFlightClient.read(fnChildrenClient)} />
1393 </ErrorBoundary>
1394 <ErrorBoundary
1395 expectedMessage={
1396 'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".'
1397 }>
1398 <Render promise={ReactNoopFlightClient.read(fnClient)} />
1399 </ErrorBoundary>
1400 <ErrorBoundary expectedMessage="Only global symbols received from Symbol.for(...) can be passed to Client Components.">
1401 <Render promise={ReactNoopFlightClient.read(symbolClient)} />
1402 </ErrorBoundary>
1403 <ErrorBoundary expectedMessage="Refs cannot be used in Server Components, nor passed to Client Components.">
1404 <Render promise={ReactNoopFlightClient.read(refsClient)} />
1405 </ErrorBoundary>
1406 </>,
1407 );
1408 });
1409 });
1410 });
1411
1412 it('should emit descriptions of errors in dev', async () => {
1413 const ClientErrorBoundary = clientReference(ErrorBoundary);
1414
1415 function Throw({value}) {
1416 throw value;
1417 }
1418
1419 function RenderInlined() {
1420 const inlinedElement = {
1421 $$typeof: Symbol.for('react.element'),
1422 type: () => {},
1423 key: null,
1424 ref: null,
1425 props: {},
1426 _owner: null,
1427 };
1428 return inlinedElement;
1429 }
1430
1431 // We wrap in lazy to ensure the errors throws lazily.
1432 const LazyInlined = React.lazy(async () => ({default: RenderInlined}));
1433
1434 const testCases = (
1435 <>
1436 <ClientErrorBoundary expectedMessage="This is a real Error.">
1437 <Throw value={new TypeError('This is a real Error.')} />
1438 </ClientErrorBoundary>
1439 <ClientErrorBoundary expectedMessage="This is a string error.">
1440 <Throw value="This is a string error." />
1441 </ClientErrorBoundary>
1442 <ClientErrorBoundary expectedMessage="{message: ..., extra: ..., nested: ...}">
1443 <Throw
1444 value={{
1445 message: 'This is a long message',
1446 extra: 'properties',
1447 nested: {more: 'prop'},
1448 }}
1449 />
1450 </ClientErrorBoundary>
1451 <ClientErrorBoundary
1452 expectedMessage={'{message: "Short", extra: ..., nested: ...}'}>
1453 <Throw
1454 value={{
1455 message: 'Short',
1456 extra: 'properties',
1457 nested: {more: 'prop'},
1458 }}
1459 />
1460 </ClientErrorBoundary>
1461 <ClientErrorBoundary expectedMessage="Symbol(hello)">
1462 <Throw value={Symbol('hello')} />
1463 </ClientErrorBoundary>
1464 <ClientErrorBoundary expectedMessage="123">
1465 <Throw value={123} />
1466 </ClientErrorBoundary>
1467 <ClientErrorBoundary expectedMessage="undefined">
1468 <Throw value={undefined} />
1469 </ClientErrorBoundary>
1470 <ClientErrorBoundary expectedMessage="<div/>">
1471 <Throw value={<div />} />
1472 </ClientErrorBoundary>
1473 <ClientErrorBoundary expectedMessage="function Foo() {}">
1474 <Throw value={function Foo() {}} />
1475 </ClientErrorBoundary>
1476 <ClientErrorBoundary expectedMessage={'["array"]'}>
1477 <Throw value={['array']} />
1478 </ClientErrorBoundary>
1479 <ClientErrorBoundary
1480 expectedMessage={
1481 'A React Element from an older version of React was rendered. ' +
1482 'This is not supported. It can happen if:\n' +
1483 '- Multiple copies of the "react" package is used.\n' +
1484 '- A library pre-bundled an old copy of "react" or "react/jsx-runtime".\n' +
1485 '- A compiler tries to "inline" JSX instead of using the runtime.'
1486 }>
1487 <LazyInlined />
1488 </ClientErrorBoundary>
1489 </>
1490 );
1491
1492 const transport = ReactNoopFlightServer.render(testCases, {
1493 onError(x) {
1494 if (__DEV__) {
1495 return 'a dev digest';
1496 }
1497 if (x instanceof Error) {
1498 return `digest("${x.message}")`;
1499 } else if (Array.isArray(x)) {
1500 return `digest([])`;
1501 } else if (typeof x === 'object' && x !== null) {
1502 return `digest({})`;
1503 }
1504 return `digest(${String(x)})`;
1505 },
1506 });
1507
1508 await act(() => {
1509 startTransition(() => {
1510 ReactNoop.render(ReactNoopFlightClient.read(transport));
1511 });
1512 });
1513 });
1514
1515 it('should include server components in error boundary stacks in dev', async () => {
1516 const ClientErrorBoundary = clientReference(ErrorBoundary);
1517
1518 function Throw({value}) {
1519 throw value;
1520 }
1521
1522 const expectedStack = __DEV__
1523 ? '\n in Throw' +
1524 '\n in div' +
1525 '\n in ErrorBoundary (at **)' +
1526 '\n in App'
1527 : '\n in div' + '\n in ErrorBoundary (at **)';
1528
1529 function App() {
1530 return (
1531 <ClientErrorBoundary
1532 expectedMessage="This is a real Error."
1533 expectedStack={expectedStack}>
1534 <div>
1535 <Throw value={new TypeError('This is a real Error.')} />
1536 </div>
1537 </ClientErrorBoundary>
1538 );
1539 }
1540
1541 const transport = ReactNoopFlightServer.render(<App />, {
1542 onError(x) {
1543 if (__DEV__) {
1544 return 'a dev digest';
1545 }
1546 if (x instanceof Error) {
1547 return `digest("${x.message}")`;
1548 } else if (Array.isArray(x)) {
1549 return `digest([])`;
1550 } else if (typeof x === 'object' && x !== null) {
1551 return `digest({})`;
1552 }
1553 return `digest(${String(x)})`;
1554 },
1555 });
1556
1557 await act(() => {
1558 startTransition(() => {
1559 ReactNoop.render(ReactNoopFlightClient.read(transport));
1560 });
1561 });
1562 });
1563
1564 it('should handle serialization errors in element inside error boundary', async () => {
1565 const ClientErrorBoundary = clientReference(ErrorBoundary);
1566
1567 const expectedStack = __DEV__
1568 ? '\n in div' + '\n in ErrorBoundary (at **)' + '\n in App'
1569 : '\n in ErrorBoundary (at **)';
1570
1571 function App() {
1572 return (
1573 <ClientErrorBoundary
1574 expectedMessage="Event handlers cannot be passed to Client Component props."
1575 expectedStack={expectedStack}>
1576 <div onClick={function () {}} />
1577 </ClientErrorBoundary>
1578 );
1579 }
1580
1581 const transport = ReactNoopFlightServer.render(<App />, {
1582 onError(x) {
1583 if (__DEV__) {
1584 return 'a dev digest';
1585 }
1586 if (x instanceof Error) {
1587 return `digest("${x.message}")`;
1588 } else if (Array.isArray(x)) {
1589 return `digest([])`;
1590 } else if (typeof x === 'object' && x !== null) {
1591 return `digest({})`;
1592 }
1593 return `digest(${String(x)})`;
1594 },
1595 });
1596
1597 await act(() => {
1598 startTransition(() => {
1599 ReactNoop.render(ReactNoopFlightClient.read(transport));
1600 });
1601 });
1602 });
1603
1604 it('should handle exotic stack frames', async () => {
1605 function ServerComponent() {
1606 const error = new Error('This is an error');
1607 const originalStackLines = error.stack.split('\n');
1608 // Fake a stack
1609 error.stack = [
1610 originalStackLines[0],
1611 // original
1612 // ' at ServerComponentError (file://~/react/packages/react-client/src/__tests__/ReactFlight-test.js:1166:19)',
1613 // nested eval (https://github.com/ChromeDevTools/devtools-frontend/blob/831be28facb4e85de5ee8c1acc4d98dfeda7a73b/test/unittests/front_end/panels/console/ErrorStackParser_test.ts#L198)
1614 ' at eval (eval at testFunction (inspected-page.html:29:11), <anonymous>:1:10)',
1615 // parens may be added by Webpack when bundle layers are used. They're also valid in directory names.
1616 ' at ServerComponentError (file://~/(some)(really)(exotic-directory)/ReactFlight-test.js:1166:19)',
1617 // anon function (https://github.com/ChromeDevTools/devtools-frontend/blob/831be28facb4e85de5ee8c1acc4d98dfeda7a73b/test/unittests/front_end/panels/console/ErrorStackParser_test.ts#L115C9-L115C35)
1618 ' at file:///testing.js:42:3',
1619 // async anon function (https://github.com/ChromeDevTools/devtools-frontend/blob/831be28facb4e85de5ee8c1acc4d98dfeda7a73b/test/unittests/front_end/panels/console/ErrorStackParser_test.ts#L130C9-L130C41)
1620 ' at async file:///testing.js:42:3',
1621 // third-party RSC frame
1622 // Ideally this would be a real frame produced by React not a mocked one.
1623 ' at ThirdParty (about://React/ThirdParty/file:///code/%5Broot%2520of%2520the%2520server%5D.js?42:1:1)',
1624 // We'll later filter this out based on line/column in `filterStackFrame`.
1625 ' at ThirdPartyModule (file:///file-with-index-source-map.js:52656:16374)',
1626 // host component in parent stack
1627 ' at div (<anonymous>)',
1628 ...originalStackLines.slice(2),
1629 ].join('\n');
1630 throw error;
1631 }
1632
1633 const findSourceMapURL = jest.fn(() => null);
1634 const errors = [];
1635 class MyErrorBoundary extends React.Component {
1636 state = {error: null};
1637 static getDerivedStateFromError(error) {
1638 return {error};
1639 }
1640 componentDidCatch(error, componentInfo) {
1641 errors.push(error);
1642 }
1643 render() {
1644 if (this.state.error) {
1645 return null;
1646 }
1647 return this.props.children;
1648 }
1649 }
1650 const ClientErrorBoundary = clientReference(MyErrorBoundary);
1651
1652 function App() {
1653 return ReactServer.createElement(
1654 ClientErrorBoundary,
1655 null,
1656 ReactServer.createElement(ServerComponent),
1657 );
1658 }
1659
1660 const transport = ReactNoopFlightServer.render(<App />, {
1661 onError(x) {
1662 if (__DEV__) {
1663 return 'a dev digest';
1664 }
1665 if (x instanceof Error) {
1666 return `digest("${x.message}")`;
1667 } else if (Array.isArray(x)) {
1668 return `digest([])`;
1669 } else if (typeof x === 'object' && x !== null) {
1670 return `digest({})`;
1671 }
1672 return `digest(${String(x)})`;
1673 },
1674 filterStackFrame(filename, functionName, lineNumber, columnNumber) {
1675 if (lineNumber === 52656 && columnNumber === 16374) {
1676 return false;
1677 }
1678 if (!filename) {
1679 // Allow anonymous
1680 return functionName === 'div';
1681 }
1682 return (
1683 !filename.startsWith('node:') &&
1684 !filename.includes('node_modules') &&
1685 // sourceURL from an ES module in `/code/[root of the server].js`
1686 filename !== 'file:///code/[root%20of%20the%20server].js'
1687 );
1688 },
1689 });
1690
1691 await act(() => {
1692 startTransition(() => {
1693 ReactNoop.render(
1694 ReactNoopFlightClient.read(transport, {findSourceMapURL}),
1695 );
1696 });
1697 });
1698
1699 if (__DEV__) {
1700 expect({
1701 errors: errors.map(getErrorForJestMatcher),
1702 findSourceMapURLCalls: findSourceMapURL.mock.calls,
1703 }).toEqual({
1704 errors: [
1705 {
1706 message: 'This is an error',
1707 name: 'Error',
1708 stack: expect.stringContaining(
1709 'Error: This is an error\n' +
1710 ' at eval (eval at testFunction (inspected-page.html:29:11),%20%3Canonymous%3E:1:35)\n' +
1711 ' at ServerComponentError (file://~/(some)(really)(exotic-directory)/ReactFlight-test.js:1166:19)\n' +
1712 ' at <anonymous> (file:///testing.js:42:3)\n' +
1713 ' at <anonymous> (file:///testing.js:42:3)\n' +
1714 ' at div (<anonymous>',
1715 ),
1716 digest: 'a dev digest',
1717 environmentName: 'Server',
1718 },
1719 ],
1720 findSourceMapURLCalls: expect.arrayContaining([
1721 // TODO: What should we request here? The outer (<anonymous>) or the inner (inspected-page.html)?
1722 ['inspected-page.html:29:11), <anonymous>', 'Server'],
1723 [
1724 'file://~/(some)(really)(exotic-directory)/ReactFlight-test.js',
1725 'Server',
1726 ],
1727 ['file:///testing.js', 'Server'],
1728 ['', 'Server'],
1729 ]),
1730 });
1731 } else {
1732 expect(errors.map(getErrorForJestMatcher)).toEqual([
1733 {
1734 message:
1735 'An error occurred in the Server Components render. The specific message is omitted in production' +
1736 ' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
1737 ' may provide additional details about the nature of the error.',
1738 stack:
1739 'Error: An error occurred in the Server Components render. The specific message is omitted in production' +
1740 ' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
1741 ' may provide additional details about the nature of the error.',
1742 digest: 'digest("This is an error")',
1743 },
1744 ]);
1745 }
1746 });
1747
1748 it('should include server components in warning stacks', async () => {
1749 function Component() {
1750 // Trigger key warning
1751 return <div>{[<span />]}</div>;
1752 }
1753 const ClientComponent = clientReference(Component);
1754
1755 function Indirection({children}) {
1756 return children;
1757 }
1758
1759 function App() {
1760 // We use the ReactServer runtime here to get the Server owner.
1761 return ReactServer.createElement(
1762 Indirection,
1763 null,
1764 ReactServer.createElement(ClientComponent),
1765 );
1766 }
1767
1768 const transport = ReactNoopFlightServer.render(<App />);
1769
1770 await act(() => {
1771 startTransition(() => {
1772 ReactNoop.render(ReactNoopFlightClient.read(transport));
1773 });
1774 });
1775 assertConsoleErrorDev([
1776 'Each child in a list should have a unique "key" prop.\n' +
1777 '\n' +
1778 'Check the render method of `Component`. See https://react.dev/link/warning-keys for more information.\n' +
1779 ' in span (at **)\n' +
1780 ' in Component (at **)\n' +
1781 ' in App (at **)',
1782 ]);
1783 });
1784
1785 it('should trigger the inner most error boundary inside a Client Component', async () => {
1786 function ServerComponent() {
1787 throw new Error('This was thrown in the Server Component.');
1788 }
1789
1790 function ClientComponent({children}) {
1791 // This should catch the error thrown by the Server Component, even though it has already happened.
1792 // We currently need to wrap it in a div because as it's set up right now, a lazy reference will
1793 // throw during reconciliation which will trigger the parent of the error boundary.
1794 // This is similar to how these will suspend the parent if it's a direct child of a Suspense boundary.
1795 // That's a bug.
1796 return (
1797 <ErrorBoundary expectedMessage="This was thrown in the Server Component.">
1798 <div>{children}</div>
1799 </ErrorBoundary>
1800 );
1801 }
1802
1803 const ClientComponentReference = clientReference(ClientComponent);
1804
1805 function Server() {
1806 return (
1807 <ClientComponentReference>
1808 <ServerComponent />
1809 </ClientComponentReference>
1810 );
1811 }
1812
1813 const data = ReactNoopFlightServer.render(<Server />, {
1814 onError(x) {
1815 // ignore
1816 },
1817 });
1818
1819 function Client({promise}) {
1820 return use(promise);
1821 }
1822
1823 await act(() => {
1824 startTransition(() => {
1825 ReactNoop.render(
1826 <NoErrorExpected>
1827 <Client promise={ReactNoopFlightClient.read(data)} />
1828 </NoErrorExpected>,
1829 );
1830 });
1831 });
1832 });
1833
1834 it('should warn in DEV if a toJSON instance is passed to a host component', () => {
1835 const obj = {
1836 toJSON() {
1837 return 123;
1838 },
1839 };
1840 const transport = ReactNoopFlightServer.render(<input value={obj} />);
1841 assertConsoleErrorDev([
1842 'Only plain objects can be passed to Client Components from Server Components. ' +
1843 'Objects with toJSON methods are not supported. ' +
1844 'Convert it manually to a simple value before passing it to props.\n' +
1845 ' <input value={{toJSON: ...}}>\n' +
1846 ' ^^^^^^^^^^^^^^^',
1847 ]);
1848
1849 ReactNoopFlightClient.read(transport);
1850 assertConsoleErrorDev([
1851 'Only plain objects can be passed to Client Components from Server Components. ' +
1852 'Objects with toJSON methods are not supported. ' +
1853 'Convert it manually to a simple value before passing it to props.\n' +
1854 ' <input value={{toJSON: ...}}>\n' +
1855 ' ^^^^^^^^^^^^^^^\n' +
1856 ' at (<anonymous>)',
1857 ]);
1858 });
1859
1860 it('should warn in DEV if a toJSON instance is passed to a host component child', () => {
1861 class MyError extends Error {
1862 toJSON() {
1863 return 123;
1864 }
1865 }
1866 const transport = ReactNoopFlightServer.render(
1867 <div>Womp womp: {new MyError('spaghetti')}</div>,
1868 );
1869 assertConsoleErrorDev([
1870 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' +
1871 ' <div>Womp womp: {Error}</div>\n' +
1872 ' ^^^^^^^',
1873 ]);
1874
1875 ReactNoopFlightClient.read(transport);
1876 assertConsoleErrorDev([
1877 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' +
1878 ' <div>Womp womp: {Error}</div>\n' +
1879 ' ^^^^^^^\n' +
1880 ' at (<anonymous>)',
1881 ]);
1882 });
1883
1884 it('should warn in DEV if a special object is passed to a host component', () => {
1885 const transport = ReactNoopFlightServer.render(<input value={Math} />);
1886 assertConsoleErrorDev([
1887 'Only plain objects can be passed to Client Components from Server Components. ' +
1888 'Math objects are not supported.\n' +
1889 ' <input value={Math}>\n' +
1890 ' ^^^^^^',
1891 ]);
1892
1893 ReactNoopFlightClient.read(transport);
1894 assertConsoleErrorDev([
1895 'Only plain objects can be passed to Client Components from Server Components. ' +
1896 'Math objects are not supported.\n' +
1897 ' <input value={Math}>\n' +
1898 ' ^^^^^^\n' +
1899 ' at (<anonymous>)',
1900 ]);
1901 });
1902
1903 it('should warn in DEV if an object with symbols is passed to a host component', () => {
1904 const transport = ReactNoopFlightServer.render(
1905 <input value={{[Symbol.iterator]: {}}} />,
1906 );
1907 assertConsoleErrorDev([
1908 'Only plain objects can be passed to Client Components from Server Components. ' +
1909 'Objects with symbol properties like Symbol.iterator are not supported.\n' +
1910 ' <input value={{}}>\n' +
1911 ' ^^^^',
1912 ]);
1913
1914 ReactNoopFlightClient.read(transport);
1915 assertConsoleErrorDev([
1916 'Only plain objects can be passed to Client Components from Server Components. ' +
1917 'Objects with symbol properties like Symbol.iterator are not supported.\n' +
1918 ' <input value={{}}>\n' +
1919 ' ^^^^\n' +
1920 ' at (<anonymous>)',
1921 ]);
1922 });
1923
1924 it('should warn in DEV if a toJSON instance is passed to a Client Component', () => {
1925 const obj = {
1926 toJSON() {
1927 return 123;
1928 },
1929 };
1930 function ClientImpl({value}) {
1931 return <div>{value}</div>;
1932 }
1933 const Client = clientReference(ClientImpl);
1934 const transport = ReactNoopFlightServer.render(<Client value={obj} />);
1935 assertConsoleErrorDev([
1936 'Only plain objects can be passed to Client Components from Server Components. ' +
1937 'Objects with toJSON methods are not supported. ' +
1938 'Convert it manually to a simple value before passing it to props.\n' +
1939 ' <... value={{toJSON: ...}}>\n' +
1940 ' ^^^^^^^^^^^^^^^',
1941 ]);
1942
1943 ReactNoopFlightClient.read(transport);
1944 assertConsoleErrorDev([
1945 'Only plain objects can be passed to Client Components from Server Components. ' +
1946 'Objects with toJSON methods are not supported. ' +
1947 'Convert it manually to a simple value before passing it to props.\n' +
1948 ' <... value={{toJSON: ...}}>\n' +
1949 ' ^^^^^^^^^^^^^^^\n' +
1950 ' at (<anonymous>)',
1951 ]);
1952 });
1953
1954 it('should warn in DEV if a toJSON instance is passed to a Client Component child', () => {
1955 const obj = {
1956 toJSON() {
1957 return 123;
1958 },
1959 };
1960 function ClientImpl({children}) {
1961 return <div>{children}</div>;
1962 }
1963 const Client = clientReference(ClientImpl);
1964 const transport = ReactNoopFlightServer.render(
1965 <Client>Current date: {obj}</Client>,
1966 );
1967 assertConsoleErrorDev([
1968 'Only plain objects can be passed to Client Components from Server Components. ' +
1969 'Objects with toJSON methods are not supported. ' +
1970 'Convert it manually to a simple value before passing it to props.\n' +
1971 ' <>Current date: {{toJSON: ...}}</>\n' +
1972 ' ^^^^^^^^^^^^^^^',
1973 ]);
1974
1975 ReactNoopFlightClient.read(transport);
1976 assertConsoleErrorDev([
1977 'Only plain objects can be passed to Client Components from Server Components. ' +
1978 'Objects with toJSON methods are not supported. ' +
1979 'Convert it manually to a simple value before passing it to props.\n' +
1980 ' <>Current date: {{toJSON: ...}}</>\n' +
1981 ' ^^^^^^^^^^^^^^^\n' +
1982 ' at (<anonymous>)',
1983 ]);
1984 });
1985
1986 it('should warn in DEV if a special object is passed to a Client Component', () => {
1987 function ClientImpl({value}) {
1988 return <div>{value}</div>;
1989 }
1990 const Client = clientReference(ClientImpl);
1991 const transport = ReactNoopFlightServer.render(<Client value={Math} />);
1992 assertConsoleErrorDev([
1993 'Only plain objects can be passed to Client Components from Server Components. ' +
1994 'Math objects are not supported.\n' +
1995 ' <... value={Math}>\n' +
1996 ' ^^^^^^',
1997 ]);
1998
1999 ReactNoopFlightClient.read(transport);
2000 assertConsoleErrorDev([
2001 'Only plain objects can be passed to Client Components from Server Components. ' +
2002 'Math objects are not supported.\n' +
2003 ' <... value={Math}>\n' +
2004 ' ^^^^^^\n' +
2005 ' at (<anonymous>)',
2006 ]);
2007 });
2008
2009 it('should warn in DEV if an object with symbols is passed to a Client Component', () => {
2010 function ClientImpl({value}) {
2011 return <div>{value}</div>;
2012 }
2013 const Client = clientReference(ClientImpl);
2014 assertConsoleErrorDev([]);
2015 const transport = ReactNoopFlightServer.render(
2016 <Client value={{[Symbol.iterator]: {}}} />,
2017 );
2018 assertConsoleErrorDev([
2019 'Only plain objects can be passed to Client Components from Server Components. ' +
2020 'Objects with symbol properties like Symbol.iterator are not supported.\n' +
2021 ' <... value={{}}>\n' +
2022 ' ^^^^',
2023 ]);
2024
2025 ReactNoopFlightClient.read(transport);
2026
2027 assertConsoleErrorDev([
2028 'Only plain objects can be passed to Client Components from Server Components. ' +
2029 'Objects with symbol properties like Symbol.iterator are not supported.\n' +
2030 ' <... value={{}}>\n' +
2031 ' ^^^^\n' +
2032 ' in (at **)',
2033 ]);
2034 });
2035
2036 it('should warn in DEV if a special object is passed to a nested object in Client Component', () => {
2037 function ClientImpl({value}) {
2038 return <div>{value}</div>;
2039 }
2040 const Client = clientReference(ClientImpl);
2041 const transport = ReactNoopFlightServer.render(
2042 <Client value={{[Symbol.iterator]: {}}} />,
2043 );
2044 ReactNoopFlightClient.read(transport);
2045
2046 assertConsoleErrorDev([
2047 'Only plain objects can be passed to Client Components from Server Components. ' +
2048 'Objects with symbol properties like Symbol.iterator are not supported.\n' +
2049 ' <... value={{}}>\n' +
2050 ' ^^^^',
2051 'Only plain objects can be passed to Client Components from Server Components. ' +
2052 'Objects with symbol properties like Symbol.iterator are not supported.\n' +
2053 ' <... value={{}}>\n' +
2054 ' ^^^^\n' +
2055 ' at (<anonymous>)',
2056 ]);
2057 });
2058
2059 it('should warn in DEV if a special object is passed to a nested array in Client Component', () => {
2060 function ClientImpl({value}) {
2061 return <div>{value}</div>;
2062 }
2063 const Client = clientReference(ClientImpl);
2064 const transport = ReactNoopFlightServer.render(
2065 <Client value={['looooong string takes up noise', Math, <h1>hi</h1>]} />,
2066 );
2067 ReactNoopFlightClient.read(transport);
2068 assertConsoleErrorDev([
2069 'Only plain objects can be passed to Client Components from Server Components. ' +
2070 'Math objects are not supported.\n' +
2071 ' [..., Math, <h1/>]\n' +
2072 ' ^^^^',
2073 'Only plain objects can be passed to Client Components from Server Components. ' +
2074 'Math objects are not supported.\n' +
2075 ' [..., Math, <h1/>]\n' +
2076 ' ^^^^\n' +
2077 ' at (<anonymous>)',
2078 ]);
2079 });
2080
2081 it('should serialize an own __proto__ property nested among siblings without disturbing them', async () => {
2082 // `__proto__` here is a real own enumerable data property (not the
2083 // prototype). It sits between sibling keys and holds an object value, which
2084 // is the case most likely to regress if the serializer used a plain
2085 // `obj.__proto__ = value` assignment: that would hit the prototype setter,
2086 // dropping the key and mutating the holder's prototype instead.
2087 const value = {a: 1};
2088 Object.defineProperty(value, '__proto__', {
2089 value: {nested: true},
2090 enumerable: true,
2091 writable: true,
2092 configurable: true,
2093 });
2094 value.b = 2;
2095
2096 const transport = ReactNoopFlightServer.render(value);
2097 assertConsoleErrorDev([
2098 'Expected not to serialize an object with own property `__proto__`. ' +
2099 'When parsed this property will be omitted.\n' +
2100 ' {a: 1, __proto__: {nested: true}, b: 2}\n' +
2101 ' ^^^^^^^^^^^^^^',
2102 ]);
2103
2104 const decoder = new TextDecoder();
2105 const payload = transport
2106 .map(chunk => (typeof chunk === 'string' ? chunk : decoder.decode(chunk)))
2107 .join('');
2108 // The legacy key is serialized as ordinary data, in source order, with its
2109 // object value intact and without clobbering its sibling properties.
2110 expect(payload).toContain('"a":1,"__proto__":{"nested":true},"b":2');
2111
2112 const model = await ReactNoopFlightClient.read(transport);
2113 assertConsoleErrorDev([
2114 'Expected not to serialize an object with own property `__proto__`. ' +
2115 'When parsed this property will be omitted.\n' +
2116 ' {a: 1, __proto__: {nested: true}, b: 2}\n' +
2117 ' ^^^^^^^^^^^^^^\n' +
2118 ' in (at **)',
2119 ]);
2120 // On the client the legacy key is omitted, but its siblings survive intact
2121 // and the holder's prototype is untouched.
2122 expect(Object.prototype.hasOwnProperty.call(model, '__proto__')).toBe(
2123 false,
2124 );
2125 expect(Object.getPrototypeOf(model)).toBe(Object.prototype);
2126 expect(model.a).toBe(1);
2127 expect(model.b).toBe(2);
2128 });
2129
2130 it('should NOT warn in DEV for key getters', () => {
2131 const transport = ReactNoopFlightServer.render(<div key="a" />);
2132 ReactNoopFlightClient.read(transport);
2133 });
2134
2135 it('should warn in DEV a child is missing keys on server component', () => {
2136 function NoKey({children}) {
2137 return ReactServer.createElement('div', {
2138 key: "this has a key but parent doesn't",
2139 });
2140 }
2141 // While we're on the server we need to have the Server version active to track component stacks.
2142 jest.resetModules();
2143 jest.mock('react', () => ReactServer);
2144 const transport = ReactNoopFlightServer.render(
2145 ReactServer.createElement(
2146 'div',
2147 null,
2148 Array(6).fill(ReactServer.createElement(NoKey)),
2149 ),
2150 );
2151 jest.resetModules();
2152 jest.mock('react', () => React);
2153 ReactNoopFlightClient.read(transport);
2154 assertConsoleErrorDev([
2155 'Each child in a list should have a unique "key" prop. ' +
2156 'See https://react.dev/link/warning-keys for more information.\n' +
2157 ' in NoKey (at **)',
2158 'Each child in a list should have a unique "key" prop. ' +
2159 'See https://react.dev/link/warning-keys for more information.\n' +
2160 ' in NoKey (at **)',
2161 ]);
2162 });
2163
2164 it('should warn in DEV a child is missing keys on a fragment', () => {
2165 // While we're on the server we need to have the Server version active to track component stacks.
2166 jest.resetModules();
2167 jest.mock('react', () => ReactServer);
2168 const transport = ReactNoopFlightServer.render(
2169 ReactServer.createElement(
2170 'div',
2171 null,
2172 Array(6).fill(ReactServer.createElement(ReactServer.Fragment)),
2173 ),
2174 );
2175 jest.resetModules();
2176 jest.mock('react', () => React);
2177 ReactNoopFlightClient.read(transport);
2178 assertConsoleErrorDev([
2179 'Each child in a list should have a unique "key" prop. ' +
2180 'See https://react.dev/link/warning-keys for more information.\n' +
2181 ' in Fragment (at **)',
2182 'Each child in a list should have a unique "key" prop. ' +
2183 'See https://react.dev/link/warning-keys for more information.\n' +
2184 ' in Fragment (at **)',
2185 ]);
2186 });
2187
2188 it('should warn in DEV a child is missing keys in client component', async () => {
2189 function ParentClient({children}) {
2190 return children;
2191 }
2192
2193 await act(async () => {
2194 const Parent = clientReference(ParentClient);
2195 const transport = ReactNoopFlightServer.render(
2196 <Parent>{Array(6).fill(<div>no key</div>)}</Parent>,
2197 );
2198 ReactNoopFlightClient.read(transport);
2199
2200 ReactNoop.render(await ReactNoopFlightClient.read(transport));
2201 });
2202 assertConsoleErrorDev([
2203 'Each child in a list should have a unique "key" prop.\n\n' +
2204 'Check the top-level render call using <ParentClient>. ' +
2205 'See https://react.dev/link/warning-keys for more information.\n' +
2206 ' in div (at **)',
2207 ]);
2208 });
2209
2210 it('should error if a class instance is passed to a host component', () => {
2211 class Foo {
2212 method() {}
2213 }
2214 const errors = [];
2215 ReactNoopFlightServer.render(<input value={new Foo()} />, {
2216 onError(x) {
2217 errors.push(x.message);
2218 },
2219 });
2220
2221 expect(errors).toEqual([
2222 'Only plain objects, and a few built-ins, can be passed to Client Components ' +
2223 'from Server Components. Classes or null prototypes are not supported.' +
2224 (__DEV__
2225 ? '\n' + ' <input value={{}}>\n' + ' ^^^^'
2226 : '\n' + ' {value: {}}\n' + ' ^^'),
2227 ]);
2228 });
2229
2230 it('should error if useContext is called()', () => {
2231 function ServerComponent() {
2232 return ReactServer.useContext();
2233 }
2234 const errors = [];
2235 ReactNoopFlightServer.render(<ServerComponent />, {
2236 onError(x) {
2237 errors.push(x.message);
2238 },
2239 });
2240 expect(errors).toEqual(['ReactServer.useContext is not a function']);
2241 });
2242
2243 it('should error if a context without a client reference is passed to use()', () => {
2244 const Context = React.createContext();
2245 function ServerComponent() {
2246 return ReactServer.use(Context);
2247 }
2248 const errors = [];
2249 ReactNoopFlightServer.render(<ServerComponent />, {
2250 onError(x) {
2251 errors.push(x.message);
2252 },
2253 });
2254 expect(errors).toEqual([
2255 'Cannot read a Client Context from a Server Component.',
2256 ]);
2257 });
2258
2259 it('should error if a client reference is passed to use()', () => {
2260 const Context = React.createContext();
2261 const ClientContext = clientReference(Context);
2262 function ServerComponent() {
2263 return ReactServer.use(ClientContext);
2264 }
2265 const errors = [];
2266 ReactNoopFlightServer.render(<ServerComponent />, {
2267 onError(x) {
2268 errors.push(x.message);
2269 },
2270 });
2271 expect(errors).toEqual([
2272 'Cannot read a Client Context from a Server Component.',
2273 ]);
2274 });
2275
2276 describe('Hooks', () => {
2277 function DivWithId({children}) {
2278 const id = ReactServer.useId();
2279 return <div prop={id}>{children}</div>;
2280 }
2281
2282 it('should support useId', async () => {
2283 function App() {
2284 return (
2285 <>
2286 <DivWithId />
2287 <DivWithId />
2288 </>
2289 );
2290 }
2291
2292 const transport = ReactNoopFlightServer.render(<App />);
2293 await act(async () => {
2294 ReactNoop.render(await ReactNoopFlightClient.read(transport));
2295 });
2296 expect(ReactNoop).toMatchRenderedOutput(
2297 <>
2298 <div prop="_S_1_" />
2299 <div prop="_S_2_" />
2300 </>,
2301 );
2302 });
2303
2304 it('accepts an identifier prefix that prefixes generated ids', async () => {
2305 function App() {
2306 return (
2307 <>
2308 <DivWithId />
2309 <DivWithId />
2310 </>
2311 );
2312 }
2313
2314 const transport = ReactNoopFlightServer.render(<App />, {
2315 identifierPrefix: 'foo',
2316 });
2317 await act(async () => {
2318 ReactNoop.render(await ReactNoopFlightClient.read(transport));
2319 });
2320 expect(ReactNoop).toMatchRenderedOutput(
2321 <>
2322 <div prop="_fooS_1_" />
2323 <div prop="_fooS_2_" />
2324 </>,
2325 );
2326 });
2327
2328 it('[TODO] it does not warn if you render a server element passed to a client module reference twice on the client when using useId', async () => {
2329 // @TODO Today if you render a Server Component with useId and pass it to a Client Component and that Client Component renders the element in two or more
2330 // places the id used on the server will be duplicated in the client. This is a deviation from the guarantees useId makes for Fizz/Client and is a consequence
2331 // of the fact that the Server Component is actually rendered on the server and is reduced to a set of host elements before being passed to the Client component
2332 // so the output passed to the Client has no knowledge of the useId use. In the future we would like to add a DEV warning when this happens. For now
2333 // we just accept that it is a nuance of useId in Flight
2334 function App() {
2335 const id = ReactServer.useId();
2336 const div = <div prop={id}>{id}</div>;
2337 return <ClientDoublerModuleRef el={div} />;
2338 }
2339
2340 function ClientDoubler({el}) {
2341 Scheduler.log('ClientDoubler');
2342 return (
2343 <>
2344 {el}
2345 {el}
2346 </>
2347 );
2348 }
2349
2350 const ClientDoublerModuleRef = clientReference(ClientDoubler);
2351
2352 const transport = ReactNoopFlightServer.render(<App />);
2353 assertLog([]);
2354
2355 await act(async () => {
2356 ReactNoop.render(await ReactNoopFlightClient.read(transport));
2357 });
2358
2359 assertLog(['ClientDoubler']);
2360 expect(ReactNoop).toMatchRenderedOutput(
2361 <>
2362 <div prop="_S_1_">_S_1_</div>
2363 <div prop="_S_1_">_S_1_</div>
2364 </>,
2365 );
2366 });
2367 });
2368
2369 // @gate enableTaint
2370 it('errors when a tainted object is serialized', async () => {
2371 function UserClient({user}) {
2372 return <span>{user.name}</span>;
2373 }
2374 const User = clientReference(UserClient);
2375
2376 const user = {
2377 name: 'Seb',
2378 age: 'rather not say',
2379 };
2380 ReactServer.experimental_taintObjectReference(
2381 "Don't pass the raw user object to the client",
2382 user,
2383 );
2384 const errors = [];
2385 ReactNoopFlightServer.render(<User user={user} />, {
2386 onError(x) {
2387 errors.push(x.message);
2388 },
2389 });
2390
2391 expect(errors).toEqual(["Don't pass the raw user object to the client"]);
2392 });
2393
2394 // @gate enableTaint
2395 it('errors with a specific message when a tainted function is serialized', async () => {
2396 function UserClient({user}) {
2397 return <span>{user.name}</span>;
2398 }
2399 const User = clientReference(UserClient);
2400
2401 function change() {}
2402 ReactServer.experimental_taintObjectReference(
2403 'A change handler cannot be passed to a client component',
2404 change,
2405 );
2406 const errors = [];
2407 ReactNoopFlightServer.render(<User onChange={change} />, {
2408 onError(x) {
2409 errors.push(x.message);
2410 },
2411 });
2412
2413 expect(errors).toEqual([
2414 'A change handler cannot be passed to a client component',
2415 ]);
2416 });
2417
2418 // @gate enableTaint
2419 it('errors when a tainted string is serialized', async () => {
2420 function UserClient({user}) {
2421 return <span>{user.name}</span>;
2422 }
2423 const User = clientReference(UserClient);
2424
2425 const process = {
2426 env: {
2427 SECRET: '3e971ecc1485fe78625598bf9b6f85db',
2428 },
2429 };
2430 ReactServer.experimental_taintUniqueValue(
2431 'Cannot pass a secret token to the client',
2432 process,
2433 process.env.SECRET,
2434 );
2435
2436 const errors = [];
2437 ReactNoopFlightServer.render(<User token={process.env.SECRET} />, {
2438 onError(x) {
2439 errors.push(x.message);
2440 },
2441 });
2442
2443 expect(errors).toEqual(['Cannot pass a secret token to the client']);
2444
2445 // This just ensures the process object is kept alive for the life time of
2446 // the test since we're simulating a global as an example.
2447 expect(process.env.SECRET).toBe('3e971ecc1485fe78625598bf9b6f85db');
2448 });
2449
2450 // @gate enableTaint
2451 it('errors when a tainted bigint is serialized', async () => {
2452 function UserClient({user}) {
2453 return <span>{user.name}</span>;
2454 }
2455 const User = clientReference(UserClient);
2456
2457 const currentUser = {
2458 name: 'Seb',
2459 token: BigInt('0x3e971ecc1485fe78625598bf9b6f85dc'),
2460 };
2461 ReactServer.experimental_taintUniqueValue(
2462 'Cannot pass a secret token to the client',
2463 currentUser,
2464 currentUser.token,
2465 );
2466
2467 function App({user}) {
2468 return <User token={user.token} />;
2469 }
2470
2471 const errors = [];
2472 ReactNoopFlightServer.render(<App user={currentUser} />, {
2473 onError(x) {
2474 errors.push(x.message);
2475 },
2476 });
2477
2478 expect(errors).toEqual(['Cannot pass a secret token to the client']);
2479 });
2480
2481 // @gate enableTaint
2482 it('errors when a tainted binary value is serialized', async () => {
2483 function UserClient({user}) {
2484 return <span>{user.name}</span>;
2485 }
2486 const User = clientReference(UserClient);
2487
2488 const currentUser = {
2489 name: 'Seb',
2490 token: new Uint32Array([0x3e971ecc, 0x1485fe78, 0x625598bf, 0x9b6f85dd]),
2491 };
2492 ReactServer.experimental_taintUniqueValue(
2493 'Cannot pass a secret token to the client',
2494 currentUser,
2495 currentUser.token,
2496 );
2497
2498 function App({user}) {
2499 const clone = user.token.slice();
2500 return <User token={clone} />;
2501 }
2502
2503 const errors = [];
2504 ReactNoopFlightServer.render(<App user={currentUser} />, {
2505 onError(x) {
2506 errors.push(x.message);
2507 },
2508 });
2509
2510 expect(errors).toEqual(['Cannot pass a secret token to the client']);
2511 });
2512
2513 // @gate enableTaint
2514 it('keep a tainted value tainted until the end of any pending requests', async () => {
2515 function UserClient({user}) {
2516 return <span>{user.name}</span>;
2517 }
2518 const User = clientReference(UserClient);
2519
2520 function getUser() {
2521 const user = {
2522 name: 'Seb',
2523 token: '3e971ecc1485fe78625598bf9b6f85db',
2524 };
2525 ReactServer.experimental_taintUniqueValue(
2526 'Cannot pass a secret token to the client',
2527 user,
2528 user.token,
2529 );
2530 return user;
2531 }
2532
2533 function App() {
2534 const user = getUser();
2535 const derivedValue = {...user};
2536 // A garbage collection can happen at any time. Even before the end of
2537 // this request. This would clean up the user object.
2538 gc();
2539 // We should still block the tainted value.
2540 return <User user={derivedValue} />;
2541 }
2542
2543 let errors = [];
2544 ReactNoopFlightServer.render(<App />, {
2545 onError(x) {
2546 errors.push(x.message);
2547 },
2548 });
2549
2550 expect(errors).toEqual(['Cannot pass a secret token to the client']);
2551
2552 // After the previous requests finishes, the token can be rendered again.
2553
2554 errors = [];
2555 ReactNoopFlightServer.render(
2556 <User user={{token: '3e971ecc1485fe78625598bf9b6f85db'}} />,
2557 {
2558 onError(x) {
2559 errors.push(x.message);
2560 },
2561 },
2562 );
2563
2564 expect(errors).toEqual([]);
2565 });
2566
2567 it('preserves state when keying a server component', async () => {
2568 function StatefulClient({name}) {
2569 const [state] = React.useState(name.toLowerCase());
2570 return state;
2571 }
2572 const Stateful = clientReference(StatefulClient);
2573
2574 function Item({item}) {
2575 return (
2576 <div>
2577 {item}
2578 <Stateful name={item} />
2579 </div>
2580 );
2581 }
2582
2583 function Items({items}) {
2584 return items.map(item => {
2585 return <Item key={item} item={item} />;
2586 });
2587 }
2588
2589 const transport = ReactNoopFlightServer.render(
2590 <Items items={['A', 'B', 'C']} />,
2591 );
2592
2593 await act(async () => {
2594 ReactNoop.render(await ReactNoopFlightClient.read(transport));
2595 });
2596
2597 expect(ReactNoop).toMatchRenderedOutput(
2598 <>
2599 <div>Aa</div>
2600 <div>Bb</div>
2601 <div>Cc</div>
2602 </>,
2603 );
2604
2605 const transport2 = ReactNoopFlightServer.render(
2606 <Items items={['B', 'A', 'D', 'C']} />,
2607 );
2608
2609 await act(async () => {
2610 ReactNoop.render(await ReactNoopFlightClient.read(transport2));
2611 });
2612
2613 expect(ReactNoop).toMatchRenderedOutput(
2614 <>
2615 <div>Bb</div>
2616 <div>Aa</div>
2617 <div>Dd</div>
2618 <div>Cc</div>
2619 </>,
2620 );
2621 });
2622
2623 it('does not inherit keys of children inside a server component', async () => {
2624 function StatefulClient({name, initial}) {
2625 const [state] = React.useState(initial);
2626 return state;
2627 }
2628 const Stateful = clientReference(StatefulClient);
2629
2630 function Item({item, initial}) {
2631 // This key is the key of the single item of this component.
2632 // It's NOT part of the key of the list the parent component is
2633 // in.
2634 return (
2635 <div key={item}>
2636 {item}
2637 <Stateful name={item} initial={initial} />
2638 </div>
2639 );
2640 }
2641
2642 function IndirectItem({item, initial}) {
2643 // Even though we render two items with the same child key this key
2644 // should not conflict, because the key belongs to the parent slot.
2645 return <Item key="parent" item={item} initial={initial} />;
2646 }
2647
2648 // These items don't have their own keys because they're in a fixed set
2649 const transport = ReactNoopFlightServer.render(
2650 <>
2651 <Item item="A" initial={1} />
2652 <Item item="B" initial={2} />
2653 <IndirectItem item="C" initial={5} />
2654 <IndirectItem item="C" initial={6} />
2655 </>,
2656 );
2657
2658 await act(async () => {
2659 ReactNoop.render(await ReactNoopFlightClient.read(transport));
2660 });
2661
2662 expect(ReactNoop).toMatchRenderedOutput(
2663 <>
2664 <div>A1</div>
2665 <div>B2</div>
2666 <div>C5</div>
2667 <div>C6</div>
2668 </>,
2669 );
2670
2671 // This means that they shouldn't swap state when the properties update
2672 const transport2 = ReactNoopFlightServer.render(
2673 <>
2674 <Item item="B" initial={3} />
2675 <Item item="A" initial={4} />
2676 <IndirectItem item="C" initial={7} />
2677 <IndirectItem item="C" initial={8} />
2678 </>,
2679 );
2680
2681 await act(async () => {
2682 ReactNoop.render(await ReactNoopFlightClient.read(transport2));
2683 });
2684
2685 expect(ReactNoop).toMatchRenderedOutput(
2686 <>
2687 <div>B3</div>
2688 <div>A4</div>
2689 <div>C5</div>
2690 <div>C6</div>
2691 </>,
2692 );
2693 });
2694
2695 it('shares state between single return and array return in a parent', async () => {
2696 function StatefulClient({name, initial}) {
2697 const [state] = React.useState(initial);
2698 return state;
2699 }
2700 const Stateful = clientReference(StatefulClient);
2701
2702 function Item({item, initial}) {
2703 // This key is the key of the single item of this component.
2704 // It's NOT part of the key of the list the parent component is
2705 // in.
2706 return (
2707 <span key={item}>
2708 {item}
2709 <Stateful name={item} initial={initial} />
2710 </span>
2711 );
2712 }
2713
2714 function Condition({condition}) {
2715 if (condition) {
2716 return <Item item="A" initial={1} />;
2717 }
2718 // The first item in the fragment is the same as the single item.
2719 return (
2720 <>
2721 <Item item="A" initial={2} />
2722 <Item item="B" initial={3} />
2723 </>
2724 );
2725 }
2726
2727 function ConditionPlain({condition}) {
2728 if (condition) {
2729 return (
2730 <span>
2731 C
2732 <Stateful name="C" initial={1} />
2733 </span>
2734 );
2735 }
2736 // The first item in the fragment is the same as the single item.
2737 return (
2738 <>
2739 <span>
2740 C
2741 <Stateful name="C" initial={2} />
2742 </span>
2743 <span>
2744 D
2745 <Stateful name="D" initial={3} />
2746 </span>
2747 </>
2748 );
2749 }
2750
2751 const transport = ReactNoopFlightServer.render(
2752 // This two item wrapper ensures we're already one step inside an array.
2753 // A single item is not the same as a set when it's nested one level.
2754 <>
2755 <div>
2756 <Condition condition={true} />
2757 </div>
2758 <div>
2759 <ConditionPlain condition={true} />
2760 </div>
2761 <div key="keyed">
2762 <ConditionPlain condition={true} />
2763 </div>
2764 </>,
2765 );
2766
2767 await act(async () => {
2768 ReactNoop.render(await ReactNoopFlightClient.read(transport));
2769 });
2770
2771 expect(ReactNoop).toMatchRenderedOutput(
2772 <>
2773 <div>
2774 <span>A1</span>
2775 </div>
2776 <div>
2777 <span>C1</span>
2778 </div>
2779 <div>
2780 <span>C1</span>
2781 </div>
2782 </>,
2783 );
2784
2785 const transport2 = ReactNoopFlightServer.render(
2786 <>
2787 <div>
2788 <Condition condition={false} />
2789 </div>
2790 <div>
2791 <ConditionPlain condition={false} />
2792 </div>
2793 {null}
2794 <div key="keyed">
2795 <ConditionPlain condition={false} />
2796 </div>
2797 </>,
2798 );
2799
2800 await act(async () => {
2801 ReactNoop.render(await ReactNoopFlightClient.read(transport2));
2802 });
2803
2804 // We're intentionally breaking from the semantics here for efficiency of the protocol.
2805 // In the case a Server Component inside a fragment is itself implicitly keyed but its
2806 // return value has a key, then we need a wrapper fragment. This means they can't
2807 // reconcile. To solve this we would need to add a wrapper fragment to every Server
2808 // Component just in case it returns a fragment later which is a lot.
2809 expect(ReactNoop).toMatchRenderedOutput(
2810 <>
2811 <div>
2812 <span>A2{/* This should be A1 ideally */}</span>
2813 <span>B3</span>
2814 </div>
2815 <div>
2816 <span>C1</span>
2817 <span>D3</span>
2818 </div>
2819 <div>
2820 <span>C1</span>
2821 <span>D3</span>
2822 </div>
2823 </>,
2824 );
2825 });
2826
2827 it('shares state between single return and array return in a set', async () => {
2828 function StatefulClient({name, initial}) {
2829 const [state] = React.useState(initial);
2830 return state;
2831 }
2832 const Stateful = clientReference(StatefulClient);
2833
2834 function Item({item, initial}) {
2835 // This key is the key of the single item of this component.
2836 // It's NOT part of the key of the list the parent component is
2837 // in.
2838 return (
2839 <span key={item}>
2840 {item}
2841 <Stateful name={item} initial={initial} />
2842 </span>
2843 );
2844 }
2845
2846 function Condition({condition}) {
2847 if (condition) {
2848 return <Item item="A" initial={1} />;
2849 }
2850 // The first item in the fragment is the same as the single item.
2851 return (
2852 <>
2853 <Item item="A" initial={2} />
2854 <Item item="B" initial={3} />
2855 </>
2856 );
2857 }
2858
2859 function ConditionPlain({condition}) {
2860 if (condition) {
2861 return (
2862 <span>
2863 C
2864 <Stateful name="C" initial={1} />
2865 </span>
2866 );
2867 }
2868 // The first item in the fragment is the same as the single item.
2869 return (
2870 <>
2871 <span>
2872 C
2873 <Stateful name="C" initial={2} />
2874 </span>
2875 <span>
2876 D
2877 <Stateful name="D" initial={3} />
2878 </span>
2879 </>
2880 );
2881 }
2882
2883 const transport = ReactNoopFlightServer.render(
2884 // This two item wrapper ensures we're already one step inside an array.
2885 // A single item is not the same as a set when it's nested one level.
2886 <div>
2887 <Condition condition={true} />
2888 <ConditionPlain condition={true} />
2889 <ConditionPlain key="keyed" condition={true} />
2890 </div>,
2891 );
2892
2893 await act(async () => {
2894 ReactNoop.render(await ReactNoopFlightClient.read(transport));
2895 });
2896
2897 expect(ReactNoop).toMatchRenderedOutput(
2898 <div>
2899 <span>A1</span>
2900 <span>C1</span>
2901 <span>C1</span>
2902 </div>,
2903 );
2904
2905 const transport2 = ReactNoopFlightServer.render(
2906 <div>
2907 <Condition condition={false} />
2908 <ConditionPlain condition={false} />
2909 {null}
2910 <ConditionPlain key="keyed" condition={false} />
2911 </div>,
2912 );
2913
2914 await act(async () => {
2915 ReactNoop.render(await ReactNoopFlightClient.read(transport2));
2916 });
2917
2918 // We're intentionally breaking from the semantics here for efficiency of the protocol.
2919 // The issue with this test scenario is that when the Server Component is in a set,
2920 // the next slot can't be conditionally a fragment or single. That would require wrapping
2921 // in an additional fragment for every single child just in case it every expands to a
2922 // fragment.
2923 expect(ReactNoop).toMatchRenderedOutput(
2924 <div>
2925 <span>A2{/* Should be A1 */}</span>
2926 <span>B3</span>
2927 <span>C2{/* Should be C1 */}</span>
2928 <span>D3</span>
2929 <span>C2{/* Should be C1 */}</span>
2930 <span>D3</span>
2931 </div>,
2932 );
2933 });
2934
2935 it('preserves state with keys split across async work', async () => {
2936 let resolve;
2937 const promise = new Promise(r => (resolve = r));
2938
2939 function StatefulClient({name}) {
2940 const [state] = React.useState(name.toLowerCase());
2941 return state;
2942 }
2943 const Stateful = clientReference(StatefulClient);
2944
2945 function Item({name}) {
2946 if (name === 'A') {
2947 return promise.then(() => (
2948 <div>
2949 {name}
2950 <Stateful name={name} />
2951 </div>
2952 ));
2953 }
2954 return (
2955 <div>
2956 {name}
2957 <Stateful name={name} />
2958 </div>
2959 );
2960 }
2961
2962 const transport = ReactNoopFlightServer.render([
2963 <Item key="a" name="A" />,
2964 null,
2965 ]);
2966
2967 // Create a gap in the stream
2968 await resolve();
2969
2970 await act(async () => {
2971 ReactNoop.render(await ReactNoopFlightClient.read(transport));
2972 });
2973
2974 expect(ReactNoop).toMatchRenderedOutput(<div>Aa</div>);
2975
2976 const transport2 = ReactNoopFlightServer.render([
2977 null,
2978 <Item key="a" name="B" />,
2979 ]);
2980
2981 await act(async () => {
2982 ReactNoop.render(await ReactNoopFlightClient.read(transport2));
2983 });
2984
2985 expect(ReactNoop).toMatchRenderedOutput(<div>Ba</div>);
2986 });
2987
2988 it('shares state when moving keyed Server Components that render fragments', async () => {
2989 function StatefulClient({name, initial}) {
2990 const [state] = React.useState(initial);
2991 return <span>{state}</span>;
2992 }
2993 const Stateful = clientReference(StatefulClient);
2994
2995 function ServerComponent({item, initial}) {
2996 return [
2997 <Stateful key="a" initial={'a' + initial} />,
2998 <Stateful key="b" initial={'b' + initial} />,
2999 ];
3000 }
3001
3002 const transport = ReactNoopFlightServer.render(
3003 <div>
3004 <ServerComponent key="A" initial={1} />
3005 <ServerComponent key="B" initial={2} />
3006 </div>,
3007 );
3008
3009 await act(async () => {
3010 ReactNoop.render(await ReactNoopFlightClient.read(transport));
3011 });
3012
3013 expect(ReactNoop).toMatchRenderedOutput(
3014 <div>
3015 <span>a1</span>
3016 <span>b1</span>
3017 <span>a2</span>
3018 <span>b2</span>
3019 </div>,
3020 );
3021
3022 // We swap the Server Components and the state of each child inside each fragment should move.
3023 // Really the Fragment itself moves.
3024 const transport2 = ReactNoopFlightServer.render(
3025 <div>
3026 <ServerComponent key="B" initial={4} />
3027 <ServerComponent key="A" initial={3} />
3028 </div>,
3029 );
3030
3031 await act(async () => {
3032 ReactNoop.render(await ReactNoopFlightClient.read(transport2));
3033 });
3034
3035 expect(ReactNoop).toMatchRenderedOutput(
3036 <div>
3037 <span>a2</span>
3038 <span>b2</span>
3039 <span>a1</span>
3040 <span>b1</span>
3041 </div>,
3042 );
3043 });
3044
3045 // @gate enableAsyncIterableChildren
3046 it('shares state when moving keyed Server Components that render async iterables', async () => {
3047 function StatefulClient({name, initial}) {
3048 const [state] = React.useState(initial);
3049 return <span>{state}</span>;
3050 }
3051 const Stateful = clientReference(StatefulClient);
3052
3053 async function* ServerComponent({item, initial}) {
3054 yield <Stateful key="a" initial={'a' + initial} />;
3055 yield <Stateful key="b" initial={'b' + initial} />;
3056 }
3057
3058 const transport = ReactNoopFlightServer.render(
3059 <div>
3060 <ServerComponent key="A" initial={1} />
3061 <ServerComponent key="B" initial={2} />
3062 </div>,
3063 );
3064
3065 await act(async () => {
3066 ReactNoop.render(await ReactNoopFlightClient.read(transport));
3067 });
3068
3069 expect(ReactNoop).toMatchRenderedOutput(
3070 <div>
3071 <span>a1</span>
3072 <span>b1</span>
3073 <span>a2</span>
3074 <span>b2</span>
3075 </div>,
3076 );
3077
3078 // We swap the Server Components and the state of each child inside each fragment should move.
3079 // Really the Fragment itself moves.
3080 const transport2 = ReactNoopFlightServer.render(
3081 <div>
3082 <ServerComponent key="B" initial={4} />
3083 <ServerComponent key="A" initial={3} />
3084 </div>,
3085 );
3086
3087 await act(async () => {
3088 ReactNoop.render(await ReactNoopFlightClient.read(transport2));
3089 });
3090
3091 expect(ReactNoop).toMatchRenderedOutput(
3092 <div>
3093 <span>a2</span>
3094 <span>b2</span>
3095 <span>a1</span>
3096 <span>b1</span>
3097 </div>,
3098 );
3099 });
3100
3101 // @gate !__DEV__ || enableComponentPerformanceTrack
3102 it('preserves debug info for server-to-server pass through', async () => {
3103 function ThirdPartyLazyComponent() {
3104 return <span>!</span>;
3105 }
3106
3107 const lazy = React.lazy(async function myLazy() {
3108 return {
3109 default: <ThirdPartyLazyComponent />,
3110 };
3111 });
3112
3113 function ThirdPartyComponent() {
3114 return <span>stranger</span>;
3115 }
3116
3117 function ThirdPartyFragmentComponent() {
3118 return [<span key="1">Who</span>, ' ', <span key="2">dis?</span>];
3119 }
3120
3121 function ServerComponent({transport}) {
3122 // This is a Server Component that receives other Server Components from a third party.
3123 const children = ReactNoopFlightClient.read(transport);
3124 return <div>Hello, {children}</div>;
3125 }
3126
3127 const promiseComponent = Promise.resolve(<ThirdPartyComponent />);
3128
3129 const thirdPartyTransport = ReactNoopFlightServer.render(
3130 [promiseComponent, lazy, <ThirdPartyFragmentComponent key="3" />],
3131 {
3132 environmentName: 'third-party',
3133 },
3134 );
3135
3136 // Wait for the lazy component to initialize
3137 await 0;
3138
3139 const transport = ReactNoopFlightServer.render(
3140 <ServerComponent transport={thirdPartyTransport} />,
3141 );
3142
3143 await act(async () => {
3144 const result = await ReactNoopFlightClient.read(transport);
3145 expect(getDebugInfo(result)).toEqual(
3146 __DEV__
3147 ? [
3148 {time: gate(flags => flags.enableAsyncDebugInfo) ? 22 : 20},
3149 {
3150 name: 'ServerComponent',
3151 env: 'Server',
3152 key: null,
3153 stack: ' in Object.<anonymous> (at **)',
3154 props: {
3155 transport: expect.arrayContaining([]),
3156 },
3157 },
3158 {time: gate(flags => flags.enableAsyncDebugInfo) ? 53 : 21},
3159 ]
3160 : undefined,
3161 );
3162
3163 const thirdPartyChildren = await result.props.children[1];
3164 // We expect the debug info to be transferred from the inner stream to the outer.
3165 expect(getDebugInfo(await thirdPartyChildren[0])).toEqual(
3166 __DEV__
3167 ? [
3168 {time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22}, // Clamped to the start
3169 {
3170 name: 'ThirdPartyComponent',
3171 env: 'third-party',
3172 key: null,
3173 stack: ' in Object.<anonymous> (at **)',
3174 props: {},
3175 },
3176 {time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22},
3177 {time: gate(flags => flags.enableAsyncDebugInfo) ? 55 : 23}, // This last one is when the promise resolved into the first party.
3178 ]
3179 : undefined,
3180 );
3181 expect(getDebugInfo(thirdPartyChildren[1])).toEqual(
3182 __DEV__
3183 ? [
3184 {time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22}, // Clamped to the start
3185 {
3186 name: 'ThirdPartyLazyComponent',
3187 env: 'third-party',
3188 key: null,
3189 stack: ' in myLazy (at **)\n in lazyInitializer (at **)',
3190 props: {},
3191 },
3192 {time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22},
3193 ]
3194 : undefined,
3195 );
3196 const fragment = thirdPartyChildren[2];
3197 expect(getDebugInfo(fragment)).toEqual(
3198 __DEV__
3199 ? [
3200 {time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22},
3201 {
3202 name: 'ThirdPartyFragmentComponent',
3203 env: 'third-party',
3204 key: '3',
3205 stack: ' in Object.<anonymous> (at **)',
3206 props: {},
3207 },
3208 {time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22},
3209 ]
3210 : undefined,
3211 );
3212 expect(getDebugInfo(fragment.props.children[0])).toEqual(
3213 __DEV__ ? null : undefined,
3214 );
3215 ReactNoop.render(result);
3216 });
3217
3218 expect(ReactNoop).toMatchRenderedOutput(
3219 <div>
3220 Hello, <span>stranger</span>
3221 <span>!</span>
3222 <span>Who</span> <span>dis?</span>
3223 </div>,
3224 );
3225 });
3226
3227 it('preserves debug info for keyed Fragment', async () => {
3228 function App() {
3229 return ReactServer.createElement(
3230 ReactServer.Fragment,
3231 {key: 'app'},
3232 ReactServer.createElement('h1', null, 'App'),
3233 ReactServer.createElement('div', null, 'Child'),
3234 );
3235 }
3236
3237 const transport = ReactNoopFlightServer.render(
3238 ReactServer.createElement(
3239 ReactServer.Fragment,
3240 null,
3241 ReactServer.createElement('link', {key: 'styles'}),
3242 ReactServer.createElement(App, null),
3243 ),
3244 );
3245
3246 await act(async () => {
3247 const root = await ReactNoopFlightClient.read(transport);
3248
3249 const fragment = root[1];
3250 expect(getDebugInfo(fragment)).toEqual(
3251 __DEV__
3252 ? [
3253 {time: 12},
3254 {
3255 name: 'App',
3256 env: 'Server',
3257 key: null,
3258 stack: ' in Object.<anonymous> (at **)',
3259 props: {},
3260 },
3261 {time: 13},
3262 ]
3263 : undefined,
3264 );
3265 // Making sure debug info doesn't get added multiple times on Fragment children
3266 expect(getDebugInfo(fragment[0])).toEqual(__DEV__ ? null : undefined);
3267 const fragmentChild = fragment[0].props.children[0];
3268 expect(getDebugInfo(fragmentChild)).toEqual(__DEV__ ? null : undefined);
3269
3270 ReactNoop.render(root);
3271 });
3272
3273 expect(ReactNoop).toMatchRenderedOutput(
3274 <>
3275 <link />
3276 <h1>App</h1>
3277 <div>Child</div>
3278 </>,
3279 );
3280 });
3281
3282 // @gate enableAsyncIterableChildren && enableComponentPerformanceTrack
3283 it('preserves debug info for server-to-server pass through of async iterables', async () => {
3284 let resolve;
3285 const iteratorPromise = new Promise(r => (resolve = r));
3286
3287 async function* ThirdPartyAsyncIterableComponent({item, initial}) {
3288 yield <span key="1">Who</span>;
3289 yield <span key="2">dis?</span>;
3290 resolve();
3291 }
3292
3293 function Keyed({children}) {
3294 // Keying this should generate a fragment.
3295 return children;
3296 }
3297
3298 function ServerComponent({transport}) {
3299 // This is a Server Component that receives other Server Components from a third party.
3300 const children = ReactServer.use(
3301 ReactNoopFlightClient.read(transport),
3302 ).root;
3303 return (
3304 <div>
3305 <Keyed key="keyed">{children}</Keyed>
3306 </div>
3307 );
3308 }
3309
3310 const thirdPartyTransport = ReactNoopFlightServer.render(
3311 {root: <ThirdPartyAsyncIterableComponent />},
3312 {
3313 environmentName: 'third-party',
3314 },
3315 );
3316
3317 // Wait for the iterator to finish
3318 await iteratorPromise;
3319
3320 await 0; // One more tick for the return value / closing.
3321
3322 const transport = ReactNoopFlightServer.render(
3323 <ServerComponent transport={thirdPartyTransport} />,
3324 );
3325
3326 await act(async () => {
3327 const result = await ReactNoopFlightClient.read(transport);
3328 expect(getDebugInfo(result)).toEqual(
3329 __DEV__
3330 ? [
3331 {time: 16},
3332 {
3333 name: 'ServerComponent',
3334 env: 'Server',
3335 key: null,
3336 stack: ' in Object.<anonymous> (at **)',
3337 props: {
3338 transport: expect.arrayContaining([]),
3339 },
3340 },
3341 {time: 31},
3342 ]
3343 : undefined,
3344 );
3345 const thirdPartyFragment = await result.props.children;
3346 expect(getDebugInfo(thirdPartyFragment)).toEqual(
3347 __DEV__
3348 ? [
3349 {time: 32},
3350 {
3351 name: 'Keyed',
3352 env: 'Server',
3353 key: 'keyed',
3354 stack: ' in ServerComponent (at **)',
3355 props: {
3356 children: {},
3357 },
3358 },
3359 {time: 33},
3360 ]
3361 : undefined,
3362 );
3363 // We expect the debug info to be transferred from the inner stream to the outer.
3364 expect(getDebugInfo(thirdPartyFragment.props.children)).toEqual(
3365 __DEV__
3366 ? [
3367 {time: 33}, // Clamp to the start
3368 {
3369 name: 'ThirdPartyAsyncIterableComponent',
3370 env: 'third-party',
3371 key: null,
3372 stack: ' in Object.<anonymous> (at **)',
3373 props: {},
3374 },
3375 {time: 33},
3376 ]
3377 : undefined,
3378 );
3379
3380 ReactNoop.render(result);
3381 });
3382
3383 expect(ReactNoop).toMatchRenderedOutput(
3384 <div>
3385 <span>Who</span>
3386 <span>dis?</span>
3387 </div>,
3388 );
3389 });
3390
3391 // @gate !__DEV__ || enableComponentPerformanceTrack
3392 it('preserves debug info for server-to-server through use()', async () => {
3393 function ThirdPartyComponent() {
3394 return 'hi';
3395 }
3396
3397 function ServerComponent({transport}) {
3398 // This is a Server Component that receives other Server Components from a third party.
3399 const text = ReactServer.use(ReactNoopFlightClient.read(transport));
3400 return <div>{text.toUpperCase()}</div>;
3401 }
3402
3403 const thirdPartyTransport = ReactNoopFlightServer.render(
3404 <ThirdPartyComponent />,
3405 {
3406 environmentName: 'third-party',
3407 },
3408 );
3409
3410 const transport = ReactNoopFlightServer.render(
3411 <ServerComponent transport={thirdPartyTransport} />,
3412 );
3413
3414 await act(async () => {
3415 const result = await ReactNoopFlightClient.read(transport);
3416 expect(getDebugInfo(result)).toEqual(
3417 __DEV__
3418 ? [
3419 {time: 16},
3420 {
3421 name: 'ServerComponent',
3422 env: 'Server',
3423 key: null,
3424 stack: ' in Object.<anonymous> (at **)',
3425 props: {
3426 transport: expect.arrayContaining([]),
3427 },
3428 },
3429 {time: 16},
3430 {
3431 name: 'ThirdPartyComponent',
3432 env: 'third-party',
3433 key: null,
3434 stack: ' in Object.<anonymous> (at **)',
3435 props: {},
3436 },
3437 {time: 16},
3438 {time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 17},
3439 ]
3440 : undefined,
3441 );
3442 ReactNoop.render(result);
3443 });
3444
3445 expect(ReactNoop).toMatchRenderedOutput(<div>HI</div>);
3446 });
3447
3448 it('preserves error stacks passed through server-to-server with source maps', async () => {
3449 async function ServerComponent({transport}) {
3450 // This is a Server Component that receives other Server Components from a third party.
3451 const thirdParty = ReactServer.use(
3452 ReactNoopFlightClient.read(transport, {
3453 findSourceMapURL(url) {
3454 // By giving a source map url we're saying that we can't use the original
3455 // file as the sourceURL, which gives stack traces a about://React/ prefix.
3456 return 'source-map://' + url;
3457 },
3458 }),
3459 );
3460 // This will throw a third-party error inside the first-party server component.
3461 await thirdParty.model;
3462 return 'Should never render';
3463 }
3464
3465 async function bar() {
3466 throw new Error('third-party-error');
3467 }
3468
3469 async function foo() {
3470 await bar();
3471 }
3472
3473 const rejectedPromise = foo();
3474
3475 const thirdPartyTransport = ReactNoopFlightServer.render(
3476 {model: rejectedPromise},
3477 {
3478 environmentName: 'third-party',
3479 onError(x) {
3480 if (__DEV__) {
3481 return 'a dev digest';
3482 }
3483 return `digest("${x.message}")`;
3484 },
3485 },
3486 );
3487
3488 let originalError;
3489 try {
3490 await rejectedPromise;
3491 } catch (x) {
3492 originalError = x;
3493 }
3494 expect(originalError.message).toBe('third-party-error');
3495
3496 const transport = ReactNoopFlightServer.render(
3497 <ServerComponent transport={thirdPartyTransport} />,
3498 {
3499 onError(x) {
3500 if (__DEV__) {
3501 return 'a dev digest';
3502 }
3503 return x.digest; // passthrough
3504 },
3505 },
3506 );
3507
3508 await 0;
3509 await 0;
3510 await 0;
3511
3512 const expectedErrorStack = originalError.stack
3513 // Test only the first rows since there's a lot of noise after that is eliminated.
3514 .split('\n')
3515 .slice(0, 4)
3516 .join('\n')
3517 .replaceAll(' (/', ' (file:///'); // The eval will end up normalizing these
3518
3519 let sawReactPrefix = false;
3520 const environments = [];
3521 await act(async () => {
3522 ReactNoop.render(
3523 <ErrorBoundary
3524 expectedMessage="third-party-error"
3525 expectedEnviromentName="third-party"
3526 expectedErrorStack={expectedErrorStack}>
3527 {ReactNoopFlightClient.read(transport, {
3528 findSourceMapURL(url, environmentName) {
3529 if (url.startsWith('about://React/')) {
3530 // We don't expect to see any React prefixed URLs here.
3531 sawReactPrefix = true;
3532 }
3533 environments.push(environmentName);
3534 // My not giving a source map, we should leave it intact.
3535 return null;
3536 },
3537 })}
3538 </ErrorBoundary>,
3539 );
3540 });
3541
3542 expect(sawReactPrefix).toBe(false);
3543 if (__DEV__) {
3544 expect(environments.slice(0, 4)).toEqual([
3545 'Server',
3546 'third-party',
3547 'third-party',
3548 'third-party',
3549 ]);
3550 } else {
3551 expect(environments).toEqual([]);
3552 }
3553 });
3554
3555 // @gate !__DEV__ || enableComponentPerformanceTrack
3556 it('can change the environment name inside a component', async () => {
3557 let env = 'A';
3558 function Component(props) {
3559 env = 'B';
3560 return <div>hi</div>;
3561 }
3562
3563 const transport = ReactNoopFlightServer.render(
3564 {
3565 greeting: <Component />,
3566 },
3567 {
3568 environmentName() {
3569 return env;
3570 },
3571 },
3572 );
3573
3574 await act(async () => {
3575 const rootModel = await ReactNoopFlightClient.read(transport);
3576 const greeting = rootModel.greeting;
3577 expect(getDebugInfo(greeting)).toEqual(
3578 __DEV__
3579 ? [
3580 {time: 12},
3581 {
3582 name: 'Component',
3583 env: 'A',
3584 key: null,
3585 stack: ' in Object.<anonymous> (at **)',
3586 props: {},
3587 },
3588 {
3589 env: 'B',
3590 },
3591 {time: 13},
3592 ]
3593 : undefined,
3594 );
3595 ReactNoop.render(greeting);
3596 });
3597
3598 expect(ReactNoop).toMatchRenderedOutput(<div>hi</div>);
3599 });
3600
3601 // @gate __DEV__
3602 it('replays logs, but not onError logs', async () => {
3603 function foo() {
3604 return 'hello';
3605 }
3606
3607 class MyClass {
3608 constructor() {
3609 this.x = 1;
3610 }
3611 method() {}
3612 get y() {
3613 return this.x + 1;
3614 }
3615 get z() {
3616 return this.x + 5;
3617 }
3618 }
3619 Object.defineProperty(MyClass.prototype, 'y', {enumerable: true});
3620
3621 Object.defineProperty(MyClass, 'name', {value: 'MyClassName'});
3622
3623 function ServerComponent() {
3624 console.log('hi', {
3625 prop: 123,
3626 fn: foo,
3627 map: new Map([['foo', foo]]),
3628 promise: Promise.resolve('yo'),
3629 infinitePromise: new Promise(() => {}),
3630 Class: MyClass,
3631 instance: new MyClass(),
3632 });
3633 throw new Error('err');
3634 }
3635
3636 function App() {
3637 return ReactServer.createElement(ServerComponent);
3638 }
3639
3640 let ownerStacks = [];
3641
3642 // These tests are specifically testing console.log.
3643 // Assign to `mockConsoleLog` so we can still inspect it when `console.log`
3644 // is overridden by the test modules. The original function will be restored
3645 // after this test finishes by `jest.restoreAllMocks()`.
3646 const mockConsoleLog = spyOnDevAndProd(console, 'log').mockImplementation(
3647 () => {
3648 // Uses server React.
3649 ownerStacks.push(normalizeCodeLocInfo(ReactServer.captureOwnerStack()));
3650 },
3651 );
3652
3653 // Reset the modules so that we get a new overridden console on top of the
3654 // one installed by expect. This ensures that we still emit console.error
3655 // calls.
3656 jest.resetModules();
3657 jest.mock('react', () => require('react/react.react-server'));
3658 ReactServer = require('react');
3659 ReactNoopFlightServer = require('react-noop-renderer/flight-server');
3660 const transport = ReactNoopFlightServer.render({
3661 root: ReactServer.createElement(App),
3662 });
3663 assertConsoleErrorDev(['Error: err' + '\n in <stack>']);
3664
3665 expect(mockConsoleLog).toHaveBeenCalledTimes(1);
3666 expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');
3667 expect(mockConsoleLog.mock.calls[0][1].prop).toBe(123);
3668 expect(ownerStacks).toEqual(['\n in App (at **)']);
3669 mockConsoleLog.mockClear();
3670 mockConsoleLog.mockImplementation(() => {
3671 // Switching to client React.
3672 ownerStacks.push(normalizeCodeLocInfo(React.captureOwnerStack()));
3673 });
3674 ownerStacks = [];
3675
3676 // Let the Promises resolve.
3677 await 0;
3678 await 0;
3679 await 0;
3680
3681 // The error should not actually get logged because we're not awaiting the root
3682 // so it's not thrown but the server log also shouldn't be replayed.
3683 await ReactNoopFlightClient.read(transport, {close: true});
3684
3685 expect(mockConsoleLog).toHaveBeenCalledTimes(1);
3686 expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');
3687 expect(mockConsoleLog.mock.calls[0][1].prop).toBe(123);
3688 const loggedFn = mockConsoleLog.mock.calls[0][1].fn;
3689 expect(typeof loggedFn).toBe('function');
3690 expect(loggedFn).not.toBe(foo);
3691 expect(loggedFn.toString()).toBe(foo.toString());
3692
3693 const loggedMap = mockConsoleLog.mock.calls[0][1].map;
3694 expect(loggedMap instanceof Map).toBe(true);
3695 const loggedFn2 = loggedMap.get('foo');
3696 expect(typeof loggedFn2).toBe('function');
3697 expect(loggedFn2).not.toBe(foo);
3698 expect(loggedFn2.toString()).toBe(foo.toString());
3699 expect(loggedFn2).toBe(loggedFn);
3700
3701 const promise = mockConsoleLog.mock.calls[0][1].promise;
3702 expect(promise).toBeInstanceOf(Promise);
3703 expect(await promise).toBe('yo');
3704
3705 const infinitePromise = mockConsoleLog.mock.calls[0][1].infinitePromise;
3706 expect(infinitePromise).toBeInstanceOf(Promise);
3707 let resolved = false;
3708 infinitePromise.then(
3709 () => (resolved = true),
3710 x => {
3711 console.error(x);
3712 resolved = true;
3713 },
3714 );
3715 await 0;
3716 await 0;
3717 await 0;
3718 // This should not reject upon aborting the stream.
3719 expect(resolved).toBe(false);
3720
3721 const Class = mockConsoleLog.mock.calls[0][1].Class;
3722 const instance = mockConsoleLog.mock.calls[0][1].instance;
3723 expect(typeof Class).toBe('function');
3724 expect(Class.prototype.constructor).toBe(Class);
3725 expect(Class.name).toBe('MyClassName');
3726 expect(instance instanceof Class).toBe(true);
3727 expect(Object.getPrototypeOf(instance)).toBe(Class.prototype);
3728 expect(instance.x).toBe(1);
3729 expect(instance.hasOwnProperty('y')).toBe(true);
3730 expect(instance.y).toBe(2); // Enumerable getter was reified
3731 expect(instance.hasOwnProperty('z')).toBe(false);
3732 expect(instance.z).toBe(6); // Not enumerable getter was transferred as part of the toString() of the class
3733 expect(typeof instance.method).toBe('function'); // Methods are included only if they're part of the toString()
3734
3735 expect(ownerStacks).toEqual(['\n in App (at **)']);
3736 });
3737
3738 // @gate __DEV__
3739 it('replays logs with cyclic objects', async () => {
3740 const cyclic = {cycle: null};
3741 cyclic.cycle = cyclic;
3742
3743 function ServerComponent() {
3744 console.log('hi', {cyclic});
3745 return null;
3746 }
3747
3748 function App() {
3749 return ReactServer.createElement(ServerComponent);
3750 }
3751
3752 // These tests are specifically testing console.log.
3753 // Assign to `mockConsoleLog` so we can still inspect it when `console.log`
3754 // is overridden by the test modules. The original function will be restored
3755 // after this test finishes by `jest.restoreAllMocks()`.
3756 const mockConsoleLog = spyOnDevAndProd(console, 'log').mockImplementation(
3757 () => {},
3758 );
3759
3760 // Reset the modules so that we get a new overridden console on top of the
3761 // one installed by expect. This ensures that we still emit console.error
3762 // calls.
3763 jest.resetModules();
3764 jest.mock('react', () => require('react/react.react-server'));
3765 ReactServer = require('react');
3766 ReactNoopFlightServer = require('react-noop-renderer/flight-server');
3767 const transport = ReactNoopFlightServer.render({
3768 root: ReactServer.createElement(App),
3769 });
3770
3771 expect(mockConsoleLog).toHaveBeenCalledTimes(1);
3772 expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');
3773 expect(mockConsoleLog.mock.calls[0][1].cyclic).toBe(cyclic);
3774 mockConsoleLog.mockClear();
3775 mockConsoleLog.mockImplementation(() => {});
3776
3777 // The error should not actually get logged because we're not awaiting the root
3778 // so it's not thrown but the server log also shouldn't be replayed.
3779 await ReactNoopFlightClient.read(transport);
3780
3781 expect(mockConsoleLog).toHaveBeenCalledTimes(1);
3782 expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');
3783 const cyclic2 = mockConsoleLog.mock.calls[0][1].cyclic;
3784 expect(cyclic2).not.toBe(cyclic); // Was serialized and therefore cloned
3785 expect(cyclic2.cycle).toBe(cyclic2);
3786 });
3787
3788 // @gate __DEV__
3789 it('replays logs with large strings replaced by a placeholder', async () => {
3790 // This string exceeds the threshold for debug string length. Reconstructing
3791 // a multi-megabyte string on the client when replaying the log would block
3792 // the main thread for too long, so we omit it and send a placeholder
3793 // instead.
3794 const largeString = 'x'.repeat(1000001);
3795
3796 function ServerComponent() {
3797 console.log('large string:', largeString);
3798 return null;
3799 }
3800
3801 function App() {
3802 return ReactServer.createElement(ServerComponent);
3803 }
3804
3805 // These tests are specifically testing console.log.
3806 // Assign to `mockConsoleLog` so we can still inspect it when `console.log`
3807 // is overridden by the test modules. The original function will be restored
3808 // after this test finishes by `jest.restoreAllMocks()`.
3809 const mockConsoleLog = spyOnDevAndProd(console, 'log').mockImplementation(
3810 () => {},
3811 );
3812
3813 // Reset the modules so that we get a new overridden console on top of the
3814 // one installed by expect. This ensures that we still emit console.error
3815 // calls.
3816 jest.resetModules();
3817 jest.mock('react', () => require('react/react.react-server'));
3818 ReactServer = require('react');
3819 ReactNoopFlightServer = require('react-noop-renderer/flight-server');
3820 const transport = ReactNoopFlightServer.render({
3821 root: ReactServer.createElement(App),
3822 });
3823
3824 // The server logged the actual string synchronously while rendering.
3825 expect(mockConsoleLog).toHaveBeenCalledTimes(1);
3826 expect(mockConsoleLog.mock.calls[0][1]).toBe(largeString);
3827 mockConsoleLog.mockClear();
3828 mockConsoleLog.mockImplementation(() => {});
3829
3830 await ReactNoopFlightClient.read(transport);
3831
3832 // The replayed log received a placeholder instead of the actual string.
3833 expect(mockConsoleLog).toHaveBeenCalledTimes(1);
3834 expect(mockConsoleLog.mock.calls[0][0]).toBe('large string:');
3835 expect(mockConsoleLog.mock.calls[0][1]).toBe(
3836 'This string of length 1000001 has been omitted by React to avoid ' +
3837 'sending too much data from the server.',
3838 );
3839 });
3840
3841 // @gate !__DEV__ || enableComponentPerformanceTrack
3842 it('uses the server component debug info as the element owner in DEV', async () => {
3843 function Container({children}) {
3844 return children;
3845 }
3846
3847 function Greeting({firstName}) {
3848 // We can't use JSX here because it'll use the Client React.
3849 return ReactServer.createElement(
3850 Container,
3851 null,
3852 ReactServer.createElement('span', null, 'Hello, ', firstName),
3853 );
3854 }
3855
3856 const model = {
3857 greeting: ReactServer.createElement(Greeting, {firstName: 'Seb'}),
3858 };
3859
3860 const transport = ReactNoopFlightServer.render(model);
3861
3862 await act(async () => {
3863 const rootModel = await ReactNoopFlightClient.read(transport);
3864 const greeting = rootModel.greeting;
3865 // We've rendered down to the span.
3866 expect(greeting.type).toBe('span');
3867 if (__DEV__) {
3868 const greetInfo = {
3869 name: 'Greeting',
3870 env: 'Server',
3871 key: null,
3872 stack: ' in Object.<anonymous> (at **)',
3873 props: {
3874 firstName: 'Seb',
3875 },
3876 };
3877 expect(getDebugInfo(greeting)).toEqual([
3878 {time: 12},
3879 greetInfo,
3880 {time: 13},
3881 {
3882 name: 'Container',
3883 env: 'Server',
3884 key: null,
3885 owner: greetInfo,
3886 stack: ' in Greeting (at **)',
3887 props: {
3888 children: expect.objectContaining({
3889 type: 'span',
3890 props: {
3891 children: ['Hello, ', 'Seb'],
3892 },
3893 }),
3894 },
3895 },
3896 {time: 14},
3897 ]);
3898 // The owner that created the span was the outer server component.
3899 // We expect the debug info to be referentially equal to the owner.
3900 expect(greeting._owner).toBe(greeting._debugInfo[1]);
3901 } else {
3902 expect(greeting._debugInfo).toBe(undefined);
3903 expect(greeting._owner).toBe(undefined);
3904 }
3905 ReactNoop.render(greeting);
3906 });
3907
3908 expect(ReactNoop).toMatchRenderedOutput(<span>Hello, Seb</span>);
3909 });
3910
3911 it('restores the stack trace limit after recreating JSX call sites', async () => {
3912 function Component() {
3913 return ReactServer.createElement('div');
3914 }
3915
3916 const transport = ReactNoopFlightServer.render(
3917 ReactServer.createElement(Component),
3918 );
3919 const previousStackTraceLimit = Error.stackTraceLimit;
3920 Error.stackTraceLimit = 50;
3921 try {
3922 await ReactNoopFlightClient.read(transport);
3923
3924 expect(Error.stackTraceLimit).toBe(50);
3925 } finally {
3926 Error.stackTraceLimit = previousStackTraceLimit;
3927 }
3928 });
3929
3930 // @gate __DEV__
3931 it('can get the component owner stacks during rendering in dev', () => {
3932 let stack;
3933
3934 function Foo() {
3935 return ReactServer.createElement(Bar, null);
3936 }
3937 function Bar() {
3938 return ReactServer.createElement(
3939 'div',
3940 null,
3941 ReactServer.createElement(Baz, null),
3942 );
3943 }
3944
3945 function Baz() {
3946 stack = ReactServer.captureOwnerStack();
3947 return ReactServer.createElement('span', null, 'hi');
3948 }
3949 ReactNoopFlightServer.render(
3950 ReactServer.createElement(
3951 'div',
3952 null,
3953 ReactServer.createElement(Foo, null),
3954 ),
3955 );
3956
3957 expect(normalizeCodeLocInfo(stack)).toBe(
3958 '\n in Bar (at **)' + '\n in Foo (at **)',
3959 );
3960 });
3961
3962 // @gate __DEV__
3963 it('can track owner for a flight response created in another render', async () => {
3964 jest.resetModules();
3965 jest.mock('react', () => ReactServer);
3966 // For this to work the Flight Client needs to be the react-server version.
3967 const ReactNoopFlightClienOnTheServer = require('react-noop-renderer/flight-client');
3968 jest.resetModules();
3969 jest.mock('react', () => React);
3970
3971 let stack;
3972
3973 function Component() {
3974 stack = ReactServer.captureOwnerStack();
3975 return ReactServer.createElement('span', null, 'hi');
3976 }
3977
3978 const ClientComponent = clientReference(Component);
3979
3980 function ThirdPartyComponent() {
3981 return ReactServer.createElement(ClientComponent);
3982 }
3983
3984 // This is rendered outside the render to ensure we don't inherit anything accidental
3985 // by being in the same environment which would make it seem like it works when it doesn't.
3986 const thirdPartyTransport = ReactNoopFlightServer.render(
3987 {children: ReactServer.createElement(ThirdPartyComponent)},
3988 {
3989 environmentName: 'third-party',
3990 },
3991 );
3992
3993 async function fetchThirdParty() {
3994 return ReactNoopFlightClienOnTheServer.read(thirdPartyTransport);
3995 }
3996
3997 async function FirstPartyComponent() {
3998 // This component fetches from a third party
3999 const thirdParty = await fetchThirdParty();
4000 return thirdParty.children;
4001 }
4002 function App() {
4003 return ReactServer.createElement(FirstPartyComponent);
4004 }
4005
4006 const transport = ReactNoopFlightServer.render(
4007 ReactServer.createElement(App),
4008 );
4009
4010 await act(async () => {
4011 const root = await ReactNoopFlightClient.read(transport);
4012 ReactNoop.render(root);
4013 });
4014
4015 expect(normalizeCodeLocInfo(stack)).toBe(
4016 '\n in ThirdPartyComponent (at **)' +
4017 '\n in createResponse (at **)' + // These two internal frames should
4018 '\n in read (at **)' + // ideally not be included.
4019 '\n in fetchThirdParty (at **)' +
4020 '\n in FirstPartyComponent (at **)' +
4021 '\n in App (at **)',
4022 );
4023 });
4024
4025 // @gate __DEV__
4026 it('can get the component owner stacks for onError in dev', async () => {
4027 const thrownError = new Error('hi');
4028 let caughtError;
4029 let ownerStack;
4030
4031 function Foo() {
4032 return ReactServer.createElement(Bar, null);
4033 }
4034 function Bar() {
4035 return ReactServer.createElement(
4036 'div',
4037 null,
4038 ReactServer.createElement(Baz, null),
4039 );
4040 }
4041 function Baz() {
4042 throw thrownError;
4043 }
4044
4045 ReactNoopFlightServer.render(
4046 ReactServer.createElement(
4047 'div',
4048 null,
4049 ReactServer.createElement(Foo, null),
4050 ),
4051 {
4052 onError(error, errorInfo) {
4053 caughtError = error;
4054 ownerStack = ReactServer.captureOwnerStack
4055 ? ReactServer.captureOwnerStack()
4056 : null;
4057 },
4058 },
4059 );
4060
4061 expect(caughtError).toBe(thrownError);
4062 expect(normalizeCodeLocInfo(ownerStack)).toBe(
4063 '\n in Bar (at **)' + '\n in Foo (at **)',
4064 );
4065 });
4066
4067 it('should include only one component stack in replayed logs (if DevTools or polyfill adds them)', () => {
4068 class MyError extends Error {
4069 toJSON() {
4070 return 123;
4071 }
4072 }
4073
4074 function Foo() {
4075 return ReactServer.createElement('div', null, [
4076 'Womp womp: ',
4077 new MyError('spaghetti'),
4078 ]);
4079 }
4080
4081 function Bar() {
4082 const array = [];
4083 // Trigger key warning
4084 array.push(ReactServer.createElement(Foo));
4085 return ReactServer.createElement('div', null, array);
4086 }
4087
4088 function App() {
4089 return ReactServer.createElement(Bar);
4090 }
4091
4092 // While we're on the server we need to have the Server version active to track component stacks.
4093 jest.resetModules();
4094 jest.mock('react', () => ReactServer);
4095 const transport = ReactNoopFlightServer.render(
4096 ReactServer.createElement(App),
4097 );
4098
4099 assertConsoleErrorDev([
4100 'Each child in a list should have a unique "key" prop.' +
4101 ' See https://react.dev/link/warning-keys for more information.\n' +
4102 ' in Bar (at **)\n' +
4103 ' in App (at **)',
4104 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' +
4105 ' <div>Womp womp: {Error}</div>\n' +
4106 ' ^^^^^^^\n' +
4107 ' in Foo (at **)\n' +
4108 ' in Bar (at **)\n' +
4109 ' in App (at **)',
4110 ]);
4111
4112 // Replay logs on the client
4113 jest.resetModules();
4114 jest.mock('react', () => React);
4115 ReactNoopFlightClient.read(transport);
4116 assertConsoleErrorDev([
4117 'Each child in a list should have a unique "key" prop.' +
4118 ' See https://react.dev/link/warning-keys for more information.\n' +
4119 ' in Bar (at **)\n' +
4120 ' in App (at **)',
4121 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' +
4122 ' <div>Womp womp: {Error}</div>\n' +
4123 ' ^^^^^^^\n' +
4124 ' in Foo (at **)\n' +
4125 ' in Bar (at **)\n' +
4126 ' in App (at **)',
4127 ]);
4128 });
4129
4130 it('can filter out stack frames of a serialized error in dev', async () => {
4131 async function bar() {
4132 throw new Error('my-error');
4133 }
4134
4135 async function intermediate() {
4136 await bar();
4137 }
4138
4139 async function foo() {
4140 await intermediate();
4141 }
4142
4143 const rejectedPromise = foo();
4144 const transport = ReactNoopFlightServer.render(
4145 {model: rejectedPromise},
4146 {
4147 onError(x) {
4148 return `digest("${x.message}")`;
4149 },
4150 filterStackFrame(url, functionName, lineNumber, columnNumber) {
4151 return functionName !== 'intermediate';
4152 },
4153 },
4154 );
4155
4156 let originalError;
4157 try {
4158 await rejectedPromise;
4159 } catch (x) {
4160 originalError = x;
4161 }
4162
4163 const root = await ReactNoopFlightClient.read(transport);
4164 let caughtError;
4165 try {
4166 await root.model;
4167 } catch (x) {
4168 caughtError = x;
4169 }
4170 if (__DEV__) {
4171 expect(caughtError.message).toBe(originalError.message);
4172 expect(normalizeCodeLocInfo(caughtError.stack)).toContain(
4173 '\n in bar (at **)' + '\n in foo (at **)',
4174 );
4175 }
4176 expect(normalizeCodeLocInfo(originalError.stack)).toContain(
4177 '\n in bar (at **)' +
4178 '\n in intermediate (at **)' +
4179 '\n in foo (at **)',
4180 );
4181 expect(caughtError.digest).toBe('digest("my-error")');
4182 });
4183
4184 it('can transport function names in stackframes in dev even without eval', async () => {
4185 function a() {
4186 return b();
4187 }
4188 function b() {
4189 return c();
4190 }
4191 function c() {
4192 return new Error('boom');
4193 }
4194
4195 // eslint-disable-next-line no-eval
4196 const previousEval = globalThis.eval.bind(globalThis);
4197 // eslint-disable-next-line no-eval
4198 globalThis.eval = () => {
4199 throw new Error('eval is disabled');
4200 };
4201
4202 try {
4203 const transport = ReactNoopFlightServer.render(
4204 {model: a()},
4205 {onError: () => 'digest'},
4206 );
4207
4208 const root = await ReactNoopFlightClient.read(transport);
4209 const receivedError = await root.model;
4210
4211 if (__DEV__) {
4212 const normalizedErrorStack = normalizeCodeLocInfo(
4213 receivedError.stack.split('\n').slice(0, 4).join('\n'),
4214 );
4215
4216 expect(normalizedErrorStack).toEqual(
4217 'Error: boom' +
4218 '\n in c (at **)' +
4219 '\n in b (at **)' +
4220 '\n in a (at **)',
4221 );
4222 assertConsoleErrorDev([
4223 'eval() is not supported in this environment. ' +
4224 'React requires eval() in development mode for various debugging features ' +
4225 'like reconstructing callstacks from a different environment.\n' +
4226 'React will never use eval() in production mode',
4227 ]);
4228 } else {
4229 expect(receivedError.message).toEqual(
4230 'An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.',
4231 );
4232 expect(receivedError).not.toHaveProperty('digest');
4233 }
4234 } finally {
4235 // eslint-disable-next-line no-eval
4236 globalThis.eval = previousEval;
4237 }
4238 });
4239
4240 // @gate __DEV__ && enableComponentPerformanceTrack
4241 it('can render deep but cut off JSX in debug info', async () => {
4242 function createDeepJSX(n) {
4243 if (n <= 0) {
4244 return null;
4245 }
4246 return <div>{createDeepJSX(n - 1)}</div>;
4247 }
4248
4249 function ServerComponent(props) {
4250 return <div>not using props</div>;
4251 }
4252
4253 const transport = ReactNoopFlightServer.render({
4254 root: (
4255 <ServerComponent>
4256 {createDeepJSX(100) /* deper than objectLimit */}
4257 </ServerComponent>
4258 ),
4259 });
4260
4261 await act(async () => {
4262 const rootModel = await ReactNoopFlightClient.read(transport);
4263 const root = rootModel.root;
4264 const children = root._debugInfo[1].props.children;
4265 expect(children.type).toBe('div');
4266 expect(children.props.children.type).toBe('div');
4267 ReactNoop.render(root);
4268 });
4269
4270 expect(ReactNoop).toMatchRenderedOutput(<div>not using props</div>);
4271 });
4272
4273 // @gate __DEV__ && enableComponentPerformanceTrack
4274 it('can render deep but cut off Map/Set in debug info', async () => {
4275 function createDeepMap(n) {
4276 if (n <= 0) {
4277 return null;
4278 }
4279 const map = new Map();
4280 map.set('key', createDeepMap(n - 1));
4281 return map;
4282 }
4283
4284 function createDeepSet(n) {
4285 if (n <= 0) {
4286 return null;
4287 }
4288 const set = new Set();
4289 set.add(createDeepSet(n - 1));
4290 return set;
4291 }
4292
4293 function ServerComponent(props) {
4294 return <div>not using props</div>;
4295 }
4296
4297 const transport = ReactNoopFlightServer.render({
4298 set: (
4299 <ServerComponent
4300 set={createDeepSet(100) /* deper than objectLimit */}
4301 />
4302 ),
4303 map: (
4304 <ServerComponent
4305 map={createDeepMap(100) /* deper than objectLimit */}
4306 />
4307 ),
4308 });
4309
4310 await act(async () => {
4311 const rootModel = await ReactNoopFlightClient.read(transport);
4312 const set = rootModel.set._debugInfo[1].props.set;
4313 const map = rootModel.map._debugInfo[1].props.map;
4314 expect(set instanceof Set).toBe(true);
4315 expect(set.size).toBe(1);
4316 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
4317 for (const entry of set) {
4318 expect(entry instanceof Set).toBe(true);
4319 break;
4320 }
4321
4322 expect(map instanceof Map).toBe(true);
4323 expect(map.size).toBe(1);
4324 expect(map.get('key') instanceof Map).toBe(true);
4325
4326 ReactNoop.render(rootModel.set);
4327 });
4328
4329 expect(ReactNoop).toMatchRenderedOutput(<div>not using props</div>);
4330 });
4331
4332 // @gate !__DEV__ || enableComponentPerformanceTrack
4333 it('produces correct parent stacks', async () => {
4334 function Container() {
4335 return ReactServer.createElement('div', null);
4336 }
4337 function ContainerParent() {
4338 return ReactServer.createElement(Container, null);
4339 }
4340 function App() {
4341 return ReactServer.createElement(
4342 'main',
4343 null,
4344 ReactServer.createElement(ContainerParent, null),
4345 );
4346 }
4347
4348 const transport = ReactNoopFlightServer.render({
4349 root: ReactServer.createElement(App, null),
4350 });
4351
4352 await act(async () => {
4353 const {root} = await ReactNoopFlightClient.read(transport);
4354
4355 ReactNoop.render(root);
4356
4357 expect(root.type).toBe('main');
4358 if (__DEV__) {
4359 const div = root.props.children;
4360 expect(getDebugInfo(div)).toEqual([
4361 {
4362 time: 14,
4363 },
4364 {
4365 env: 'Server',
4366 key: null,
4367 name: 'ContainerParent',
4368 owner: {
4369 env: 'Server',
4370 key: null,
4371 name: 'App',
4372 props: {},
4373 stack: ' in Object.<anonymous> (at **)',
4374 },
4375 props: {},
4376 stack: ' in App (at **)',
4377 },
4378 {
4379 time: 15,
4380 },
4381 {
4382 env: 'Server',
4383 key: null,
4384 name: 'Container',
4385 owner: {
4386 env: 'Server',
4387 key: null,
4388 name: 'ContainerParent',
4389 owner: {
4390 env: 'Server',
4391 key: null,
4392 name: 'App',
4393 props: {},
4394 stack: ' in Object.<anonymous> (at **)',
4395 },
4396 props: {},
4397 stack: ' in App (at **)',
4398 },
4399 props: {},
4400 stack: ' in ContainerParent (at **)',
4401 },
4402 {
4403 time: 16,
4404 },
4405 ]);
4406 expect(getDebugInfo(root)).toEqual([
4407 {
4408 time: 12,
4409 },
4410 {
4411 env: 'Server',
4412 key: null,
4413 name: 'App',
4414 props: {},
4415 stack: ' in Object.<anonymous> (at **)',
4416 },
4417 {
4418 time: 13,
4419 },
4420 ]);
4421 } else {
4422 expect(root._debugInfo).toBe(undefined);
4423 expect(root._owner).toBe(undefined);
4424 }
4425 });
4426
4427 expect(ReactNoop).toMatchRenderedOutput(
4428 <main>
4429 <div />
4430 </main>,
4431 );
4432 });
4433
4434 // @gate enableOptimisticKey
4435 it('collapses optimistic keys to an optimistic key', async () => {
4436 function Bar({text}) {
4437 return <div />;
4438 }
4439 function Foo() {
4440 return <Bar key={ReactServer.optimisticKey} />;
4441 }
4442 const transport = ReactNoopFlightServer.render({
4443 element: <Foo key="Outer Key" />,
4444 });
4445 const model = await ReactNoopFlightClient.read(transport);
4446 expect(model.element.key).toBe(React.optimisticKey);
4447 });
4448
4449 it('can use a JSX element exported as a client reference in multiple server components', async () => {
4450 const ClientReference = clientReference(React.createElement('span'));
4451
4452 function Foo() {
4453 return ClientReference;
4454 }
4455
4456 function Bar() {
4457 return ClientReference;
4458 }
4459
4460 function App() {
4461 return ReactServer.createElement(
4462 'div',
4463 null,
4464 ReactServer.createElement(Foo),
4465 ReactServer.createElement(Bar),
4466 );
4467 }
4468
4469 const transport = ReactNoopFlightServer.render(
4470 ReactServer.createElement(App),
4471 );
4472
4473 await act(async () => {
4474 const result = await ReactNoopFlightClient.read(transport);
4475 ReactNoop.render(result);
4476
4477 if (__DEV__) {
4478 // TODO: Debug info is dropped for frozen elements (client-created JSX
4479 // exported as a client reference in this case). Ideally we'd clone the
4480 // element so that each context gets its own mutable copy with correct
4481 // debug info. When fixed, foo should have Foo's debug info and bar should
4482 // have Bar's debug info.
4483 const [foo, bar] = result.props.children;
4484 expect(getDebugInfo(foo)).toBe(null);
4485 expect(getDebugInfo(bar)).toBe(null);
4486 }
4487 });
4488
4489 // TODO: With cloning, each context would get its own element copy, so this
4490 // key warning should go away.
4491 assertConsoleErrorDev([
4492 'Each child in a list should have a unique "key" prop.\n\n' +
4493 'Check the top-level render call using <div>. ' +
4494 'See https://react.dev/link/warning-keys for more information.\n' +
4495 ' in span (at **)',
4496 ]);
4497
4498 expect(ReactNoop).toMatchRenderedOutput(
4499 <div>
4500 <span />
4501 <span />
4502 </div>,
4503 );
4504 });
4505 });