Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion packages/react-devtools-shared/src/__tests__/store-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2489,7 +2489,7 @@ describe('Store', () => {
withErrorsOrWarningsIgnored(['test-only:'], async () => {
await act(() => render(<React.Fragment />));
});
expect(store).toMatchInlineSnapshot(`[root]`);
expect(store).toMatchInlineSnapshot(``);
expect(store.componentWithErrorCount).toBe(0);
expect(store.componentWithWarningCount).toBe(0);
});
Expand Down Expand Up @@ -3083,6 +3083,9 @@ describe('Store', () => {

it('should handle an empty root', async () => {
await actAsync(() => render(null));
expect(store).toMatchInlineSnapshot(``);

await actAsync(() => render(<span />));
expect(store).toMatchInlineSnapshot(`[root]`);
});
});
110 changes: 71 additions & 39 deletions packages/react-devtools-shared/src/backend/fiber/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -4405,8 +4405,10 @@ export function attach(
traceNearestHostComponentUpdate,
virtualLevel,
);
updateFlags |=
ShouldResetChildren | ShouldResetSuspenseChildren;
updateFlags |= ShouldResetSuspenseChildren;
if (!isInDisconnectedSubtree) {
updateFlags |= ShouldResetChildren;
}
} else {
updateFlags |= updateVirtualInstanceRecursively(
previousVirtualInstance,
Expand Down Expand Up @@ -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.
Expand All @@ -4486,7 +4491,10 @@ export function attach(
traceNearestHostComponentUpdate,
virtualLevel,
);
updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren;
updateFlags |= ShouldResetSuspenseChildren;
if (!isInDisconnectedSubtree) {
updateFlags |= ShouldResetChildren;
}
} else {
updateFlags |= updateVirtualInstanceRecursively(
previousVirtualInstance,
Expand Down Expand Up @@ -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);
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -4602,7 +4619,10 @@ export function attach(
traceNearestHostComponentUpdate,
virtualLevel,
);
updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren;
updateFlags |= ShouldResetSuspenseChildren;
if (!isInDisconnectedSubtree) {
updateFlags |= ShouldResetChildren;
}
} else {
updateFlags |= updateVirtualInstanceRecursively(
previousVirtualInstance,
Expand All @@ -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;
}
Expand All @@ -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,
Expand Down Expand Up @@ -4854,7 +4879,10 @@ export function attach(
traceNearestHostComponentUpdate,
);

updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren;
updateFlags |= ShouldResetSuspenseChildren;
if (!isInDisconnectedSubtree) {
updateFlags |= ShouldResetChildren;
}
}

const childrenUpdateFlags =
Expand All @@ -4877,7 +4905,10 @@ export function attach(
nextPrimaryChildSet,
traceNearestHostComponentUpdate,
);
updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren;
updateFlags |= ShouldResetSuspenseChildren;
if (!isInDisconnectedSubtree) {
updateFlags |= ShouldResetChildren;
}
}
} else if (!prevDidTimeout && nextDidTimeOut) {
// Primary -> Fallback:
Expand All @@ -4894,7 +4925,10 @@ export function attach(
nextFallbackChildSet,
traceNearestHostComponentUpdate,
);
updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren;
updateFlags |= ShouldResetSuspenseChildren;
if (!isInDisconnectedSubtree) {
updateFlags |= ShouldResetChildren;
}
}
} else if (nextIsHidden) {
if (!prevWasHidden) {
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
10 changes: 10 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOM-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -959,5 +959,15 @@ describe('ReactDOM', () => {
expect(document.documentElement.outerHTML).toBe(
'<html lang="en"><head><meta itemprop="" content="primary" style=""></head><body><div style="">goodbye!</div></body></html>',
);

expect(window.store).toMatchInlineSnapshot(`
[root]
▾ <App>
▾ <Suspense>
<Comp>
<Message>
[suspense-root] rects={[]}
<Suspense name="App" rects={[]}>
`);
});
});
13 changes: 12 additions & 1 deletion scripts/jest/config.base.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
'use strict';

const moduleNameMapper = {};

moduleNameMapper['react-devtools-feature-flags'] =
'<rootDir>/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.default';

module.exports = {
globalSetup: require.resolve('./setupGlobal.js'),
modulePathIgnorePatterns: [
Expand All @@ -26,14 +31,20 @@ 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: ['<rootDir>/packages', '<rootDir>/scripts'],
collectCoverageFrom: ['packages/**/*.js'],
fakeTimers: {
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: '<rootDir>/scripts/jest/ReactJSDOMEnvironment',

Expand Down
5 changes: 5 additions & 0 deletions scripts/jest/setupEnvironment.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
66 changes: 66 additions & 0 deletions scripts/jest/setupTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>) {
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 (
Expand Down