diff --git a/packages/react-devtools-shared/src/__tests__/__serializers__/storeSerializer.js b/packages/react-devtools-shared/src/__tests__/__serializers__/storeSerializer.js index ea5a402bdc..c7f19f1404 100644 --- a/packages/react-devtools-shared/src/__tests__/__serializers__/storeSerializer.js +++ b/packages/react-devtools-shared/src/__tests__/__serializers__/storeSerializer.js @@ -5,10 +5,7 @@ export function test(maybeStore) { // It's important to lazy-require the Store rather than imported at the head of the module. // Because we reset modules between tests, different Store implementations will be used for each test. // Unfortunately Jest does not reset its own serializer modules. - return ( - maybeStore instanceof - require('react-devtools-shared/src/devtools/store').default - ); + return maybeStore?.constructor?.name === 'Store'; } // print() is part of Jest's serializer API diff --git a/packages/react-devtools-shared/src/__tests__/store-test.js b/packages/react-devtools-shared/src/__tests__/store-test.js index fad4622acc..3b60d5ae09 100644 --- a/packages/react-devtools-shared/src/__tests__/store-test.js +++ b/packages/react-devtools-shared/src/__tests__/store-test.js @@ -2489,7 +2489,7 @@ describe('Store', () => { withErrorsOrWarningsIgnored(['test-only:'], async () => { await act(() => render()); }); - expect(store).toMatchInlineSnapshot(`[root]`); + expect(store).toMatchInlineSnapshot(``); expect(store.componentWithErrorCount).toBe(0); expect(store.componentWithWarningCount).toBe(0); }); @@ -3083,6 +3083,9 @@ describe('Store', () => { it('should handle an empty root', async () => { await actAsync(() => render(null)); + expect(store).toMatchInlineSnapshot(``); + + await actAsync(() => render()); expect(store).toMatchInlineSnapshot(`[root]`); }); }); diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 12e2ce31fb..17551f6211 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -4405,8 +4405,10 @@ export function attach( traceNearestHostComponentUpdate, virtualLevel, ); - updateFlags |= - ShouldResetChildren | ShouldResetSuspenseChildren; + updateFlags |= ShouldResetSuspenseChildren; + if (!isInDisconnectedSubtree) { + updateFlags |= ShouldResetChildren; + } } else { updateFlags |= updateVirtualInstanceRecursively( previousVirtualInstance, @@ -4459,7 +4461,10 @@ export function attach( insertChild(newVirtualInstance); previousVirtualInstance = newVirtualInstance; previousVirtualInstanceWasMount = true; - updateFlags |= ShouldResetChildren; + updateFlags |= ShouldResetSuspenseChildren; + if (!isInDisconnectedSubtree) { + updateFlags |= ShouldResetChildren; + } } // Existing children might be reparented into this new virtual instance. // TODO: This will cause the front end to error which needs to be fixed. @@ -4486,7 +4491,10 @@ export function attach( traceNearestHostComponentUpdate, virtualLevel, ); - updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; + updateFlags |= ShouldResetSuspenseChildren; + if (!isInDisconnectedSubtree) { + updateFlags |= ShouldResetChildren; + } } else { updateFlags |= updateVirtualInstanceRecursively( previousVirtualInstance, @@ -4538,7 +4546,10 @@ export function attach( // They are always different referentially, but if the instances line up // conceptually we'll want to know that. if (prevChild !== prevChildAtSameIndex) { - updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; + updateFlags |= ShouldResetSuspenseChildren; + if (!isInDisconnectedSubtree) { + updateFlags |= ShouldResetChildren; + } } moveChild(fiberInstance, previousSiblingOfExistingInstance); @@ -4555,7 +4566,10 @@ export function attach( } else if (prevChild !== null && shouldFilterFiber(nextChild)) { // The filtered instance could've reordered. if (prevChild !== prevChildAtSameIndex) { - updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; + updateFlags |= ShouldResetSuspenseChildren; + if (!isInDisconnectedSubtree) { + updateFlags |= ShouldResetChildren; + } } // If this Fiber should be filtered, we need to still update its children. @@ -4579,7 +4593,10 @@ export function attach( mountFiberRecursively(nextChild, traceNearestHostComponentUpdate); // Need to mark the parent set to remount the new instance. - updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; + updateFlags |= ShouldResetSuspenseChildren; + if (!isInDisconnectedSubtree) { + updateFlags |= ShouldResetChildren; + } } } // Try the next child. @@ -4602,7 +4619,10 @@ export function attach( traceNearestHostComponentUpdate, virtualLevel, ); - updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; + updateFlags |= ShouldResetSuspenseChildren; + if (!isInDisconnectedSubtree) { + updateFlags |= ShouldResetChildren; + } } else { updateFlags |= updateVirtualInstanceRecursively( previousVirtualInstance, @@ -4616,7 +4636,10 @@ export function attach( } // If we have no more children, but used to, they don't line up. if (prevChildAtSameIndex !== null) { - updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; + updateFlags |= ShouldResetSuspenseChildren; + if (!isInDisconnectedSubtree) { + updateFlags |= ShouldResetChildren; + } } return updateFlags; } @@ -4628,7 +4651,9 @@ export function attach( traceNearestHostComponentUpdate: boolean, ): UpdateFlags { if (nextFirstChild === null) { - return prevFirstChild !== null ? ShouldResetChildren : NoUpdate; + return prevFirstChild !== null && !isInDisconnectedSubtree + ? ShouldResetChildren + : NoUpdate; } return updateVirtualChildrenRecursively( nextFirstChild, @@ -4854,7 +4879,10 @@ export function attach( traceNearestHostComponentUpdate, ); - updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; + updateFlags |= ShouldResetSuspenseChildren; + if (!isInDisconnectedSubtree) { + updateFlags |= ShouldResetChildren; + } } const childrenUpdateFlags = @@ -4877,7 +4905,10 @@ export function attach( nextPrimaryChildSet, traceNearestHostComponentUpdate, ); - updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; + updateFlags |= ShouldResetSuspenseChildren; + if (!isInDisconnectedSubtree) { + updateFlags |= ShouldResetChildren; + } } } else if (!prevDidTimeout && nextDidTimeOut) { // Primary -> Fallback: @@ -4894,7 +4925,10 @@ export function attach( nextFallbackChildSet, traceNearestHostComponentUpdate, ); - updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; + updateFlags |= ShouldResetSuspenseChildren; + if (!isInDisconnectedSubtree) { + updateFlags |= ShouldResetChildren; + } } } else if (nextIsHidden) { if (!prevWasHidden) { @@ -4939,7 +4973,10 @@ export function attach( if (fiberInstance !== null && !isInDisconnectedSubtree) { reconnectChildrenRecursively(fiberInstance); // Children may have reordered while they were hidden. - updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; + updateFlags |= ShouldResetSuspenseChildren; + if (!isInDisconnectedSubtree) { + updateFlags |= ShouldResetChildren; + } } } else if ( nextFiber.tag === SuspenseComponent && @@ -5318,12 +5355,12 @@ export function attach( root: FiberRoot, priorityLevel: void | number, ) { - const current = root.current; + const nextFiber = root.current; let prevFiber: null | Fiber = null; let rootInstance = rootToFiberInstanceMap.get(root); if (!rootInstance) { - rootInstance = createFiberInstance(current); + rootInstance = createFiberInstance(nextFiber); rootToFiberInstanceMap.set(root, rootInstance); idToDevToolsInstanceMap.set(rootInstance.id, rootInstance); } else { @@ -5362,30 +5399,25 @@ export function attach( }; } - if (prevFiber !== null) { - // TODO: relying on this seems a bit fishy. - const wasMounted = - prevFiber.memoizedState != null && - prevFiber.memoizedState.element != null; - const isMounted = - current.memoizedState != null && current.memoizedState.element != null; - if (!wasMounted && isMounted) { - // Mount a new root. - setRootPseudoKey(currentRoot.id, current); - mountFiberRecursively(current, false); - } else if (wasMounted && isMounted) { - // Update an existing root. - updateFiberRecursively(rootInstance, current, prevFiber, false); - } else if (wasMounted && !isMounted) { - // Unmount an existing root. - unmountInstanceRecursively(rootInstance); - removeRootPseudoKey(currentRoot.id); - rootToFiberInstanceMap.delete(root); - } - } else { + const isMounted = nextFiber.child !== null; + const wasMounted = prevFiber !== null && prevFiber.child !== null; + if (!wasMounted && isMounted) { // Mount a new root. - setRootPseudoKey(currentRoot.id, current); - mountFiberRecursively(current, false); + setRootPseudoKey(currentRoot.id, nextFiber); + mountFiberRecursively(nextFiber, false); + } else if (wasMounted && isMounted) { + if (prevFiber === null) { + throw new Error( + 'Expected previous fiber when updating an existing root', + ); + } + // Update an existing root. + updateFiberRecursively(rootInstance, nextFiber, prevFiber, false); + } else if (wasMounted && !isMounted) { + // Unmount an existing root. + unmountInstanceRecursively(rootInstance); + removeRootPseudoKey(currentRoot.id); + rootToFiberInstanceMap.delete(root); } if (isProfiling && isProfilingSupported) { diff --git a/packages/react-dom/src/__tests__/ReactDOM-test.js b/packages/react-dom/src/__tests__/ReactDOM-test.js index ff82965445..e80841c844 100644 --- a/packages/react-dom/src/__tests__/ReactDOM-test.js +++ b/packages/react-dom/src/__tests__/ReactDOM-test.js @@ -959,5 +959,15 @@ describe('ReactDOM', () => { expect(document.documentElement.outerHTML).toBe( '
goodbye!
', ); + + expect(window.store).toMatchInlineSnapshot(` + [root] + ▾ + ▾ + + + [suspense-root] rects={[]} + + `); }); }); diff --git a/scripts/jest/config.base.js b/scripts/jest/config.base.js index ba001e165e..e36329cdf6 100644 --- a/scripts/jest/config.base.js +++ b/scripts/jest/config.base.js @@ -1,5 +1,10 @@ 'use strict'; +const moduleNameMapper = {}; + +moduleNameMapper['react-devtools-feature-flags'] = + '/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.default'; + module.exports = { globalSetup: require.resolve('./setupGlobal.js'), modulePathIgnorePatterns: [ @@ -26,6 +31,7 @@ module.exports = { // Only include files directly in __tests__, not in nested folders. testRegex: '/__tests__/[^/]*(\\.js|\\.coffee|[^d]\\.ts)$', moduleFileExtensions: ['js', 'json', 'node', 'coffee', 'ts'], + moduleNameMapper, rootDir: process.cwd(), roots: ['/packages', '/scripts'], collectCoverageFrom: ['packages/**/*.js'], @@ -33,7 +39,12 @@ module.exports = { enableGlobally: true, legacyFakeTimers: true, }, - snapshotSerializers: [require.resolve('jest-snapshot-serializer-raw')], + snapshotSerializers: [ + require.resolve( + '../../packages/react-devtools-shared/src/__tests__/__serializers__/storeSerializer.js' + ), + require.resolve('jest-snapshot-serializer-raw'), + ], testEnvironment: '/scripts/jest/ReactJSDOMEnvironment', diff --git a/scripts/jest/setupEnvironment.js b/scripts/jest/setupEnvironment.js index 44acb04f18..c7ac16110f 100644 --- a/scripts/jest/setupEnvironment.js +++ b/scripts/jest/setupEnvironment.js @@ -8,6 +8,11 @@ global.__DEV__ = NODE_ENV === 'development'; global.__EXTENSION__ = false; global.__TEST__ = NODE_ENV === 'test'; global.__PROFILE__ = NODE_ENV === 'development'; +global.__IS_FIREFOX__ = false; +global.__IS_CHROME__ = false; +global.__IS_EDGE__ = false; +global.__IS_NATIVE__ = false; +global.__IS_INTERNAL_MCP_BUILD__ = false; const RELEASE_CHANNEL = process.env.RELEASE_CHANNEL; diff --git a/scripts/jest/setupTests.js b/scripts/jest/setupTests.js index 1e8d8e5276..563eeda5e7 100644 --- a/scripts/jest/setupTests.js +++ b/scripts/jest/setupTests.js @@ -67,6 +67,72 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) { beforeEach(resetAllUnexpectedConsoleCalls); afterEach(assertConsoleLogsCleared); + const devtoolsHookTarget = globalThis; + beforeEach(() => { + const Agent = require('react-devtools-shared/src/backend/agent').default; + const {initBackend} = require('react-devtools-shared/src/backend'); + const Bridge = require('react-devtools-shared/src/bridge').default; + const Store = require('react-devtools-shared/src/devtools/store').default; + const {installHook} = require('react-devtools-shared/src/hook'); + const { + getDefaultComponentFilters, + setSavedComponentFilters, + } = require('react-devtools-shared/src/utils'); + + // Initialize filters to a known good state. + setSavedComponentFilters(getDefaultComponentFilters()); + devtoolsHookTarget.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = + getDefaultComponentFilters(); + + // Also initialize inline warnings so that we can test them. + devtoolsHookTarget.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = + true; + + installHook(devtoolsHookTarget, { + appendComponentStack: false, + breakOnConsoleErrors: false, + showInlineWarningsAndErrors: true, + hideConsoleLogsInStrictMode: false, + }); + + const bridgeListeners = []; + const bridge = new Bridge({ + listen(callback) { + bridgeListeners.push(callback); + return () => { + const index = bridgeListeners.indexOf(callback); + if (index >= 0) { + bridgeListeners.splice(index, 1); + } + }; + }, + send(event: string, payload: any, transferable: Array) { + bridgeListeners.forEach(callback => + callback({event, payload, transferable}) + ); + }, + }); + + const store = new Store(bridge, { + supportsTimeline: true, + }); + + const agent = new Agent(bridge); + const hook = devtoolsHookTarget.__REACT_DEVTOOLS_GLOBAL_HOOK__; + initBackend(hook, agent, devtoolsHookTarget); + + devtoolsHookTarget.agent = agent; + devtoolsHookTarget.bridge = bridge; + devtoolsHookTarget.store = store; + }); + + afterEach(() => { + delete devtoolsHookTarget.__REACT_DEVTOOLS_GLOBAL_HOOK__; + delete devtoolsHookTarget.store; + delete devtoolsHookTarget.agent; + delete devtoolsHookTarget.bridge; + }); + // TODO: enable this check so we don't forget to reset spyOnX mocks. // afterEach(() => { // if (