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 (