From 6f4758c62f7d462f1041494d69ad7d32c004df9b Mon Sep 17 00:00:00 2001 From: Srividya Ramaswamy Date: Mon, 26 Oct 2026 18:24:21 -0700 Subject: [PATCH] test(tablist): cover dynamic tab recovery and shortcut ownership Exercise TBL-001, TBL-004, TBL-005 and TBL-006 across owner/removal/disable recovery, empty eligibility and finite modifier/orientation matrices. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b66f3ef-85ac-4405-bd35-dcc167a077b1 --- .changeset/sparkly-clocks-train.md | 5 + .../src/components/tablist/tablist.test.tsx | 149 ++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 .changeset/sparkly-clocks-train.md diff --git a/.changeset/sparkly-clocks-train.md b/.changeset/sparkly-clocks-train.md new file mode 100644 index 0000000000..064435dc68 --- /dev/null +++ b/.changeset/sparkly-clocks-train.md @@ -0,0 +1,5 @@ +--- + +--- + + diff --git a/packages/agentic/components/src/components/tablist/tablist.test.tsx b/packages/agentic/components/src/components/tablist/tablist.test.tsx index 8b4f9d3ded..63722619e9 100644 --- a/packages/agentic/components/src/components/tablist/tablist.test.tsx +++ b/packages/agentic/components/src/components/tablist/tablist.test.tsx @@ -37,6 +37,155 @@ function selected(tab: ReturnType[number]) { } describe('TabList', () => { + it.each( + (['controlled', 'uncontrolled'] as const).flatMap((owner) => (['remove', 'disable'] as const).map((change) => ({ owner, change }))), + )('TBL-001 TBL-004 recovers after $change of the active Tab under $owner selection', async ({ owner, change }) => { + const ref = React.createRef>(); + const onSelectionChange = jest.fn(); + let context: TabListContextValue | undefined; + function Probe() { + context = React.useContext(TabListContext); + return null; + } + const scene = (invalid: boolean) => ( + + + {!(invalid && change === 'remove') && ( + + )} + + + + ); + const component = await render(scene(false)); + expect(getTabs(component).map((tab) => tab.props.focusable)).toEqual([false, true, false]); + await fireEvent(component.getByRole('tab', { name: 'Activity' }), 'focus', {}); + expect(context?.focusedValue).toBe('activity'); + const focus = jest.spyOn(ref.current!, 'focus').mockImplementation(() => { + const overview = component.getByRole('tab', { name: 'Overview' }); + expect(overview.props.focusable).toBe(true); + expect(selected(overview)).toBe(owner === 'uncontrolled'); + }); + try { + await component.rerender(scene(true)); + + expect(getTabs(component).map((tab) => tab.props.focusable)).toEqual(change === 'remove' ? [true, false] : [true, false, false]); + expect(selected(component.getByRole('tab', { name: 'Overview' }))).toBe(owner === 'uncontrolled'); + if (change === 'remove') { + expect(component.queryByRole('tab', { name: 'Activity' })).toBeNull(); + expect(getTabs(component).map(selected)).toEqual([owner === 'uncontrolled', false]); + } else { + const activity = component.getByRole('tab', { name: 'Activity' }); + expect(activity).toBeDisabled(); + expect(selected(activity)).toBe(owner === 'controlled'); + } + expect(onSelectionChange.mock.calls).toEqual(owner === 'controlled' ? [] : [['overview']]); + expect(focus).toHaveBeenCalledTimes(1); + expect(context?.focusedValue).toBeUndefined(); + + await fireEvent(component.getByRole('tab', { name: 'Overview' }), 'focus', {}); + expect(context?.focusedValue).toBe('overview'); + await component.rerender(scene(true)); + expect(focus).toHaveBeenCalledTimes(1); + expect(onSelectionChange.mock.calls).toEqual(owner === 'controlled' ? [] : [['overview']]); + } finally { + focus.mockRestore(); + } + }); + + it.each(['controlled', 'uncontrolled'] as const)( + 'TBL-004 exposes no tab stop while all Tabs are disabled and recovers eligibility for %s selection', + async (owner) => { + const onSelectionChange = jest.fn(); + const scene = (stage: 'initial' | 'disabled' | 'files') => ( + + + + + + ); + const component = await render(scene('initial')); + expect(getTabs(component).map((tab) => tab.props.focusable)).toEqual([false, true, false]); + + await component.rerender(scene('disabled')); + expect(getTabs(component).map((tab) => tab.props.focusable)).toEqual([false, false, false]); + expect(getTabs(component).every((tab) => tab.props.accessibilityState.disabled)).toBe(true); + expect(onSelectionChange).not.toHaveBeenCalled(); + + await component.rerender(scene('files')); + expect(getTabs(component).map((tab) => tab.props.focusable)).toEqual([false, false, true]); + expect(getTabs(component).map(selected)).toEqual(owner === 'controlled' ? [false, true, false] : [false, false, true]); + expect(onSelectionChange.mock.calls).toEqual(owner === 'controlled' ? [] : [['files']]); + }, + ); + + it.each( + (['horizontal', 'vertical'] as const).flatMap((orientation) => + (['altKey', 'ctrlKey', 'metaKey', 'shiftKey'] as const).flatMap((modifier) => + [ + orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp', + orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown', + 'Home', + 'End', + ].map((key) => ({ orientation, modifier, key })), + ), + ), + )('TBL-005 TBL-006 forwards $modifier+$key once in $orientation lists without consuming it', async ({ orientation, modifier, key }) => { + const onKeyDown = jest.fn(); + const onSelectionChange = jest.fn(); + const component = await render( + + + + , + ); + const event = { nativeEvent: { key, [modifier]: true }, preventDefault: jest.fn(), stopPropagation: jest.fn() }; + + await fireEvent(getTabs(component)[0], 'keyDown', event); + + expect(onKeyDown.mock.calls).toEqual([[event]]); + expect(event.preventDefault).not.toHaveBeenCalled(); + expect(event.stopPropagation).not.toHaveBeenCalled(); + expect(onSelectionChange).not.toHaveBeenCalled(); + expect(getTabs(component).map(selected)).toEqual([true, false]); + expect(getTabs(component).map((tab) => tab.props.focusable)).toEqual([true, false]); + }); + + it.each([ + { orientation: 'horizontal', key: 'ArrowUp' }, + { orientation: 'horizontal', key: 'ArrowDown' }, + { orientation: 'vertical', key: 'ArrowLeft' }, + { orientation: 'vertical', key: 'ArrowRight' }, + ] as const)('TBL-005 forwards the inactive-axis $key in $orientation lists', async ({ orientation, key }) => { + const onKeyDown = jest.fn(); + const onSelectionChange = jest.fn(); + const component = await render( + + + + , + ); + const event = { nativeEvent: { key }, preventDefault: jest.fn(), stopPropagation: jest.fn() }; + + await fireEvent(getTabs(component)[0], 'keyDown', event); + + expect(onKeyDown.mock.calls).toEqual([[event]]); + expect(event.preventDefault).not.toHaveBeenCalled(); + expect(event.stopPropagation).not.toHaveBeenCalled(); + expect(onSelectionChange).not.toHaveBeenCalled(); + expect(getTabs(component).map(selected)).toEqual([true, false]); + expect(getTabs(component).map((tab) => tab.props.focusable)).toEqual([true, false]); + }); + it('defaults to the first enabled Tab and exposes the list semantics', async () => { const component = await renderTabList(); const root = getRoot(component);