From de82912e620518d501680bbd93fbb5cc8d134223 Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Fri, 20 Dec 2024 09:48:50 -0500 Subject: [PATCH 1/4] Turn off enableYieldingBeforePassive in internal test renderers (#31863) https://github.com/facebook/react/pull/31785 turned on `enableYieldingBeforePassive` for the internal test renderer builds. We have some failing tests on the RN side blocking the sync so lets turn these off for now. --- .../shared/forks/ReactFeatureFlags.test-renderer.native-fb.js | 2 +- packages/shared/forks/ReactFeatureFlags.test-renderer.www.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index 8d38112b16879..81060cfafb1b1 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -68,7 +68,7 @@ export const enableFabricCompleteRootInCommitPhase = false; export const enableSiblingPrerendering = true; export const enableUseResourceEffectHook = true; export const enableHydrationLaneScheduling = true; -export const enableYieldingBeforePassive = true; +export const enableYieldingBeforePassive = false; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 28a303a034cac..e0e9906d52b8f 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -82,7 +82,7 @@ export const enableUseResourceEffectHook = false; export const enableHydrationLaneScheduling = true; -export const enableYieldingBeforePassive = true; +export const enableYieldingBeforePassive = false; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); From 6a3d6a4382cdafc1260483a6fc5f76593fc038e4 Mon Sep 17 00:00:00 2001 From: Joseph Savona <6425824+josephsavona@users.noreply.github.com> Date: Fri, 20 Dec 2024 08:56:48 -0800 Subject: [PATCH 2/4] [compiler] Allow type cast expressions with refs (#31871) We report a false positive for the combination of a ref-accessing function placed inside an array which is they type-cast. Here we teach ref validation about type casts. I also tried other variants like `return ref as const` but those already worked. Closes #31864 --- .../Validation/ValidateNoRefAccesInRender.ts | 8 +++ .../allow-ref-type-cast-in-render.expect.md | 60 +++++++++++++++++++ .../compiler/allow-ref-type-cast-in-render.js | 17 ++++++ 3 files changed, 85 insertions(+) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-type-cast-in-render.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-type-cast-in-render.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts index b361b2016a1dd..4db8c700f387f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts @@ -305,6 +305,14 @@ function validateNoRefAccessInRenderImpl( ); break; } + case 'TypeCastExpression': { + env.set( + instr.lvalue.identifier.id, + env.get(instr.value.value.identifier.id) ?? + refTypeOfType(instr.lvalue), + ); + break; + } case 'LoadContext': case 'LoadLocal': { env.set( diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-type-cast-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-type-cast-in-render.expect.md new file mode 100644 index 0000000000000..56e3039f63cec --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-type-cast-in-render.expect.md @@ -0,0 +1,60 @@ + +## Input + +```javascript +import {useRef} from 'react'; + +function useArrayOfRef() { + const ref = useRef(null); + const callback = value => { + ref.current = value; + }; + return [callback] as const; +} + +export const FIXTURE_ENTRYPOINT = { + fn: () => { + useArrayOfRef(); + return 'ok'; + }, + params: [{}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useRef } from "react"; + +function useArrayOfRef() { + const $ = _c(1); + const ref = useRef(null); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + const callback = (value) => { + ref.current = value; + }; + + t0 = [callback]; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0 as const; +} + +export const FIXTURE_ENTRYPOINT = { + fn: () => { + useArrayOfRef(); + return "ok"; + }, + + params: [{}], +}; + +``` + +### Eval output +(kind: ok) "ok" \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-type-cast-in-render.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-type-cast-in-render.js new file mode 100644 index 0000000000000..2d0aafeffda4c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-type-cast-in-render.js @@ -0,0 +1,17 @@ +import {useRef} from 'react'; + +function useArrayOfRef() { + const ref = useRef(null); + const callback = value => { + ref.current = value; + }; + return [callback] as const; +} + +export const FIXTURE_ENTRYPOINT = { + fn: () => { + useArrayOfRef(); + return 'ok'; + }, + params: [{}], +}; From 26297f5383f7e7150d9aa2cf12e8326c96991cab Mon Sep 17 00:00:00 2001 From: Ricky Date: Fri, 20 Dec 2024 12:41:13 -0500 Subject: [PATCH 3/4] [assert helpers] not dom or reconciler (#31862) converts everything left outside react-dom and react-reconciler --- .../__tests__/ReactCacheOld-test.internal.js | 35 +++++--- .../ReactHooksInspectionIntegration-test.js | 13 +-- .../__tests__/trustedTypes-test.internal.js | 14 +-- .../__tests__/ReactFabric-test.internal.js | 86 ++++++++++--------- .../ReactNativeEvents-test.internal.js | 50 ++++++----- .../ReactNativeMount-test.internal.js | 33 +++---- .../__tests__/ReactFlightDOMBrowser-test.js | 52 +++++++---- .../src/__tests__/ReactFlightDOMEdge-test.js | 21 +++-- .../src/__tests__/ReactFlightDOMForm-test.js | 13 ++- .../src/__tests__/ReactTestRenderer-test.js | 23 +++-- 10 files changed, 195 insertions(+), 145 deletions(-) diff --git a/packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js b/packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js index 0e9cb549f653a..554e6e4bfb29a 100644 --- a/packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js +++ b/packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js @@ -22,6 +22,7 @@ let waitForPaint; let assertLog; let waitForThrow; let act; +let assertConsoleErrorDev; describe('ReactCache', () => { beforeEach(() => { @@ -39,6 +40,7 @@ describe('ReactCache', () => { assertLog = InternalTestUtils.assertLog; waitForThrow = InternalTestUtils.waitForThrow; waitForPaint = InternalTestUtils.waitForPaint; + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; act = InternalTestUtils.act; TextResource = createResource( @@ -190,20 +192,31 @@ describe('ReactCache', () => { ); if (__DEV__) { - await expect(async () => { - await waitForAll([ - 'App', - 'Loading...', - - ...(gate('enableSiblingPrerendering') ? ['App'] : []), - ]); - }).toErrorDev([ + await waitForAll([ + 'App', + 'Loading...', + + ...(gate('enableSiblingPrerendering') ? ['App'] : []), + ]); + assertConsoleErrorDev([ 'Invalid key type. Expected a string, number, symbol, or ' + "boolean, but instead received: [ 'Hi', 100 ]\n\n" + 'To use non-primitive values as keys, you must pass a hash ' + - 'function as the second argument to createResource().', - - ...(gate('enableSiblingPrerendering') ? ['Invalid key type'] : []), + 'function as the second argument to createResource().\n' + + ' in App (at **)' + + (gate(flags => flags.enableOwnerStacks) + ? '' + : '\n in Suspense (at **)'), + + ...(gate('enableSiblingPrerendering') + ? [ + 'Invalid key type. Expected a string, number, symbol, or ' + + "boolean, but instead received: [ 'Hi', 100 ]\n\n" + + 'To use non-primitive values as keys, you must pass a hash ' + + 'function as the second argument to createResource().\n' + + ' in App (at **)', + ] + : []), ]); } else { await waitForAll([ diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index ff8e7e1ac8683..87f98b99f2534 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -14,6 +14,7 @@ let React; let ReactTestRenderer; let ReactDebugTools; let act; +let assertConsoleErrorDev; let useMemoCache; function normalizeSourceLoc(tree) { @@ -33,7 +34,7 @@ describe('ReactHooksInspectionIntegration', () => { jest.resetModules(); React = require('react'); ReactTestRenderer = require('react-test-renderer'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); ReactDebugTools = require('react-debug-tools'); useMemoCache = require('react/compiler-runtime').c; }); @@ -2344,10 +2345,12 @@ describe('ReactHooksInspectionIntegration', () => { , ); - await expect(async () => { - await act(async () => await LazyFoo); - }).toErrorDev([ - 'Foo: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.', + await act(async () => await LazyFoo); + assertConsoleErrorDev([ + 'Foo: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.' + + (gate(flags => flags.enableOwnerStacks) + ? '' + : '\n in Foo (at **)\n' + ' in Suspense (at **)'), ]); const childFiber = renderer.root._currentFiber(); diff --git a/packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js b/packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js index 923ee1f5d81a6..5a43a9ec2f06c 100644 --- a/packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js +++ b/packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js @@ -14,6 +14,7 @@ describe('when Trusted Types are available in global object', () => { let ReactDOMClient; let ReactFeatureFlags; let act; + let assertConsoleErrorDev; let container; let ttObject1; let ttObject2; @@ -36,7 +37,7 @@ describe('when Trusted Types are available in global object', () => { ReactFeatureFlags.enableTrustedTypesIntegration = true; React = require('react'); ReactDOMClient = require('react-dom/client'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); ttObject1 = { toString() { return 'Hi'; @@ -208,17 +209,16 @@ describe('when Trusted Types are available in global object', () => { it('should warn once when rendering script tag in jsx on client', async () => { const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Encountered a script tag while rendering React component. ' + 'Scripts inside React components are never executed when rendering ' + 'on the client. Consider using template tag instead ' + '(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).\n' + ' in script (at **)', - ); + ]); // check that the warning is printed only once await act(() => { diff --git a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js index 03f0cd0a6c0f3..05116be30110f 100644 --- a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js @@ -16,6 +16,7 @@ let ReactNativePrivateInterface; let createReactNativeComponentClass; let StrictMode; let act; +let assertConsoleErrorDev; const DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT = "dispatchCommand was called with a ref that isn't a " + @@ -38,7 +39,7 @@ describe('ReactFabric', () => { createReactNativeComponentClass = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface') .ReactNativeViewConfigRegistry.register; - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); }); it('should be able to create and render a native component', async () => { @@ -459,9 +460,8 @@ describe('ReactFabric', () => { }); expect(nativeFabricUIManager.dispatchCommand).not.toBeCalled(); - expect(() => { - ReactFabric.dispatchCommand(viewRef, 'updateCommand', [10, 20]); - }).toErrorDev([DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT], { + ReactFabric.dispatchCommand(viewRef, 'updateCommand', [10, 20]); + assertConsoleErrorDev([DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT], { withoutStack: true, }); @@ -525,9 +525,8 @@ describe('ReactFabric', () => { }); expect(nativeFabricUIManager.sendAccessibilityEvent).not.toBeCalled(); - expect(() => { - ReactFabric.sendAccessibilityEvent(viewRef, 'eventTypeName'); - }).toErrorDev([SEND_ACCESSIBILITY_EVENT_REQUIRES_HOST_COMPONENT], { + ReactFabric.sendAccessibilityEvent(viewRef, 'eventTypeName'); + assertConsoleErrorDev([SEND_ACCESSIBILITY_EVENT_REQUIRES_HOST_COMPONENT], { withoutStack: true, }); @@ -856,24 +855,31 @@ describe('ReactFabric', () => { uiViewClassName: 'RCTView', })); - await expect(async () => { - await act(() => { - ReactFabric.render(this should warn, 11, null, true); - }); - }).toErrorDev(['Text strings must be rendered within a component.']); + await act(() => { + ReactFabric.render(this should warn, 11, null, true); + }); + assertConsoleErrorDev([ + 'Text strings must be rendered within a component.\n' + + ' in RCTView (at **)', + ]); - await expect(async () => { - await act(() => { - ReactFabric.render( - - hi hello hi - , - 11, - null, - true, - ); - }); - }).toErrorDev(['Text strings must be rendered within a component.']); + await act(() => { + ReactFabric.render( + + hi hello hi + , + 11, + null, + true, + ); + }); + assertConsoleErrorDev([ + 'Text strings must be rendered within a component.\n' + + ' in RCTScrollView (at **)' + + (gate(flags => !flags.enableOwnerStacks) + ? '\n in RCTText (at **)' + : ''), + ]); }); it('should not throw for text inside of an indirect ancestor', async () => { @@ -1166,10 +1172,8 @@ describe('ReactFabric', () => { ); }); - let match; - expect( - () => (match = ReactFabric.findHostInstance_DEPRECATED(parent)), - ).toErrorDev([ + const match = ReactFabric.findHostInstance_DEPRECATED(parent); + assertConsoleErrorDev([ 'findHostInstance_DEPRECATED is deprecated in StrictMode. ' + 'findHostInstance_DEPRECATED was passed an instance of ContainsStrictModeChild which renders StrictMode children. ' + 'Instead, add a ref directly to the element you want to reference. ' + @@ -1207,10 +1211,8 @@ describe('ReactFabric', () => { ); }); - let match; - expect( - () => (match = ReactFabric.findHostInstance_DEPRECATED(parent)), - ).toErrorDev([ + const match = ReactFabric.findHostInstance_DEPRECATED(parent); + assertConsoleErrorDev([ 'findHostInstance_DEPRECATED is deprecated in StrictMode. ' + 'findHostInstance_DEPRECATED was passed an instance of IsInStrictMode which is inside StrictMode. ' + 'Instead, add a ref directly to the element you want to reference. ' + @@ -1250,8 +1252,8 @@ describe('ReactFabric', () => { ); }); - let match; - expect(() => (match = ReactFabric.findNodeHandle(parent))).toErrorDev([ + const match = ReactFabric.findNodeHandle(parent); + assertConsoleErrorDev([ 'findNodeHandle is deprecated in StrictMode. ' + 'findNodeHandle was passed an instance of ContainsStrictModeChild which renders StrictMode children. ' + 'Instead, add a ref directly to the element you want to reference. ' + @@ -1291,8 +1293,8 @@ describe('ReactFabric', () => { ); }); - let match; - expect(() => (match = ReactFabric.findNodeHandle(parent))).toErrorDev([ + const match = ReactFabric.findNodeHandle(parent); + assertConsoleErrorDev([ 'findNodeHandle is deprecated in StrictMode. ' + 'findNodeHandle was passed an instance of IsInStrictMode which is inside StrictMode. ' + 'Instead, add a ref directly to the element you want to reference. ' + @@ -1313,16 +1315,16 @@ describe('ReactFabric', () => { return null; } } - await expect(async () => { - await act(() => { - ReactFabric.render(, 11, null, true); - }); - }).toErrorDev([ + await act(() => { + ReactFabric.render(, 11, null, true); + }); + assertConsoleErrorDev([ 'TestComponent is accessing findNodeHandle inside its render(). ' + 'render() should be a pure function of props and state. It should ' + 'never access something that requires stale data from the previous ' + 'render, such as refs. Move this logic to componentDidMount and ' + - 'componentDidUpdate instead.', + 'componentDidUpdate instead.\n' + + ' in TestComponent (at **)', ]); }); diff --git a/packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js index 46b2ad9cf1fc7..f4ab24d71d429 100644 --- a/packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js @@ -18,6 +18,7 @@ let ReactNative; let ResponderEventPlugin; let UIManager; let createReactNativeComponentClass; +let assertConsoleErrorDev; // Parallels requireNativeComponent() in that it lazily constructs a view config, // And registers view manager event types with ReactNativeViewConfigRegistry. @@ -69,6 +70,7 @@ beforeEach(() => { require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface').RCTEventEmitter; React = require('react'); act = require('internal-test-utils').act; + assertConsoleErrorDev = require('internal-test-utils').assertConsoleErrorDev; ReactNative = require('react-native-renderer'); ResponderEventPlugin = require('react-native-renderer/src/legacy-events/ResponderEventPlugin').default; @@ -227,30 +229,32 @@ test('handles events on text nodes', () => { } const log = []; - expect(() => { - ReactNative.render( - - - log.push('string touchend')} - onTouchEndCapture={() => log.push('string touchend capture')} - onTouchStart={() => log.push('string touchstart')} - onTouchStartCapture={() => log.push('string touchstart capture')}> - Text Content - - log.push('number touchend')} - onTouchEndCapture={() => log.push('number touchend capture')} - onTouchStart={() => log.push('number touchstart')} - onTouchStartCapture={() => log.push('number touchstart capture')}> - {123} - + ReactNative.render( + + + log.push('string touchend')} + onTouchEndCapture={() => log.push('string touchend capture')} + onTouchStart={() => log.push('string touchstart')} + onTouchStartCapture={() => log.push('string touchstart capture')}> + Text Content - , - 1, - ); - }).toErrorDev([ - 'ContextHack uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + log.push('number touchend')} + onTouchEndCapture={() => log.push('number touchend capture')} + onTouchStart={() => log.push('number touchstart')} + onTouchStartCapture={() => log.push('number touchstart capture')}> + {123} + + + , + 1, + ); + assertConsoleErrorDev([ + 'ContextHack uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. ' + + '(https://react.dev/link/legacy-context)' + + '\n in ContextHack (at **)', ]); expect(UIManager.createView).toHaveBeenCalledTimes(5); diff --git a/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js index 790078b224b83..0aa1c3f0ba0af 100644 --- a/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js @@ -18,6 +18,7 @@ let UIManager; let TextInputState; let ReactNativePrivateInterface; let act; +let assertConsoleErrorDev; const DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT = "dispatchCommand was called with a ref that isn't a " + @@ -32,7 +33,7 @@ describe('ReactNative', () => { jest.resetModules(); React = require('react'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); StrictMode = React.StrictMode; ReactNative = require('react-native-renderer'); ReactNativePrivateInterface = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'); @@ -158,9 +159,8 @@ describe('ReactNative', () => { ); expect(UIManager.dispatchViewManagerCommand).not.toBeCalled(); - expect(() => { - ReactNative.dispatchCommand(viewRef, 'updateCommand', [10, 20]); - }).toErrorDev([DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT], { + ReactNative.dispatchCommand(viewRef, 'updateCommand', [10, 20]); + assertConsoleErrorDev([DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT], { withoutStack: true, }); @@ -219,9 +219,8 @@ describe('ReactNative', () => { ); expect(UIManager.sendAccessibilityEvent).not.toBeCalled(); - expect(() => { - ReactNative.sendAccessibilityEvent(viewRef, 'updateCommand', [10, 20]); - }).toErrorDev([SEND_ACCESSIBILITY_EVENT_REQUIRES_HOST_COMPONENT], { + ReactNative.sendAccessibilityEvent(viewRef, 'updateCommand', [10, 20]); + assertConsoleErrorDev([SEND_ACCESSIBILITY_EVENT_REQUIRES_HOST_COMPONENT], { withoutStack: true, }); @@ -614,10 +613,8 @@ describe('ReactNative', () => { ReactNative.render( (parent = n)} />, 11); - let match; - expect( - () => (match = ReactNative.findHostInstance_DEPRECATED(parent)), - ).toErrorDev([ + const match = ReactNative.findHostInstance_DEPRECATED(parent); + assertConsoleErrorDev([ 'findHostInstance_DEPRECATED is deprecated in StrictMode. ' + 'findHostInstance_DEPRECATED was passed an instance of ContainsStrictModeChild which renders StrictMode children. ' + 'Instead, add a ref directly to the element you want to reference. ' + @@ -652,10 +649,8 @@ describe('ReactNative', () => { 11, ); - let match; - expect( - () => (match = ReactNative.findHostInstance_DEPRECATED(parent)), - ).toErrorDev([ + const match = ReactNative.findHostInstance_DEPRECATED(parent); + assertConsoleErrorDev([ 'findHostInstance_DEPRECATED is deprecated in StrictMode. ' + 'findHostInstance_DEPRECATED was passed an instance of IsInStrictMode which is inside StrictMode. ' + 'Instead, add a ref directly to the element you want to reference. ' + @@ -689,8 +684,8 @@ describe('ReactNative', () => { ReactNative.render( (parent = n)} />, 11); - let match; - expect(() => (match = ReactNative.findNodeHandle(parent))).toErrorDev([ + const match = ReactNative.findNodeHandle(parent); + assertConsoleErrorDev([ 'findNodeHandle is deprecated in StrictMode. ' + 'findNodeHandle was passed an instance of ContainsStrictModeChild which renders StrictMode children. ' + 'Instead, add a ref directly to the element you want to reference. ' + @@ -725,8 +720,8 @@ describe('ReactNative', () => { 11, ); - let match; - expect(() => (match = ReactNative.findNodeHandle(parent))).toErrorDev([ + const match = ReactNative.findNodeHandle(parent); + assertConsoleErrorDev([ 'findNodeHandle is deprecated in StrictMode. ' + 'findNodeHandle was passed an instance of IsInStrictMode which is inside StrictMode. ' + 'Instead, add a ref directly to the element you want to reference. ' + diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js index 3eccca1a9ba70..040bb046b5518 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -38,6 +38,7 @@ let ReactServerDOM; let Scheduler; let ReactServerScheduler; let reactServerAct; +let assertConsoleErrorDev; describe('ReactFlightDOMBrowser', () => { beforeEach(() => { @@ -75,7 +76,7 @@ describe('ReactFlightDOMBrowser', () => { Scheduler = require('scheduler'); patchMessageChannel(Scheduler); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); @@ -1156,25 +1157,38 @@ describe('ReactFlightDOMBrowser', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - const stream = await serverAct(() => - ReactServerDOMServer.renderToReadableStream( - <> - {Array(6).fill(
no key
)}
- - {Array(6).fill(
no key
)} -
- , - webpackMap, - ), - ); - const result = - await ReactServerDOMClient.createFromReadableStream(stream); + const stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream( + <> + {Array(6).fill(
no key
)}
+ + {Array(6).fill(
no key
)} +
+ , + webpackMap, + ), + ); + const result = await ReactServerDOMClient.createFromReadableStream(stream); - await act(() => { - root.render(result); - }); - }).toErrorDev('Each child in a list should have a unique "key" prop.'); + if (!gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ]); + } + + await act(() => { + root.render(result); + }); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using . ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ]); + } }); it('basic use(promise)', async () => { diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js index f2814f250a2df..603dbbf09e5ca 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js @@ -37,6 +37,7 @@ let ReactServerDOMStaticServer; let ReactServerDOMClient; let use; let reactServerAct; +let assertConsoleErrorDev; function normalizeCodeLocInfo(str) { return ( @@ -66,6 +67,8 @@ describe('ReactFlightDOMEdge', () => { jest.resetModules(); reactServerAct = require('internal-test-utils').serverAct; + assertConsoleErrorDev = + require('internal-test-utils').assertConsoleErrorDev; // Simulate the condition resolution jest.mock('react', () => require('react/react.react-server')); @@ -802,17 +805,19 @@ describe('ReactFlightDOMEdge', () => { ), }; - expect(() => { - ServerModule.greet.bind({}, 'hi'); - }).toErrorDev( - 'Cannot bind "this" of a Server Action. Pass null or undefined as the first argument to .bind().', + ServerModule.greet.bind({}, 'hi'); + assertConsoleErrorDev( + [ + 'Cannot bind "this" of a Server Action. Pass null or undefined as the first argument to .bind().', + ], {withoutStack: true}, ); - expect(() => { - ServerModuleImportedOnClient.greet.bind({}, 'hi'); - }).toErrorDev( - 'Cannot bind "this" of a Server Action. Pass null or undefined as the first argument to .bind().', + ServerModuleImportedOnClient.greet.bind({}, 'hi'); + assertConsoleErrorDev( + [ + 'Cannot bind "this" of a Server Action. Pass null or undefined as the first argument to .bind().', + ], {withoutStack: true}, ); }); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js index 0b4549d5bac47..bb7c2c955bcb4 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js @@ -50,6 +50,7 @@ let ReactServerDOMClient; let ReactDOMClient; let useActionState; let act; +let assertConsoleErrorDev; describe('ReactFlightDOMForm', () => { beforeEach(() => { @@ -72,6 +73,8 @@ describe('ReactFlightDOMForm', () => { ReactDOMServer = require('react-dom/server.edge'); ReactDOMClient = require('react-dom/client'); act = React.act; + assertConsoleErrorDev = + require('internal-test-utils').assertConsoleErrorDev; // TODO: Test the old api but it warns so needs warnings to be asserted. // if (__VARIANT__) { @@ -959,12 +962,13 @@ describe('ReactFlightDOMForm', () => { await readIntoContainer(postbackSsrStream); } - await expect(submitTheForm).toErrorDev( + await submitTheForm(); + assertConsoleErrorDev([ 'Failed to serialize an action for progressive enhancement:\n' + 'Error: React Element cannot be passed to Server Functions from the Client without a temporary reference set. Pass a TemporaryReferenceSet to the options.\n' + ' [
]\n' + ' ^^^^^^', - ); + ]); // The error message was returned as JSX. const form2 = container.getElementsByTagName('form')[0]; @@ -1035,10 +1039,11 @@ describe('ReactFlightDOMForm', () => { await readIntoContainer(postbackSsrStream); } - await expect(submitTheForm).toErrorDev( + await submitTheForm(); + assertConsoleErrorDev([ 'Failed to serialize an action for progressive enhancement:\n' + 'Error: File/Blob fields are not yet supported in progressive forms. Will fallback to client hydration.', - ); + ]); expect(blob instanceof Blob).toBe(true); expect(blob.size).toBe(2); diff --git a/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js b/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js index e3400b173a21d..0b08cde378c45 100644 --- a/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js +++ b/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js @@ -14,6 +14,7 @@ let React; let ReactCache; let ReactTestRenderer; let act; +let assertConsoleErrorDev; describe('ReactTestRenderer', () => { beforeEach(() => { @@ -27,19 +28,27 @@ describe('ReactTestRenderer', () => { ReactTestRenderer = require('react-test-renderer'); const InternalTestUtils = require('internal-test-utils'); act = InternalTestUtils.act; + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; }); it('should warn if used to render a ReactDOM portal', async () => { const container = document.createElement('div'); let error; - await expect(async () => { - await act(() => { - ReactTestRenderer.create(ReactDOM.createPortal('foo', container)); - }).catch(e => (error = e)); - }).toErrorDev('An invalid container has been provided.', { - withoutStack: true, - }); + await act(() => { + ReactTestRenderer.create(ReactDOM.createPortal('foo', container)); + }).catch(e => (error = e)); + assertConsoleErrorDev( + [ + 'An invalid container has been provided. ' + + 'This may indicate that another renderer is being used in addition to the test renderer. ' + + '(For example, ReactDOM.createPortal inside of a ReactTestRenderer tree.) ' + + 'This is not supported.', + ], + { + withoutStack: true, + }, + ); // After the update throws, a subsequent render is scheduled to // unmount the whole tree. This update also causes an error, so React From 99471c02dd6631df1892bf76d932afd22fffa5e3 Mon Sep 17 00:00:00 2001 From: Ricky Date: Fri, 20 Dec 2024 12:41:30 -0500 Subject: [PATCH 4/4] [assert helpers] ReactFlight (#31860) --- .../src/__tests__/ReactFlight-test.js | 640 +++++++++++++----- 1 file changed, 474 insertions(+), 166 deletions(-) diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index b2489705394a1..980dbbf0e1247 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -1467,13 +1467,12 @@ describe('ReactFlight', () => { const transport = ReactNoopFlightServer.render(); - await expect(async () => { - await act(() => { - startTransition(() => { - ReactNoop.render(ReactNoopFlightClient.read(transport)); - }); + await act(() => { + startTransition(() => { + ReactNoop.render(ReactNoopFlightClient.read(transport)); }); - }).toErrorDev( + }); + assertConsoleErrorDev([ 'Each child in a list should have a unique "key" prop.\n' + '\n' + 'Check the render method of `Component`. See https://react.dev/link/warning-keys for more information.\n' + @@ -1483,7 +1482,7 @@ describe('ReactFlight', () => { ? '' : ' in Indirection (at **)\n') + ' in App (at **)', - ); + ]); }); it('should trigger the inner most error boundary inside a Client Component', async () => { @@ -1541,17 +1540,47 @@ describe('ReactFlight', () => { return 123; }, }; - expect(() => { - const transport = ReactNoopFlightServer.render(); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Objects with toJSON methods are not supported. ' + - 'Convert it manually to a simple value before passing it to props.\n' + - ' \n' + - ' ^^^^^^^^^^^^^^^', - {withoutStack: true}, - ); + const transport = ReactNoopFlightServer.render(); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' \n' + + ' ^^^^^^^^^^^^^^^', + ], + {withoutStack: true}, + ); + } + + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' \n' + + ' ^^^^^^^^^^^^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' \n' + + ' ^^^^^^^^^^^^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' \n' + + ' ^^^^^^^^^^^^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if a toJSON instance is passed to a host component child', () => { @@ -1560,43 +1589,123 @@ describe('ReactFlight', () => { return 123; } } - expect(() => { - const transport = ReactNoopFlightServer.render( -
Womp womp: {new MyError('spaghetti')}
, + const transport = ReactNoopFlightServer.render( +
Womp womp: {new MyError('spaghetti')}
, + ); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev( + [ + 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' + + '
Womp womp: {Error}
\n' + + ' ^^^^^^^', + ], + {withoutStack: true}, ); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' + - '
Womp womp: {Error}
\n' + - ' ^^^^^^^', - {withoutStack: true}, - ); + } + + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' + + '
Womp womp: {Error}
\n' + + ' ^^^^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' + + '
Womp womp: {Error}
\n' + + ' ^^^^^^^', + 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' + + '
Womp womp: {Error}
\n' + + ' ^^^^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if a special object is passed to a host component', () => { - expect(() => { - const transport = ReactNoopFlightServer.render(); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Math objects are not supported.\n' + - ' \n' + - ' ^^^^^^', - {withoutStack: true}, - ); + const transport = ReactNoopFlightServer.render(); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' \n' + + ' ^^^^^^', + ], + {withoutStack: true}, + ); + } + + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' \n' + + ' ^^^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' \n' + + ' ^^^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' \n' + + ' ^^^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if an object with symbols is passed to a host component', () => { - expect(() => { - const transport = ReactNoopFlightServer.render( - , + const transport = ReactNoopFlightServer.render( + , + ); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' \n' + + ' ^^^^', + ], + {withoutStack: true}, ); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Objects with symbol properties like Symbol.iterator are not supported.', - {withoutStack: true}, - ); + } + + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' \n' + + ' ^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' \n' + + ' ^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' \n' + + ' ^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if a toJSON instance is passed to a Client Component', () => { @@ -1609,14 +1718,47 @@ describe('ReactFlight', () => { return
{value}
; } const Client = clientReference(ClientImpl); - expect(() => { - const transport = ReactNoopFlightServer.render(); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Objects with toJSON methods are not supported.', - {withoutStack: true}, - ); + const transport = ReactNoopFlightServer.render(); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' <... value={{toJSON: ...}}>\n' + + ' ^^^^^^^^^^^^^^^', + ], + {withoutStack: true}, + ); + } + + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' <... value={{toJSON: ...}}>\n' + + ' ^^^^^^^^^^^^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' <... value={{toJSON: ...}}>\n' + + ' ^^^^^^^^^^^^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' <... value={{toJSON: ...}}>\n' + + ' ^^^^^^^^^^^^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if a toJSON instance is passed to a Client Component child', () => { @@ -1629,19 +1771,49 @@ describe('ReactFlight', () => { return
{children}
; } const Client = clientReference(ClientImpl); - expect(() => { - const transport = ReactNoopFlightServer.render( - Current date: {obj}, + const transport = ReactNoopFlightServer.render( + Current date: {obj}, + ); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' <>Current date: {{toJSON: ...}}\n' + + ' ^^^^^^^^^^^^^^^', + ], + {withoutStack: true}, ); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Objects with toJSON methods are not supported. ' + - 'Convert it manually to a simple value before passing it to props.\n' + - ' <>Current date: {{toJSON: ...}}\n' + - ' ^^^^^^^^^^^^^^^', - {withoutStack: true}, - ); + } + + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' <>Current date: {{toJSON: ...}}\n' + + ' ^^^^^^^^^^^^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' <>Current date: {{toJSON: ...}}\n' + + ' ^^^^^^^^^^^^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' <>Current date: {{toJSON: ...}}\n' + + ' ^^^^^^^^^^^^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if a special object is passed to a Client Component', () => { @@ -1649,16 +1821,44 @@ describe('ReactFlight', () => { return
{value}
; } const Client = clientReference(ClientImpl); - expect(() => { - const transport = ReactNoopFlightServer.render(); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Math objects are not supported.\n' + - ' <... value={Math}>\n' + - ' ^^^^^^', - {withoutStack: true}, - ); + const transport = ReactNoopFlightServer.render(); + + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' <... value={Math}>\n' + + ' ^^^^^^', + ], + {withoutStack: true}, + ); + } + + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' <... value={Math}>\n' + + ' ^^^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' <... value={Math}>\n' + + ' ^^^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' <... value={Math}>\n' + + ' ^^^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if an object with symbols is passed to a Client Component', () => { @@ -1666,16 +1866,46 @@ describe('ReactFlight', () => { return
{value}
; } const Client = clientReference(ClientImpl); - expect(() => { - const transport = ReactNoopFlightServer.render( - , + assertConsoleErrorDev([]); + const transport = ReactNoopFlightServer.render( + , + ); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' <... value={{}}>\n' + + ' ^^^^', + ], + {withoutStack: true}, ); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Objects with symbol properties like Symbol.iterator are not supported.', - {withoutStack: true}, - ); + } + + ReactNoopFlightClient.read(transport); + + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' <... value={{}}>\n' + + ' ^^^^\n', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' <... value={{}}>\n' + + ' ^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' <... value={{}}>\n' + + ' ^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if a special object is passed to a nested object in Client Component', () => { @@ -1683,18 +1913,41 @@ describe('ReactFlight', () => { return
{value}
; } const Client = clientReference(ClientImpl); - expect(() => { - const transport = ReactNoopFlightServer.render( - hi}} />, - ); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Math objects are not supported.\n' + - ' {hello: Math, title:

}\n' + - ' ^^^^', - {withoutStack: true}, + const transport = ReactNoopFlightServer.render( + , ); + ReactNoopFlightClient.read(transport); + + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' <... value={{}}>\n' + + ' ^^^^', + {withoutStack: true}, + ], + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' <... value={{}}>\n' + + ' ^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' <... value={{}}>\n' + + ' ^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' <... value={{}}>\n' + + ' ^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if a special object is passed to a nested array in Client Component', () => { @@ -1702,20 +1955,40 @@ describe('ReactFlight', () => { return
{value}
; } const Client = clientReference(ClientImpl); - expect(() => { - const transport = ReactNoopFlightServer.render( - hi

]} - />, - ); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Math objects are not supported.\n' + - ' [..., Math,

]\n' + - ' ^^^^', - {withoutStack: true}, + const transport = ReactNoopFlightServer.render( + hi

]} />, ); + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' [..., Math,

]\n' + + ' ^^^^', + {withoutStack: true}, + ], + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' [..., Math,

]\n' + + ' ^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' [..., Math,

]\n' + + ' ^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' [..., Math,

]\n' + + ' ^^^^', + ], + {withoutStack: true}, + ); + } }); it('should NOT warn in DEV for key getters', () => { @@ -1729,63 +2002,100 @@ describe('ReactFlight', () => { key: "this has a key but parent doesn't", }); } - expect(() => { - // While we're on the server we need to have the Server version active to track component stacks. - jest.resetModules(); - jest.mock('react', () => ReactServer); - const transport = ReactNoopFlightServer.render( - ReactServer.createElement( - 'div', - null, - Array(6).fill(ReactServer.createElement(NoKey)), - ), - ); - jest.resetModules(); - jest.mock('react', () => React); - ReactNoopFlightClient.read(transport); - }).toErrorDev('Each child in a list should have a unique "key" prop.'); + // While we're on the server we need to have the Server version active to track component stacks. + jest.resetModules(); + jest.mock('react', () => ReactServer); + const transport = ReactNoopFlightServer.render( + ReactServer.createElement( + 'div', + null, + Array(6).fill(ReactServer.createElement(NoKey)), + ), + ); + jest.resetModules(); + jest.mock('react', () => React); + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in NoKey (at **)', + 'Each child in a list should have a unique "key" prop. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in NoKey (at **)', + ]); + } else { + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using
. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in NoKey (at **)', + ]); + } }); // @gate !__DEV__ || enableOwnerStacks it('should warn in DEV a child is missing keys on a fragment', () => { - expect(() => { - // While we're on the server we need to have the Server version active to track component stacks. - jest.resetModules(); - jest.mock('react', () => ReactServer); - const transport = ReactNoopFlightServer.render( - ReactServer.createElement( - 'div', - null, - Array(6).fill(ReactServer.createElement(ReactServer.Fragment)), - ), - ); - jest.resetModules(); - jest.mock('react', () => React); - ReactNoopFlightClient.read(transport); - }).toErrorDev('Each child in a list should have a unique "key" prop.'); + // While we're on the server we need to have the Server version active to track component stacks. + jest.resetModules(); + jest.mock('react', () => ReactServer); + const transport = ReactNoopFlightServer.render( + ReactServer.createElement( + 'div', + null, + Array(6).fill(ReactServer.createElement(ReactServer.Fragment)), + ), + ); + jest.resetModules(); + jest.mock('react', () => React); + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in Fragment (at **)', + 'Each child in a list should have a unique "key" prop. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in Fragment (at **)', + ]); + } else { + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using
. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in Fragment (at **)', + ]); + } }); it('should warn in DEV a child is missing keys in client component', async () => { function ParentClient({children}) { return children; } - const Parent = clientReference(ParentClient); - await expect(async () => { + + await act(async () => { + const Parent = clientReference(ParentClient); const transport = ReactNoopFlightServer.render( {Array(6).fill(
no key
)}
, ); ReactNoopFlightClient.read(transport); - await act(async () => { - ReactNoop.render(await ReactNoopFlightClient.read(transport)); - }); - }).toErrorDev( - gate(flags => flags.enableOwnerStacks) - ? 'Each child in a list should have a unique "key" prop.' + - '\n\nCheck the top-level render call using . ' + - 'See https://react.dev/link/warning-keys for more information.' - : 'Each child in a list should have a unique "key" prop. ' + - 'See https://react.dev/link/warning-keys for more information.', - ); + + ReactNoop.render(await ReactNoopFlightClient.read(transport)); + }); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using . ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ]); + } else { + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ]); + } }); it('should error if a class instance is passed to a host component', () => { @@ -3135,19 +3445,17 @@ describe('ReactFlight', () => { }, ); - let transport; - expect(() => { - // Reset the modules so that we get a new overridden console on top of the - // one installed by expect. This ensures that we still emit console.error - // calls. - jest.resetModules(); - jest.mock('react', () => require('react/react.react-server')); - ReactServer = require('react'); - ReactNoopFlightServer = require('react-noop-renderer/flight-server'); - transport = ReactNoopFlightServer.render({ - root: ReactServer.createElement(App), - }); - }).toErrorDev('err'); + // Reset the modules so that we get a new overridden console on top of the + // one installed by expect. This ensures that we still emit console.error + // calls. + jest.resetModules(); + jest.mock('react', () => require('react/react.react-server')); + ReactServer = require('react'); + ReactNoopFlightServer = require('react-noop-renderer/flight-server'); + const transport = ReactNoopFlightServer.render({ + root: ReactServer.createElement(App), + }); + assertConsoleErrorDev(['Error: err']); expect(mockConsoleLog).toHaveBeenCalledTimes(1); expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');