[DevTools] Send Suspense rects to frontend (#34170)

This commit is contained in:
Sebastian "Sebbie" Silbermann 2025-08-12 16:48:35 +02:00 committed by GitHub
parent ac7820a99e
commit de06211dbe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 586 additions and 134 deletions

View File

@ -228,6 +228,8 @@ describe('commit tree', () => {
[root]
<App>
<Suspense>
[shell]
<Suspense name="App>?" rects={null}>
`);
utils.act(() => modernRender(<App renderChildren={true} />));
expect(store).toMatchInlineSnapshot(`
@ -235,6 +237,8 @@ describe('commit tree', () => {
<App>
<Suspense>
<LazyInnerComponent>
[shell]
<Suspense name="App>?" rects={null}>
`);
utils.act(() => modernRender(<App renderChildren={false} />));
expect(store).toMatchInlineSnapshot(`
@ -299,6 +303,8 @@ describe('commit tree', () => {
[root]
<App>
<Suspense>
[shell]
<Suspense name="App>?" rects={null}>
`);
utils.act(() => modernRender(<App renderChildren={false} />));
expect(store).toMatchInlineSnapshot(`

View File

@ -24,6 +24,16 @@ describe('Store', () => {
let store;
let withErrorsOrWarningsIgnored;
beforeAll(() => {
// JSDDOM doesn't implement getClientRects so we're just faking one for testing purposes
Element.prototype.getClientRects = function (this: Element) {
const textContent = this.textContent;
return [
new DOMRect(1, 2, textContent.length, textContent.split('\n').length),
];
};
});
beforeEach(() => {
global.IS_REACT_ACT_ENVIRONMENT = true;
@ -123,6 +133,8 @@ describe('Store', () => {
<Suspense>
<Parent>
<Child>
[shell]
<Suspense name="Unknown" rects={null}>
`);
});
@ -480,6 +492,8 @@ describe('Store', () => {
<Component key="Outside">
<Suspense>
<Loading>
[shell]
<Suspense name="Wrapper>?" rects={null}>
`);
await act(() => {
@ -491,6 +505,8 @@ describe('Store', () => {
<Component key="Outside">
<Suspense>
<Component key="Inside">
[shell]
<Suspense name="Wrapper>?" rects={[{x:1,y:2,width:5,height:1}]}>
`);
});
@ -513,23 +529,31 @@ describe('Store', () => {
}) => (
<React.Fragment>
<Component key="Outside" />
<React.Suspense fallback={<Loading key="Parent Fallback" />}>
<React.Suspense
name="parent"
fallback={<Loading key="Parent Fallback" />}>
<Component key="Unrelated at Start" />
<React.Suspense fallback={<Loading key="Suspense 1 Fallback" />}>
<React.Suspense
name="one"
fallback={<Loading key="Suspense 1 Fallback" />}>
{suspendFirst ? (
<Never />
) : (
<Component key="Suspense 1 Content" />
)}
</React.Suspense>
<React.Suspense fallback={<Loading key="Suspense 2 Fallback" />}>
<React.Suspense
name="two"
fallback={<Loading key="Suspense 2 Fallback" />}>
{suspendSecond ? (
<Never />
) : (
<Component key="Suspense 2 Content" />
)}
</React.Suspense>
<React.Suspense fallback={<Loading key="Suspense 3 Fallback" />}>
<React.Suspense
name="three"
fallback={<Loading key="Suspense 3 Fallback" />}>
<Never />
</React.Suspense>
{suspendParent && <Never />}
@ -538,7 +562,7 @@ describe('Store', () => {
</React.Fragment>
);
await act(() =>
await actAsync(() =>
render(
<Wrapper
suspendParent={false}
@ -551,15 +575,20 @@ describe('Store', () => {
[root]
<Wrapper>
<Component key="Outside">
<Suspense>
<Suspense name="parent">
<Component key="Unrelated at Start">
<Suspense>
<Suspense name="one">
<Component key="Suspense 1 Content">
<Suspense>
<Suspense name="two">
<Component key="Suspense 2 Content">
<Suspense>
<Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
await act(() =>
render(
@ -574,15 +603,20 @@ describe('Store', () => {
[root]
<Wrapper>
<Component key="Outside">
<Suspense>
<Suspense name="parent">
<Component key="Unrelated at Start">
<Suspense>
<Suspense name="one">
<Loading key="Suspense 1 Fallback">
<Suspense>
<Suspense name="two">
<Component key="Suspense 2 Content">
<Suspense>
<Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
await act(() =>
render(
@ -597,15 +631,20 @@ describe('Store', () => {
[root]
<Wrapper>
<Component key="Outside">
<Suspense>
<Suspense name="parent">
<Component key="Unrelated at Start">
<Suspense>
<Suspense name="one">
<Component key="Suspense 1 Content">
<Suspense>
<Suspense name="two">
<Loading key="Suspense 2 Fallback">
<Suspense>
<Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
await act(() =>
render(
@ -620,15 +659,20 @@ describe('Store', () => {
[root]
<Wrapper>
<Component key="Outside">
<Suspense>
<Suspense name="parent">
<Component key="Unrelated at Start">
<Suspense>
<Suspense name="one">
<Loading key="Suspense 1 Fallback">
<Suspense>
<Suspense name="two">
<Component key="Suspense 2 Content">
<Suspense>
<Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
await act(() =>
render(
@ -643,8 +687,13 @@ describe('Store', () => {
[root]
<Wrapper>
<Component key="Outside">
<Suspense>
<Suspense name="parent">
<Loading key="Parent Fallback">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
await act(() =>
render(
@ -659,15 +708,20 @@ describe('Store', () => {
[root]
<Wrapper>
<Component key="Outside">
<Suspense>
<Suspense name="parent">
<Component key="Unrelated at Start">
<Suspense>
<Suspense name="one">
<Loading key="Suspense 1 Fallback">
<Suspense>
<Suspense name="two">
<Loading key="Suspense 2 Fallback">
<Suspense>
<Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
await act(() =>
render(
@ -682,15 +736,20 @@ describe('Store', () => {
[root]
<Wrapper>
<Component key="Outside">
<Suspense>
<Suspense name="parent">
<Component key="Unrelated at Start">
<Suspense>
<Suspense name="one">
<Component key="Suspense 1 Content">
<Suspense>
<Suspense name="two">
<Component key="Suspense 2 Content">
<Suspense>
<Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
const rendererID = getRendererID();
@ -705,15 +764,20 @@ describe('Store', () => {
[root]
<Wrapper>
<Component key="Outside">
<Suspense>
<Suspense name="parent">
<Component key="Unrelated at Start">
<Suspense>
<Suspense name="one">
<Loading key="Suspense 1 Fallback">
<Suspense>
<Suspense name="two">
<Component key="Suspense 2 Content">
<Suspense>
<Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
await act(() =>
agent.overrideSuspense({
@ -726,8 +790,13 @@ describe('Store', () => {
[root]
<Wrapper>
<Component key="Outside">
<Suspense>
<Suspense name="parent">
<Loading key="Parent Fallback">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
await act(() =>
render(
@ -742,8 +811,13 @@ describe('Store', () => {
[root]
<Wrapper>
<Component key="Outside">
<Suspense>
<Suspense name="parent">
<Loading key="Parent Fallback">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
await act(() =>
agent.overrideSuspense({
@ -756,15 +830,20 @@ describe('Store', () => {
[root]
<Wrapper>
<Component key="Outside">
<Suspense>
<Suspense name="parent">
<Component key="Unrelated at Start">
<Suspense>
<Suspense name="one">
<Loading key="Suspense 1 Fallback">
<Suspense>
<Suspense name="two">
<Loading key="Suspense 2 Fallback">
<Suspense>
<Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
await act(() =>
agent.overrideSuspense({
@ -777,15 +856,20 @@ describe('Store', () => {
[root]
<Wrapper>
<Component key="Outside">
<Suspense>
<Suspense name="parent">
<Component key="Unrelated at Start">
<Suspense>
<Suspense name="one">
<Loading key="Suspense 1 Fallback">
<Suspense>
<Suspense name="two">
<Loading key="Suspense 2 Fallback">
<Suspense>
<Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
await act(() =>
render(
@ -800,15 +884,20 @@ describe('Store', () => {
[root]
<Wrapper>
<Component key="Outside">
<Suspense>
<Suspense name="parent">
<Component key="Unrelated at Start">
<Suspense>
<Suspense name="one">
<Component key="Suspense 1 Content">
<Suspense>
<Suspense name="two">
<Component key="Suspense 2 Content">
<Suspense>
<Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
});
@ -848,6 +937,8 @@ describe('Store', () => {
<Component key="A">
<Suspense>
<Loading>
[shell]
<Suspense name="Wrapper>?" rects={null}>
`);
await act(() => {
@ -861,6 +952,8 @@ describe('Store', () => {
<Suspense>
<Component key="B">
<Component key="C">
[shell]
<Suspense name="Wrapper>?" rects={[{x:1,y:2,width:5,height:1}]}>
`);
});
@ -1197,6 +1290,8 @@ describe('Store', () => {
expect(store).toMatchInlineSnapshot(`
[root]
<Wrapper>
[shell]
<Suspense name="Wrapper>?" rects={null}>
`);
// This test isn't meaningful unless we expand the suspended tree
@ -1212,6 +1307,8 @@ describe('Store', () => {
<Component key="Outside">
<Suspense>
<Loading>
[shell]
<Suspense name="Wrapper>?" rects={null}>
`);
await act(() => {
@ -1223,6 +1320,8 @@ describe('Store', () => {
<Component key="Outside">
<Suspense>
<Component key="Inside">
[shell]
<Suspense name="Wrapper>?" rects={[{x:1,y:2,width:5,height:1}]}>
`);
});
@ -1447,6 +1546,8 @@ describe('Store', () => {
expect(store).toMatchInlineSnapshot(`
[root]
<SuspenseTree>
[shell]
<Suspense name="SuspenseTree>?" rects={null}>
`);
await act(() =>
@ -1460,6 +1561,8 @@ describe('Store', () => {
<SuspenseTree>
<Suspense>
<Parent>
[shell]
<Suspense name="SuspenseTree>?" rects={null}>
`);
const rendererID = getRendererID();
@ -1477,6 +1580,8 @@ describe('Store', () => {
<SuspenseTree>
<Suspense>
<Fallback>
[shell]
<Suspense name="SuspenseTree>?" rects={null}>
`);
await act(() =>
@ -1491,6 +1596,8 @@ describe('Store', () => {
<SuspenseTree>
<Suspense>
<Parent>
[shell]
<Suspense name="SuspenseTree>?" rects={null}>
`);
});
});
@ -1794,6 +1901,8 @@ describe('Store', () => {
[root]
<App>
<Suspense>
[shell]
<Suspense name="App>?" rects={null}>
`);
await Promise.resolve();
@ -1806,6 +1915,8 @@ describe('Store', () => {
<App>
<Suspense>
<LazyInnerComponent>
[shell]
<Suspense name="App>?" rects={null}>
`);
// Render again to unmount it
@ -2291,20 +2402,24 @@ describe('Store', () => {
await actAsync(() => render(<App renderA={true} />));
expect(store).toMatchInlineSnapshot(`
[root]
<App>
<Suspense>
<ChildA>
`);
[root]
<App>
<Suspense>
<ChildA>
[shell]
<Suspense name="App>?" rects={null}>
`);
await actAsync(() => render(<App renderA={false} />));
expect(store).toMatchInlineSnapshot(`
[root]
<App>
<Suspense>
<ChildB>
`);
[root]
<App>
<Suspense>
<ChildB>
[shell]
<Suspense name="App>?" rects={null}>
`);
});
});

View File

@ -156,6 +156,9 @@ describe('Store component filters', () => {
<div>
<Suspense>
<div>
[shell]
<Suspense name="Unknown" rects={[]}>
<Suspense name="Unknown" rects={[]}>
`);
await actAsync(
@ -171,6 +174,9 @@ describe('Store component filters', () => {
<div>
<Suspense>
<div>
[shell]
<Suspense name="Unknown" rects={[]}>
<Suspense name="Unknown" rects={[]}>
`);
await actAsync(
@ -186,6 +192,9 @@ describe('Store component filters', () => {
<div>
<Suspense>
<div>
[shell]
<Suspense name="Unknown" rects={[]}>
<Suspense name="Unknown" rects={[]}>
`);
});

View File

@ -32,7 +32,7 @@ describe('StoreStressConcurrent', () => {
// this helper with the real thing.
actAsync = require('./utils').actAsync;
print = require('./__serializers__/storeSerializer').print;
print = require('./__serializers__/storeSerializer').printStore;
});
// This is a stress test for the tree mount/update/unmount traversal.
@ -67,8 +67,7 @@ describe('StoreStressConcurrent', () => {
let container = document.createElement('div');
let root = ReactDOMClient.createRoot(container);
act(() => root.render(<Parent>{[a, b, c, d, e]}</Parent>));
expect(store).toMatchInlineSnapshot(
`
expect(store).toMatchInlineSnapshot(`
[root]
<Parent>
<A key="a">
@ -76,8 +75,7 @@ describe('StoreStressConcurrent', () => {
<C key="c">
<D key="d">
<E key="e">
`,
);
`);
expect(container.textContent).toMatch('abcde');
const snapshotForABCDE = print(store);
@ -86,8 +84,7 @@ describe('StoreStressConcurrent', () => {
act(() => {
setShowX(true);
});
expect(store).toMatchInlineSnapshot(
`
expect(store).toMatchInlineSnapshot(`
[root]
<Parent>
<A key="a">
@ -96,8 +93,7 @@ describe('StoreStressConcurrent', () => {
<X>
<D key="d">
<E key="e">
`,
);
`);
expect(container.textContent).toMatch('abxde');
const snapshotForABXDE = print(store);
@ -419,7 +415,7 @@ describe('StoreStressConcurrent', () => {
),
);
// We snapshot each step once so it doesn't regress.d
snapshots.push(print(store));
snapshots.push(print(store, false, null, false));
await act(() => root.unmount());
expect(print(store)).toBe('');
}
@ -524,7 +520,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
await act(() => root.unmount());
expect(print(store)).toBe('');
}
@ -544,7 +540,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Re-render with steps[j].
await act(() =>
root.render(
@ -556,7 +552,7 @@ describe('StoreStressConcurrent', () => {
),
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
expect(print(store, false, null, false)).toEqual(snapshots[j]);
// Check that we can transition back again.
await act(() =>
root.render(
@ -567,7 +563,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Clean up after every iteration.
await act(() => root.unmount());
expect(print(store)).toBe('');
@ -593,7 +589,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Re-render with steps[j].
await act(() =>
root.render(
@ -609,7 +605,7 @@ describe('StoreStressConcurrent', () => {
),
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
expect(print(store, false, null, false)).toEqual(snapshots[j]);
// Check that we can transition back again.
await act(() =>
root.render(
@ -624,7 +620,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Clean up after every iteration.
await act(() => root.unmount());
expect(print(store)).toBe('');
@ -646,7 +642,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Re-render with steps[j].
await act(() =>
root.render(
@ -662,7 +658,7 @@ describe('StoreStressConcurrent', () => {
),
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
expect(print(store, false, null, false)).toEqual(snapshots[j]);
// Check that we can transition back again.
await act(() =>
root.render(
@ -673,7 +669,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Clean up after every iteration.
await act(() => root.unmount());
expect(print(store)).toBe('');
@ -699,7 +695,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Re-render with steps[j].
await act(() =>
root.render(
@ -711,7 +707,7 @@ describe('StoreStressConcurrent', () => {
),
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
expect(print(store, false, null, false)).toEqual(snapshots[j]);
// Check that we can transition back again.
await act(() =>
root.render(
@ -726,7 +722,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Clean up after every iteration.
await act(() => root.unmount());
expect(print(store)).toBe('');
@ -755,7 +751,7 @@ describe('StoreStressConcurrent', () => {
const suspenseID = store.getElementIDAtIndex(2);
// Force fallback.
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
await actAsync(async () => {
bridge.send('overrideSuspense', {
id: suspenseID,
@ -763,7 +759,7 @@ describe('StoreStressConcurrent', () => {
forceFallback: true,
});
});
expect(print(store)).toEqual(snapshots[j]);
expect(print(store, false, null, false)).toEqual(snapshots[j]);
// Stop forcing fallback.
await actAsync(async () => {
@ -773,7 +769,7 @@ describe('StoreStressConcurrent', () => {
forceFallback: false,
});
});
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Trigger actual fallback.
await act(() =>
@ -789,7 +785,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[j]);
expect(print(store, false, null, false)).toEqual(snapshots[j]);
// Force fallback while we're in fallback mode.
await act(() => {
@ -800,7 +796,7 @@ describe('StoreStressConcurrent', () => {
});
});
// Keep seeing fallback content.
expect(print(store)).toEqual(snapshots[j]);
expect(print(store, false, null, false)).toEqual(snapshots[j]);
// Switch to primary mode.
await act(() =>
@ -813,7 +809,7 @@ describe('StoreStressConcurrent', () => {
),
);
// Fallback is still forced though.
expect(print(store)).toEqual(snapshots[j]);
expect(print(store, false, null, false)).toEqual(snapshots[j]);
// Stop forcing fallback. This reverts to primary content.
await actAsync(async () => {
@ -824,7 +820,7 @@ describe('StoreStressConcurrent', () => {
});
});
// Now we see primary content.
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Clean up after every iteration.
await actAsync(async () => root.unmount());
@ -910,7 +906,7 @@ describe('StoreStressConcurrent', () => {
),
);
// We snapshot each step once so it doesn't regress.
snapshots.push(print(store));
snapshots.push(print(store, false, null, false));
await act(() => root.unmount());
expect(print(store)).toBe('');
}
@ -935,7 +931,7 @@ describe('StoreStressConcurrent', () => {
),
);
// We snapshot each step once so it doesn't regress.
fallbackSnapshots.push(print(store));
fallbackSnapshots.push(print(store, false, null, false));
await act(() => root.unmount());
expect(print(store)).toBe('');
}
@ -1065,7 +1061,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Re-render with steps[j].
await act(() =>
root.render(
@ -1079,7 +1075,7 @@ describe('StoreStressConcurrent', () => {
),
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
expect(print(store, false, null, false)).toEqual(snapshots[j]);
// Check that we can transition back again.
await act(() =>
root.render(
@ -1092,7 +1088,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Clean up after every iteration.
await act(() => root.unmount());
expect(print(store)).toBe('');
@ -1121,7 +1117,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
expect(print(store, false, null, false)).toEqual(fallbackSnapshots[i]);
// Re-render with steps[j].
await act(() =>
root.render(
@ -1140,7 +1136,7 @@ describe('StoreStressConcurrent', () => {
),
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(fallbackSnapshots[j]);
expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]);
// Check that we can transition back again.
await act(() =>
root.render(
@ -1158,7 +1154,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
expect(print(store, false, null, false)).toEqual(fallbackSnapshots[i]);
// Clean up after every iteration.
await act(() => root.unmount());
expect(print(store)).toBe('');
@ -1182,7 +1178,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Re-render with steps[j].
await act(() =>
root.render(
@ -1196,7 +1192,7 @@ describe('StoreStressConcurrent', () => {
),
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(fallbackSnapshots[j]);
expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]);
// Check that we can transition back again.
await act(() =>
root.render(
@ -1209,7 +1205,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Clean up after every iteration.
await act(() => root.unmount());
expect(print(store)).toBe('');
@ -1233,7 +1229,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
expect(print(store, false, null, false)).toEqual(fallbackSnapshots[i]);
// Re-render with steps[j].
await act(() =>
root.render(
@ -1247,7 +1243,7 @@ describe('StoreStressConcurrent', () => {
),
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
expect(print(store, false, null, false)).toEqual(snapshots[j]);
// Check that we can transition back again.
await act(() =>
root.render(
@ -1260,7 +1256,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
expect(print(store, false, null, false)).toEqual(fallbackSnapshots[i]);
// Clean up after every iteration.
await act(() => root.unmount());
expect(print(store)).toBe('');
@ -1291,7 +1287,7 @@ describe('StoreStressConcurrent', () => {
const suspenseID = store.getElementIDAtIndex(2);
// Force fallback.
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
await actAsync(async () => {
bridge.send('overrideSuspense', {
id: suspenseID,
@ -1299,7 +1295,7 @@ describe('StoreStressConcurrent', () => {
forceFallback: true,
});
});
expect(print(store)).toEqual(fallbackSnapshots[j]);
expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]);
// Stop forcing fallback.
await actAsync(async () => {
@ -1309,7 +1305,7 @@ describe('StoreStressConcurrent', () => {
forceFallback: false,
});
});
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Trigger actual fallback.
await act(() =>
@ -1323,7 +1319,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(fallbackSnapshots[j]);
expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]);
// Force fallback while we're in fallback mode.
await act(() => {
@ -1334,7 +1330,7 @@ describe('StoreStressConcurrent', () => {
});
});
// Keep seeing fallback content.
expect(print(store)).toEqual(fallbackSnapshots[j]);
expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]);
// Switch to primary mode.
await act(() =>
@ -1349,7 +1345,7 @@ describe('StoreStressConcurrent', () => {
),
);
// Fallback is still forced though.
expect(print(store)).toEqual(fallbackSnapshots[j]);
expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]);
// Stop forcing fallback. This reverts to primary content.
await actAsync(async () => {
@ -1360,7 +1356,7 @@ describe('StoreStressConcurrent', () => {
});
});
// Now we see primary content.
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Clean up after every iteration.
await act(() => root.unmount());

View File

@ -1368,6 +1368,9 @@ describe('TreeListContext', () => {
<Child>
<Suspense>
<Grandchild>
[shell]
<Suspense name="Parent>?" rects={null}>
<Suspense name="Child>?" rects={null}>
`);
const outerSuspenseID = ((store.getElementIDAtIndex(1): any): number);
@ -1407,6 +1410,9 @@ describe('TreeListContext', () => {
<Child>
<Suspense>
<Grandchild>
[shell]
<Suspense name="Parent>?" rects={null}>
<Suspense name="Child>?" rects={null}>
`);
});
});
@ -2361,16 +2367,20 @@ describe('TreeListContext', () => {
jest.runAllTimers();
expect(state).toMatchInlineSnapshot(`
[root]
<Suspense>
`);
[root]
<Suspense>
[shell]
<Suspense name="Unknown" rects={null}>
`);
selectNextErrorOrWarning();
expect(state).toMatchInlineSnapshot(`
[root]
<Suspense>
`);
[root]
<Suspense>
[shell]
<Suspense name="Unknown" rects={null}>
`);
});
it('should properly handle errors/warnings from components that dont mount because of Suspense', async () => {
@ -2392,9 +2402,11 @@ describe('TreeListContext', () => {
utils.act(() => TestRenderer.create(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
<Suspense>
`);
[root]
<Suspense>
[shell]
<Suspense name="Unknown" rects={null}>
`);
await Promise.resolve();
withErrorsOrWarningsIgnored(['test-only:'], () =>
@ -2414,6 +2426,8 @@ describe('TreeListContext', () => {
<Suspense>
<Child>
<Child>
[shell]
<Suspense name="Unknown" rects={null}>
`);
});
@ -2442,6 +2456,8 @@ describe('TreeListContext', () => {
<Suspense>
<Fallback>
<Child>
[shell]
<Suspense name="Unknown" rects={null}>
`);
await Promise.resolve();
@ -2456,10 +2472,12 @@ describe('TreeListContext', () => {
);
expect(state).toMatchInlineSnapshot(`
[root]
<Suspense>
<Child>
`);
[root]
<Suspense>
<Child>
[shell]
<Suspense name="Unknown" rects={null}>
`);
});
});

View File

@ -86,6 +86,7 @@ import {
SUSPENSE_TREE_OPERATION_ADD,
SUSPENSE_TREE_OPERATION_REMOVE,
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
SUSPENSE_TREE_OPERATION_RESIZE,
} from '../../constants';
import {inspectHooksOfFiber} from 'react-debug-tools';
import {
@ -2558,6 +2559,20 @@ export function attach(
pushOperation(fiberID);
pushOperation(parentID);
pushOperation(nameStringID);
const rects = suspenseInstance.rects;
if (rects === null) {
pushOperation(-1);
} else {
pushOperation(rects.length);
for (let i = 0; i < rects.length; ++i) {
const rect = rects[i];
pushOperation(Math.round(rect.x));
pushOperation(Math.round(rect.y));
pushOperation(Math.round(rect.width));
pushOperation(Math.round(rect.height));
}
}
}
function recordUnmount(fiberInstance: FiberInstance): void {
@ -2606,7 +2621,30 @@ export function attach(
}
function recordSuspenseResize(suspenseNode: SuspenseNode): void {
// TODO: Notify the front end of the change.
if (__DEBUG__) {
console.log('recordSuspenseResize()', suspenseNode);
}
const fiberInstance = suspenseNode.instance;
if (fiberInstance.kind !== FIBER_INSTANCE) {
// TODO: Resizes of filtered Suspense nodes are currently dropped.
return;
}
pushOperation(SUSPENSE_TREE_OPERATION_RESIZE);
pushOperation(fiberInstance.id);
const rects = suspenseNode.rects;
if (rects === null) {
pushOperation(-1);
} else {
pushOperation(rects.length);
for (let i = 0; i < rects.length; ++i) {
const rect = rects[i];
pushOperation(Math.round(rect.x));
pushOperation(Math.round(rect.y));
pushOperation(Math.round(rect.width));
pushOperation(Math.round(rect.height));
}
}
}
function recordSuspenseUnmount(suspenseInstance: SuspenseNode): void {
@ -3442,7 +3480,25 @@ export function attach(
// Measure this Suspense node. In general we shouldn't do this until we have
// inserted the new children but since we know this is a FiberInstance we'll
// just use the Fiber anyway.
newSuspenseNode.rects = measureInstance(newInstance);
// Fallbacks get attributed to the parent so we only measure if we're
// showing primary content.
if (OffscreenComponent === -1) {
const isTimedOut = fiber.memoizedState !== null;
if (!isTimedOut) {
newSuspenseNode.rects = measureInstance(newInstance);
}
} else {
const contentFiber = fiber.child;
if (contentFiber === null) {
throw new Error(
'There should always be an Offscreen Fiber child in a Suspense boundary.',
);
}
const isTimedOut = fiber.memoizedState !== null;
if (!isTimedOut) {
newSuspenseNode.rects = measureInstance(newInstance);
}
}
recordSuspenseMount(newSuspenseNode, reconcilingParentSuspenseNode);
}
insertChild(newInstance);
@ -3476,7 +3532,25 @@ export function attach(
// Measure this Suspense node. In general we shouldn't do this until we have
// inserted the new children but since we know this is a FiberInstance we'll
// just use the Fiber anyway.
newSuspenseNode.rects = measureInstance(newInstance);
// Fallbacks get attributed to the parent so we only measure if we're
// showing primary content.
if (OffscreenComponent === -1) {
const isTimedOut = fiber.memoizedState !== null;
if (!isTimedOut) {
newSuspenseNode.rects = measureInstance(newInstance);
}
} else {
const contentFiber = fiber.child;
if (contentFiber === null) {
throw new Error(
'There should always be an Offscreen Fiber child in a Suspense boundary.',
);
}
const isTimedOut = fiber.memoizedState !== null;
if (!isTimedOut) {
newSuspenseNode.rects = measureInstance(newInstance);
}
}
}
insertChild(newInstance);
if (__DEBUG__) {

View File

@ -27,6 +27,7 @@ export const TREE_OPERATION_SET_SUBTREE_MODE = 7;
export const SUSPENSE_TREE_OPERATION_ADD = 8;
export const SUSPENSE_TREE_OPERATION_REMOVE = 9;
export const SUSPENSE_TREE_OPERATION_REORDER_CHILDREN = 10;
export const SUSPENSE_TREE_OPERATION_RESIZE = 11;
export const PROFILING_FLAG_BASIC_SUPPORT = 0b01;
export const PROFILING_FLAG_TIMELINE_SUPPORT = 0b10;

View File

@ -23,6 +23,7 @@ import {
SUSPENSE_TREE_OPERATION_ADD,
SUSPENSE_TREE_OPERATION_REMOVE,
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
SUSPENSE_TREE_OPERATION_RESIZE,
} from '../constants';
import {ElementTypeRoot} from '../frontend/types';
import {
@ -1418,6 +1419,7 @@ export default class Store extends EventEmitter<{
const id = operations[i + 1];
const parentID = operations[i + 2];
const nameStringID = operations[i + 3];
const numRects = ((operations[i + 4]: any): number);
let name = stringTable[nameStringID];
if (this._idToSuspense.has(id)) {
@ -1448,6 +1450,22 @@ export default class Store extends EventEmitter<{
}
}
i += 5;
let rects: SuspenseNode['rects'];
if (numRects === -1) {
rects = null;
} else {
rects = [];
for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {
const x = operations[i + 0];
const y = operations[i + 1];
const width = operations[i + 2];
const height = operations[i + 3];
rects.push({x, y, width, height});
i += 4;
}
}
if (__DEBUG__) {
debug('Suspense Add', `node ${id} as child of ${parentID}`);
}
@ -1476,10 +1494,9 @@ export default class Store extends EventEmitter<{
parentID,
children: [],
name,
rects,
});
i += 4;
hasSuspenseTreeChanged = true;
break;
}
@ -1591,6 +1608,61 @@ export default class Store extends EventEmitter<{
hasSuspenseTreeChanged = true;
break;
}
case SUSPENSE_TREE_OPERATION_RESIZE: {
const id = ((operations[i + 1]: any): number);
const numRects = ((operations[i + 2]: any): number);
i += 3;
const suspense = this._idToSuspense.get(id);
if (suspense === undefined) {
this._throwAndEmitError(
Error(
`Cannot set rects for suspense node "${id}" because no matching node was found in the Store.`,
),
);
break;
}
let nextRects: SuspenseNode['rects'];
if (numRects === -1) {
nextRects = null;
} else {
nextRects = [];
for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {
const x = operations[i + 0];
const y = operations[i + 1];
const width = operations[i + 2];
const height = operations[i + 3];
nextRects.push({x, y, width, height});
i += 4;
}
}
suspense.rects = nextRects;
if (__DEBUG__) {
debug(
'Resize',
`Suspense node ${id} resize to ${
nextRects === null
? 'null'
: nextRects
.map(
rect =>
`(${rect.x},${rect.y},${rect.width},${rect.height})`,
)
.join(',')
}`,
);
}
hasSuspenseTreeChanged = true;
break;
}
default:
this._throwAndEmitError(
new UnsupportedBridgeOperationError(

View File

@ -10,7 +10,10 @@
import JSON5 from 'json5';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
import type {Element} from 'react-devtools-shared/src/frontend/types';
import type {
Element,
SuspenseNode,
} from 'react-devtools-shared/src/frontend/types';
import type {StateContext} from './views/Components/TreeContext';
import type Store from './store';
@ -28,6 +31,11 @@ export function printElement(
key = ` key="${element.key}"`;
}
let name = '';
if (element.nameProp !== null) {
name = ` name="${element.nameProp}"`;
}
let hocDisplayNames = null;
if (element.hocDisplayNames !== null) {
hocDisplayNames = [...element.hocDisplayNames];
@ -43,7 +51,45 @@ export function printElement(
return `${' '.repeat(element.depth + 1)}${prefix} <${
element.displayName || 'null'
}${key}>${hocs}${suffix}`;
}${key}${name}>${hocs}${suffix}`;
}
function printSuspense(
suspense: SuspenseNode,
includeWeight: boolean = false,
): string {
let name = '';
if (suspense.name !== null) {
name = ` name="${suspense.name}"`;
}
let printedRects = '';
const rects = suspense.rects;
if (rects === null) {
printedRects = ' rects={null}';
} else {
printedRects = ` rects={[${rects.map(rect => `{x:${rect.x},y:${rect.y},width:${rect.width},height:${rect.height}}`).join(', ')}]}`;
}
return `<Suspense${name}${printedRects}>`;
}
function printSuspenseWithChildren(
store: Store,
suspense: SuspenseNode,
depth: number,
): Array<string> {
const lines = [' '.repeat(depth) + printSuspense(suspense)];
for (let i = 0; i < suspense.children.length; i++) {
const childID = suspense.children[i];
const child = store.getSuspenseByID(childID);
if (child === null) {
throw new Error(`Could not find Suspense node with ID "${childID}".`);
}
lines.push(...printSuspenseWithChildren(store, child, depth + 1));
}
return lines;
}
export function printOwnersList(
@ -59,6 +105,7 @@ export function printStore(
store: Store,
includeWeight: boolean = false,
state: StateContext | null = null,
includeSuspense: boolean = true,
): string {
const snapshotLines = [];
@ -129,6 +176,26 @@ export function printStore(
}
rootWeight += weight;
if (includeSuspense) {
const shell = store.getSuspenseByID(rootID);
// Roots from legacy renderers don't have a separate Suspense tree
if (shell !== null) {
if (shell.children.length > 0) {
snapshotLines.push('[shell]');
for (let i = 0; i < shell.children.length; i++) {
const childID = shell.children[i];
const child = store.getSuspenseByID(childID);
if (child === null) {
throw new Error(
`Could not find Suspense node with ID "${childID}".`,
);
}
snapshotLines.push(...printSuspenseWithChildren(store, child, 1));
}
}
}
}
});
// Make sure the pretty-printed test align with the Store's reported number of total rows.

View File

@ -19,6 +19,7 @@ import {
SUSPENSE_TREE_OPERATION_ADD,
SUSPENSE_TREE_OPERATION_REMOVE,
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
SUSPENSE_TREE_OPERATION_RESIZE,
} from 'react-devtools-shared/src/constants';
import {
parseElementDisplayNameFromBackend,
@ -376,16 +377,26 @@ function updateTree(
const fiberID = operations[i + 1];
const parentID = operations[i + 2];
const nameStringID = operations[i + 3];
const numRects = operations[i + 4];
const name = stringTable[nameStringID];
i += 4;
if (__DEBUG__) {
let rects: string;
if (numRects === -1) {
rects = 'null';
} else {
rects =
'[' +
operations.slice(i + 5, i + 5 + numRects * 4).join(',') +
']';
}
debug(
'Add suspense',
`node ${fiberID} (${String(name)}) under ${parentID}`,
`node ${fiberID} (name=${JSON.stringify(name)}, rects={${rects}}) under ${parentID}`,
);
}
i += 5 + (numRects === -1 ? 0 : numRects * 4);
break;
}
@ -416,6 +427,30 @@ function updateTree(
break;
}
case SUSPENSE_TREE_OPERATION_RESIZE: {
const suspenseID = ((operations[i + 1]: any): number);
const numRects = ((operations[i + 2]: any): number);
if (__DEBUG__) {
if (numRects === -1) {
debug('Suspense resize', `suspense ${suspenseID} rects null`);
} else {
const rects = ((operations.slice(
i + 3,
i + 3 + numRects * 4,
): any): Array<number>);
debug(
'Suspense resize',
`suspense ${suspenseID} rects [${rects.join(',')}]`,
);
}
}
i += 3 + (numRects === -1 ? 0 : numRects * 4);
break;
}
default:
throw Error(`Unsupported Bridge operation "${operation}"`);
}

View File

@ -185,11 +185,19 @@ export type Element = {
compiledWithForget: boolean,
};
export type Rect = {
x: number,
y: number,
width: number,
height: number,
};
export type SuspenseNode = {
id: Element['id'],
parentID: SuspenseNode['id'] | 0,
children: Array<SuspenseNode['id']>,
name: string | null,
rects: null | Array<Rect>,
};
// Serialized version of ReactIOInfo

View File

@ -43,6 +43,7 @@ import {
SUSPENSE_TREE_OPERATION_ADD,
SUSPENSE_TREE_OPERATION_REMOVE,
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
SUSPENSE_TREE_OPERATION_RESIZE,
} from './constants';
import {
ComponentFilterElementType,
@ -339,11 +340,34 @@ export function printOperationsArray(operations: Array<number>) {
const parentID = operations[i + 2];
const nameStringID = operations[i + 3];
const name = stringTable[nameStringID];
const numRects = operations[i + 4];
i += 4;
i += 5;
let rects: string;
if (numRects === -1) {
rects = 'null';
} else {
rects = '[';
for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {
const offset = i + rectIndex * 4;
const x = operations[offset + 0];
const y = operations[offset + 1];
const width = operations[offset + 2];
const height = operations[offset + 3];
if (rectIndex > 0) {
rects += ', ';
}
rects += `(${x}, ${y}, ${width}, ${height})`;
i += 4;
}
rects += ']';
}
logs.push(
`Add suspense node ${fiberID} (${String(name)}) under ${parentID}`,
`Add suspense node ${fiberID} (${String(name)},rects={${rects}}) under ${parentID}`,
);
break;
}
@ -372,6 +396,33 @@ export function printOperationsArray(operations: Array<number>) {
);
break;
}
case SUSPENSE_TREE_OPERATION_RESIZE: {
const id = ((operations[i + 1]: any): number);
const numRects = ((operations[i + 2]: any): number);
i += 3;
if (numRects === -1) {
logs.push(`Resize suspense node ${id} to null`);
} else {
let line = `Resize suspense node ${id} to [`;
for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {
const x = operations[i + 0];
const y = operations[i + 1];
const width = operations[i + 2];
const height = operations[i + 3];
if (rectIndex > 0) {
line += ', ';
}
line += `(${x}, ${y}, ${width}, ${height})`;
i += 4;
}
logs.push(line + ']');
}
break;
}
default:
throw Error(`Unsupported Bridge operation "${operation}"`);
}