Skip to content

Commit ed69815

Browse files
authored
[DevTools] feat: display subtree for Activity and dim in hidden mode (facebook#36094)
With this change, Components panel will display subtree of the Activity. When it is in hidden mode, the subtree will be dimmed. Added Jest tests and a sandbox case to `react-devtools-shell`. Demo: https://github.com/user-attachments/assets/69a2e8d6-585d-4fcd-b57e-e9ae06d0a1b3
1 parent 8b2e903 commit ed69815

File tree

9 files changed

+499
-30
lines changed

9 files changed

+499
-30
lines changed

packages/react-devtools-shared/src/__tests__/store-test.js

Lines changed: 271 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,269 @@ describe('Store', () => {
297297
});
298298
});
299299

300+
describe('Activity hidden state', () => {
301+
// @reactVersion >= 19
302+
it('should mark Activity subtree elements as hidden when mode is hidden', async () => {
303+
const Activity = React.Activity || React.unstable_Activity;
304+
305+
function Child() {
306+
return <div>child</div>;
307+
}
308+
309+
function App({hidden}) {
310+
return (
311+
<Activity mode={hidden ? 'hidden' : 'visible'}>
312+
<Child />
313+
</Activity>
314+
);
315+
}
316+
317+
await actAsync(() => {
318+
render(<App hidden={true} />);
319+
});
320+
321+
// Activity element should be marked as hidden and collapsed
322+
const activityElement = store.getElementAtIndex(1);
323+
expect(activityElement.displayName).toBe('Activity');
324+
expect(activityElement.isActivityHidden).toBe(true);
325+
expect(activityElement.isInsideHiddenActivity).toBe(false);
326+
expect(activityElement.isCollapsed).toBe(true);
327+
328+
// Expand to access children
329+
store.toggleIsCollapsed(activityElement.id, false);
330+
331+
// Children should still be in the tree but marked as inside hidden Activity
332+
const childElement = store.getElementAtIndex(2);
333+
expect(childElement.displayName).toBe('Child');
334+
expect(childElement.isInsideHiddenActivity).toBe(true);
335+
});
336+
337+
// @reactVersion >= 19
338+
it('should not mark Activity subtree as hidden when mode is visible', async () => {
339+
const Activity = React.Activity || React.unstable_Activity;
340+
341+
function Child() {
342+
return <div>child</div>;
343+
}
344+
345+
function App() {
346+
return (
347+
<Activity mode="visible">
348+
<Child />
349+
</Activity>
350+
);
351+
}
352+
353+
await actAsync(() => {
354+
render(<App />);
355+
});
356+
357+
const activityElement = store.getElementAtIndex(1);
358+
expect(activityElement.displayName).toBe('Activity');
359+
expect(activityElement.isActivityHidden).toBe(false);
360+
expect(activityElement.isInsideHiddenActivity).toBe(false);
361+
expect(activityElement.isCollapsed).toBe(false);
362+
363+
const childElement = store.getElementAtIndex(2);
364+
expect(childElement.displayName).toBe('Child');
365+
expect(childElement.isInsideHiddenActivity).toBe(false);
366+
});
367+
368+
// @reactVersion >= 19
369+
it('should update hidden state when Activity mode toggles', async () => {
370+
const Activity = React.Activity || React.unstable_Activity;
371+
372+
function Child() {
373+
return <div>child</div>;
374+
}
375+
376+
function App({hidden}) {
377+
return (
378+
<Activity mode={hidden ? 'hidden' : 'visible'}>
379+
<Child />
380+
</Activity>
381+
);
382+
}
383+
384+
// Start visible
385+
await actAsync(() => {
386+
render(<App hidden={false} />);
387+
});
388+
389+
let activityElement = store.getElementAtIndex(1);
390+
expect(activityElement.isActivityHidden).toBe(false);
391+
expect(activityElement.isCollapsed).toBe(false);
392+
393+
let childElement = store.getElementAtIndex(2);
394+
expect(childElement.isInsideHiddenActivity).toBe(false);
395+
396+
// Toggle to hidden — children remain but subtree collapses
397+
await actAsync(() => {
398+
render(<App hidden={true} />);
399+
});
400+
401+
activityElement = store.getElementAtIndex(1);
402+
expect(activityElement.isActivityHidden).toBe(true);
403+
expect(activityElement.isCollapsed).toBe(true);
404+
405+
// Expand to verify children are still marked
406+
store.toggleIsCollapsed(activityElement.id, false);
407+
408+
childElement = store.getElementAtIndex(2);
409+
expect(childElement.displayName).toBe('Child');
410+
expect(childElement.isInsideHiddenActivity).toBe(true);
411+
412+
// Toggle back to visible — subtree expands automatically
413+
await actAsync(() => {
414+
render(<App hidden={false} />);
415+
});
416+
417+
activityElement = store.getElementAtIndex(1);
418+
expect(activityElement.isActivityHidden).toBe(false);
419+
expect(activityElement.isCollapsed).toBe(false);
420+
421+
childElement = store.getElementAtIndex(2);
422+
expect(childElement.isInsideHiddenActivity).toBe(false);
423+
});
424+
425+
// @reactVersion >= 19
426+
it('should propagate hidden state to deeply nested children', async () => {
427+
const Activity = React.Activity || React.unstable_Activity;
428+
429+
function GrandChild() {
430+
return <div>grandchild</div>;
431+
}
432+
function Child() {
433+
return <GrandChild />;
434+
}
435+
436+
function App({hidden}) {
437+
return (
438+
<Activity mode={hidden ? 'hidden' : 'visible'}>
439+
<Child />
440+
</Activity>
441+
);
442+
}
443+
444+
await actAsync(() => {
445+
render(<App hidden={true} />);
446+
});
447+
448+
const activityElement = store.getElementAtIndex(1);
449+
expect(activityElement.displayName).toBe('Activity');
450+
expect(activityElement.isActivityHidden).toBe(true);
451+
expect(activityElement.isCollapsed).toBe(true);
452+
453+
// Expand to access children
454+
store.toggleIsCollapsed(activityElement.id, false);
455+
456+
const childElement = store.getElementAtIndex(2);
457+
expect(childElement.displayName).toBe('Child');
458+
expect(childElement.isInsideHiddenActivity).toBe(true);
459+
460+
const grandChildElement = store.getElementAtIndex(3);
461+
expect(grandChildElement.displayName).toBe('GrandChild');
462+
expect(grandChildElement.isInsideHiddenActivity).toBe(true);
463+
});
464+
465+
// @reactVersion >= 19
466+
it('should collapse hidden Activity subtree by default', async () => {
467+
const Activity = React.Activity || React.unstable_Activity;
468+
469+
function Child() {
470+
return <div>child</div>;
471+
}
472+
473+
function App({hidden}) {
474+
return (
475+
<Activity mode={hidden ? 'hidden' : 'visible'}>
476+
<Child />
477+
</Activity>
478+
);
479+
}
480+
481+
// Hidden Activity should be collapsed
482+
await actAsync(() => {
483+
render(<App hidden={true} />);
484+
});
485+
486+
expect(store).toMatchInlineSnapshot(`
487+
[root]
488+
▾ <App>
489+
▸ <Activity mode="hidden">
490+
`);
491+
492+
// Toggle to visible — should expand
493+
await actAsync(() => {
494+
render(<App hidden={false} />);
495+
});
496+
497+
expect(store).toMatchInlineSnapshot(`
498+
[root]
499+
▾ <App>
500+
▾ <Activity mode="visible">
501+
<Child>
502+
`);
503+
504+
// Toggle back to hidden — should collapse again
505+
await actAsync(() => {
506+
render(<App hidden={true} />);
507+
});
508+
509+
expect(store).toMatchInlineSnapshot(`
510+
[root]
511+
▾ <App>
512+
▸ <Activity mode="hidden">
513+
`);
514+
});
515+
516+
// @reactVersion >= 19
517+
it('should dim nested visible Activity inside a hidden Activity', async () => {
518+
const Activity = React.Activity || React.unstable_Activity;
519+
520+
function Leaf() {
521+
return <div>leaf</div>;
522+
}
523+
524+
function App() {
525+
return (
526+
<Activity mode="hidden" name="outer">
527+
<Activity mode="visible" name="inner">
528+
<Leaf />
529+
</Activity>
530+
</Activity>
531+
);
532+
}
533+
534+
await actAsync(() => {
535+
render(<App />);
536+
});
537+
538+
// Outer Activity: hidden, collapsed, not dimmed itself
539+
const outerActivity = store.getElementAtIndex(1);
540+
expect(outerActivity.displayName).toBe('Activity');
541+
expect(outerActivity.nameProp).toBe('outer');
542+
expect(outerActivity.isActivityHidden).toBe(true);
543+
expect(outerActivity.isInsideHiddenActivity).toBe(false);
544+
expect(outerActivity.isCollapsed).toBe(true);
545+
546+
// Expand to access inner elements
547+
store.toggleIsCollapsed(outerActivity.id, false);
548+
549+
// Inner Activity: visible, but inside hidden outer so still dimmed
550+
const innerActivity = store.getElementAtIndex(2);
551+
expect(innerActivity.displayName).toBe('Activity');
552+
expect(innerActivity.nameProp).toBe('inner');
553+
expect(innerActivity.isActivityHidden).toBe(false);
554+
expect(innerActivity.isInsideHiddenActivity).toBe(true);
555+
556+
// Leaf: inside both, dimmed
557+
const leaf = store.getElementAtIndex(3);
558+
expect(leaf.displayName).toBe('Leaf');
559+
expect(leaf.isInsideHiddenActivity).toBe(true);
560+
});
561+
});
562+
300563
describe('collapseNodesByDefault:false', () => {
301564
beforeEach(() => {
302565
store.collapseNodesByDefault = false;
@@ -3361,9 +3624,10 @@ describe('Store', () => {
33613624
expect(store).toMatchInlineSnapshot(`
33623625
[root]
33633626
▾ <App>
3364-
<Activity>
3627+
<Activity mode="hidden">
33653628
<Suspense name="outer-suspense">
33663629
[suspense-root] rects={[{x:1,y:2,width:15,height:1}]}
3630+
<Suspense name="inside-activity" uniqueSuspenders={false} rects={[{x:1,y:2,width:15,height:1}]}>
33673631
<Suspense name="outer-suspense" uniqueSuspenders={true} rects={null}>
33683632
`);
33693633

@@ -3378,7 +3642,7 @@ describe('Store', () => {
33783642
expect(store).toMatchInlineSnapshot(`
33793643
[root]
33803644
▾ <App>
3381-
▾ <Activity>
3645+
▾ <Activity mode="visible">
33823646
▾ <Suspense name="inside-activity">
33833647
<Component key="inside-activity">
33843648
▾ <Suspense name="outer-suspense">
@@ -3397,9 +3661,10 @@ describe('Store', () => {
33973661
expect(store).toMatchInlineSnapshot(`
33983662
[root]
33993663
▾ <App>
3400-
<Activity>
3664+
<Activity mode="hidden">
34013665
<Suspense name="outer-suspense">
34023666
[suspense-root] rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:15,height:1}]}
3667+
<Suspense name="inside-activity" uniqueSuspenders={false} rects={[{x:1,y:2,width:15,height:1}]}>
34033668
<Suspense name="outer-suspense" uniqueSuspenders={true} rects={[{x:1,y:2,width:15,height:1}]}>
34043669
<Suspense name="inner-suspense" uniqueSuspenders={false} rects={[{x:1,y:2,width:15,height:1}]}>
34053670
`);
@@ -3411,7 +3676,7 @@ describe('Store', () => {
34113676
expect(store).toMatchInlineSnapshot(`
34123677
[root]
34133678
▾ <App>
3414-
▾ <Activity>
3679+
▾ <Activity mode="visible">
34153680
▾ <Suspense name="inside-activity">
34163681
<Component key="inside-activity">
34173682
▾ <Suspense name="outer-suspense">
@@ -3604,7 +3869,7 @@ describe('Store', () => {
36043869

36053870
expect(store).toMatchInlineSnapshot(`
36063871
[root]
3607-
<Activity>
3872+
<Activity mode="hidden">
36083873
`);
36093874

36103875
await actAsync(() => {
@@ -3613,7 +3878,7 @@ describe('Store', () => {
36133878

36143879
expect(store).toMatchInlineSnapshot(`
36153880
[root]
3616-
▾ <Activity>
3881+
▾ <Activity mode="visible">
36173882
▾ <Component key="left">
36183883
<div>
36193884
`);

0 commit comments

Comments
 (0)