From 818dfabe7e4964419fb1e3ad6da40755880b88d0 Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Sun, 16 Aug 2026 23:01:53 +0200 Subject: [PATCH 1/3] test(react-list): lock down ListItem behavior contract --- .../components/ListItem/useListItem.test.tsx | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 packages/react-components/react-list/library/src/components/ListItem/useListItem.test.tsx diff --git a/packages/react-components/react-list/library/src/components/ListItem/useListItem.test.tsx b/packages/react-components/react-list/library/src/components/ListItem/useListItem.test.tsx new file mode 100644 index 0000000000000..9c5e6408b93cf --- /dev/null +++ b/packages/react-components/react-list/library/src/components/ListItem/useListItem.test.tsx @@ -0,0 +1,106 @@ +import * as React from 'react'; +import { fireEvent, render } from '@testing-library/react'; +import { List } from '../List/List'; +import { ListItem } from './ListItem'; + +/** + * Behavior that must survive the split between `useListItem_unstable` and the design free + * `useListItemBase_unstable`: Tabster wiring, the Fluent `Checkbox` checkmark, and the exact + * ordering between the consumer `onKeyDown` handler and the built in key handling. + */ +describe('ListItem behavior contract', () => { + const consoleWarn = jest.spyOn(console, 'warn').mockImplementation(() => jest.fn()); + + afterAll(() => { + consoleWarn.mockRestore(); + }); + + it('applies Tabster attributes to the list and to focusable list items', () => { + const { getByRole, getAllByRole } = render( + + First + Second + , + ); + + expect(getByRole('list')).toHaveAttribute('data-tabster'); + for (const item of getAllByRole('listitem')) { + expect(item).toHaveAttribute('data-tabster'); + } + }); + + it('does not apply arrow navigation to non focusable list items', () => { + const { getAllByRole } = render( + + First + , + ); + + expect(getAllByRole('listitem')[0].getAttribute('data-tabster')).not.toContain('mover'); + }); + + it('renders the checkmark as a Fluent Checkbox', () => { + const { getAllByRole } = render( + + First + , + ); + + const checkbox = getAllByRole('checkbox')[0]; + + expect(checkbox.tagName).toBe('INPUT'); + expect(checkbox).toHaveAttribute('tabindex', '-1'); + // The Fluent Checkbox wraps its input in a labelled root, unlike a bare native checkbox. + expect(checkbox.closest('.fui-Checkbox')).not.toBeNull(); + }); + + it('lets the consumer onKeyDown handler opt out of the built in Space handling', () => { + const onSelectionChange = jest.fn(); + + const { getByText } = render( + + e.preventDefault()}> + First + + , + ); + + fireEvent.keyDown(getByText('First'), { key: ' ' }); + + expect(onSelectionChange).not.toHaveBeenCalled(); + }); + + it('forwards the checkmark ref so clicking the checkbox does not trigger the item action', () => { + const onAction = jest.fn(); + + const { getAllByRole } = render( + + + First + + , + ); + + fireEvent.click(getAllByRole('checkbox')[0]); + + expect(onAction).not.toHaveBeenCalled(); + }); + + it('keeps the item non actionable for selection when disabledSelection is set', () => { + const onSelectionChange = jest.fn(); + + const { getByText } = render( + + + First + + , + ); + + const item = getByText('First'); + expect(item).toHaveAttribute('aria-disabled', 'true'); + + fireEvent.keyDown(item, { key: ' ' }); + expect(onSelectionChange).not.toHaveBeenCalled(); + }); +}); From 597fa6ab146dd359ebe8572345fb08c5228f60ca Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Tue, 18 Aug 2026 15:40:47 +0200 Subject: [PATCH 2/3] test(react-list): use native DOM attribute assertions Keep the ListItem behavior contract tests compatible with the package's TypeScript matcher environment. --- .../library/src/components/ListItem/useListItem.test.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-components/react-list/library/src/components/ListItem/useListItem.test.tsx b/packages/react-components/react-list/library/src/components/ListItem/useListItem.test.tsx index 9c5e6408b93cf..0021929823502 100644 --- a/packages/react-components/react-list/library/src/components/ListItem/useListItem.test.tsx +++ b/packages/react-components/react-list/library/src/components/ListItem/useListItem.test.tsx @@ -23,9 +23,9 @@ describe('ListItem behavior contract', () => { , ); - expect(getByRole('list')).toHaveAttribute('data-tabster'); + expect(getByRole('list').hasAttribute('data-tabster')).toBe(true); for (const item of getAllByRole('listitem')) { - expect(item).toHaveAttribute('data-tabster'); + expect(item.hasAttribute('data-tabster')).toBe(true); } }); @@ -49,7 +49,7 @@ describe('ListItem behavior contract', () => { const checkbox = getAllByRole('checkbox')[0]; expect(checkbox.tagName).toBe('INPUT'); - expect(checkbox).toHaveAttribute('tabindex', '-1'); + expect(checkbox.getAttribute('tabindex')).toBe('-1'); // The Fluent Checkbox wraps its input in a labelled root, unlike a bare native checkbox. expect(checkbox.closest('.fui-Checkbox')).not.toBeNull(); }); @@ -98,7 +98,7 @@ describe('ListItem behavior contract', () => { ); const item = getByText('First'); - expect(item).toHaveAttribute('aria-disabled', 'true'); + expect(item.getAttribute('aria-disabled')).toBe('true'); fireEvent.keyDown(item, { key: ' ' }); expect(onSelectionChange).not.toHaveBeenCalled(); From fdc0b1ae474c5a8d8eb1195e07fd32e8b1c9a749 Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Tue, 18 Aug 2026 22:13:46 +0200 Subject: [PATCH 3/3] test(react-list): cover List hooks --- .../src/components/List/useList.test.tsx | 14 +++ .../components/ListItem/useListItem.test.tsx | 114 ++++-------------- 2 files changed, 35 insertions(+), 93 deletions(-) create mode 100644 packages/react-components/react-list/library/src/components/List/useList.test.tsx diff --git a/packages/react-components/react-list/library/src/components/List/useList.test.tsx b/packages/react-components/react-list/library/src/components/List/useList.test.tsx new file mode 100644 index 0000000000000..e5d6acab497ec --- /dev/null +++ b/packages/react-components/react-list/library/src/components/List/useList.test.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { renderHook } from '@testing-library/react-hooks'; +import type { TabsterDOMAttribute } from '@fluentui/react-tabster'; +import { useList_unstable } from './useList'; + +describe('useList_unstable', () => { + it('applies Tabster navigation to the list', () => { + const { result } = renderHook(() => + useList_unstable({ navigationMode: 'items' }, React.createRef()), + ); + + expect((result.current.root as Partial)['data-tabster']).toContain('mover'); + }); +}); diff --git a/packages/react-components/react-list/library/src/components/ListItem/useListItem.test.tsx b/packages/react-components/react-list/library/src/components/ListItem/useListItem.test.tsx index 0021929823502..95a1affc60df3 100644 --- a/packages/react-components/react-list/library/src/components/ListItem/useListItem.test.tsx +++ b/packages/react-components/react-list/library/src/components/ListItem/useListItem.test.tsx @@ -1,106 +1,34 @@ import * as React from 'react'; -import { fireEvent, render } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import { Checkbox } from '@fluentui/react-checkbox'; +import type { TabsterDOMAttribute } from '@fluentui/react-tabster'; import { List } from '../List/List'; -import { ListItem } from './ListItem'; +import { useListItem_unstable } from './useListItem'; -/** - * Behavior that must survive the split between `useListItem_unstable` and the design free - * `useListItemBase_unstable`: Tabster wiring, the Fluent `Checkbox` checkmark, and the exact - * ordering between the consumer `onKeyDown` handler and the built in key handling. - */ -describe('ListItem behavior contract', () => { - const consoleWarn = jest.spyOn(console, 'warn').mockImplementation(() => jest.fn()); +describe('useListItem_unstable', () => { + it('applies Tabster navigation to focusable list items', () => { + const { result } = renderHook(() => useListItem_unstable({}, React.createRef()), { + wrapper: ({ children }: React.PropsWithChildren) => {children}, + }); - afterAll(() => { - consoleWarn.mockRestore(); - }); - - it('applies Tabster attributes to the list and to focusable list items', () => { - const { getByRole, getAllByRole } = render( - - First - Second - , - ); - - expect(getByRole('list').hasAttribute('data-tabster')).toBe(true); - for (const item of getAllByRole('listitem')) { - expect(item.hasAttribute('data-tabster')).toBe(true); - } + expect((result.current.root as Partial)['data-tabster']).toContain('mover'); }); it('does not apply arrow navigation to non focusable list items', () => { - const { getAllByRole } = render( - - First - , - ); - - expect(getAllByRole('listitem')[0].getAttribute('data-tabster')).not.toContain('mover'); - }); + const { result } = renderHook(() => useListItem_unstable({}, React.createRef()), { + wrapper: ({ children }: React.PropsWithChildren) => {children}, + }); - it('renders the checkmark as a Fluent Checkbox', () => { - const { getAllByRole } = render( - - First - , - ); - - const checkbox = getAllByRole('checkbox')[0]; - - expect(checkbox.tagName).toBe('INPUT'); - expect(checkbox.getAttribute('tabindex')).toBe('-1'); - // The Fluent Checkbox wraps its input in a labelled root, unlike a bare native checkbox. - expect(checkbox.closest('.fui-Checkbox')).not.toBeNull(); + expect((result.current.root as Partial)['data-tabster']).not.toContain('mover'); }); - it('lets the consumer onKeyDown handler opt out of the built in Space handling', () => { - const onSelectionChange = jest.fn(); - - const { getByText } = render( - - e.preventDefault()}> - First - - , - ); - - fireEvent.keyDown(getByText('First'), { key: ' ' }); - - expect(onSelectionChange).not.toHaveBeenCalled(); - }); - - it('forwards the checkmark ref so clicking the checkbox does not trigger the item action', () => { - const onAction = jest.fn(); - - const { getAllByRole } = render( - - - First - - , - ); - - fireEvent.click(getAllByRole('checkbox')[0]); - - expect(onAction).not.toHaveBeenCalled(); - }); - - it('keeps the item non actionable for selection when disabledSelection is set', () => { - const onSelectionChange = jest.fn(); - - const { getByText } = render( - - - First - - , - ); - - const item = getByText('First'); - expect(item.getAttribute('aria-disabled')).toBe('true'); + it('uses a Fluent Checkbox for the checkmark', () => { + const { result } = renderHook(() => useListItem_unstable({}, React.createRef()), { + wrapper: ({ children }: React.PropsWithChildren) => {children}, + }); - fireEvent.keyDown(item, { key: ' ' }); - expect(onSelectionChange).not.toHaveBeenCalled(); + // eslint-disable-next-line @typescript-eslint/no-deprecated + expect(result.current.components.checkmark).toBe(Checkbox); + expect(result.current.checkmark?.tabIndex).toBe(-1); }); });