From 183caad69dc0197604c89c578b7a73ded0133055 Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Tue, 28 Nov 2023 23:49:58 +0000 Subject: [PATCH 01/12] Backporting `useCompositeState` for Ariakit --- .../components/src/composite/test/index.tsx | 444 ++++++++++++++++++ packages/components/src/composite/types.ts | 345 ++++++++++++++ 2 files changed, 789 insertions(+) create mode 100644 packages/components/src/composite/test/index.tsx create mode 100644 packages/components/src/composite/types.ts diff --git a/packages/components/src/composite/test/index.tsx b/packages/components/src/composite/test/index.tsx new file mode 100644 index 00000000000000..eaeffda502fd74 --- /dev/null +++ b/packages/components/src/composite/test/index.tsx @@ -0,0 +1,444 @@ +// eslint-disable-next-line eslint-comments/disable-enable-pair +/* eslint-disable testing-library/render-result-naming-convention */ + +/** + * External dependencies + */ +import { act, renderHook } from '@testing-library/react'; + +/** + * WordPress dependencies + */ +import { createRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { useCompositeState as reakit } from '..'; +import type { CompositeInitialState, CompositeStateReturn } from '../types'; + +type InitialState = Partial< CompositeInitialState >; + +const DEFAULT_BASE_ID = 'base'; + +const DEFAULT_INITIAL_STATE: CompositeInitialState = { + baseId: DEFAULT_BASE_ID, + currentId: undefined, + loop: false, + orientation: undefined, + rtl: false, + shift: false, + unstable_virtual: false, + wrap: false, +}; + +const DEFAULT_PROPS = { + ...DEFAULT_INITIAL_STATE, + groups: [], + items: [], + unstable_hasActiveWidget: false, + unstable_idCountRef: { current: 0 }, + unstable_includesBaseElement: false, + unstable_moves: 0, +}; + +const DEFAULT_ACTIONS = { + down: expect.any( Function ), + first: expect.any( Function ), + last: expect.any( Function ), + move: expect.any( Function ), + next: expect.any( Function ), + previous: expect.any( Function ), + registerGroup: expect.any( Function ), + registerItem: expect.any( Function ), + reset: expect.any( Function ), + setBaseId: expect.any( Function ), + setCurrentId: expect.any( Function ), + setLoop: expect.any( Function ), + setOrientation: expect.any( Function ), + setRTL: expect.any( Function ), + setShift: expect.any( Function ), + setWrap: expect.any( Function ), + sort: expect.any( Function ), + unregisterGroup: expect.any( Function ), + unregisterItem: expect.any( Function ), + unstable_setHasActiveWidget: expect.any( Function ), + unstable_setIncludesBaseElement: expect.any( Function ), + unstable_setVirtual: expect.any( Function ), + up: expect.any( Function ), +}; + +const DEFAULT_STATE = { + ...DEFAULT_PROPS, + ...DEFAULT_ACTIONS, +}; + +function createItemRef( index: number ) { + const node = document.createElement( 'div' ); + node.id = '' + index; + node.compareDocumentPosition = ( other ): number => { + return ( other as HTMLElement ).id < node.id + ? node.DOCUMENT_POSITION_PRECEDING + : node.DOCUMENT_POSITION_FOLLOWING; + }; + const ref: React.MutableRefObject< HTMLElement | null > = createRef(); + ref.current = node; + return ref; +} + +function createItem( stem: string, index: number, groupId?: string ) { + return { + id: `${ stem }-${ index }`, + ref: createItemRef( index ), + groupId, + }; +} + +function initialiseItems( + context: CompositeStateReturn, + stem = 'test', + count = 3 +) { + act( () => { + for ( let index = 1; index <= count; index++ ) { + context.registerItem( createItem( stem, index ) ); + } + } ); +} + +function initialiseGroups( + context: CompositeStateReturn, + stem = 'test', + count = 3 +) { + act( () => { + for ( let index = 1; index <= count; index++ ) { + const id = `${ stem }-group-${ index }`; + context.registerGroup( { id, ref: createRef() } ); + context.registerItem( createItem( stem, index, id ) ); + } + } ); +} + +describe.each( [ + [ 'reakit', reakit ], + // [ 'ariakit', ariakit ] to be implemented +] )( 'Validate %s implementation', ( _, useCompositeState ) => { + function renderState( { + baseId = DEFAULT_BASE_ID, + ...additionalState + }: InitialState = {} ) { + const initialState = { baseId, ...additionalState }; + return renderHook( () => useCompositeState( initialState ) ).result; + } + + describe( 'State', () => { + test( 'No initial state', () => { + const state = renderState(); + expect( state.current ).toEqual( DEFAULT_STATE ); + } ); + + test.each( [ + [ '`baseId`', { baseId: 'test' } ], + [ '`currentId`', { currentId: 'test' } ], + [ '`loop` [boolean]', { loop: true } ], + [ '`loop` [horizontal]', { loop: 'horizontal' } ], + [ '`loop` [vertical]', { loop: 'vertical' } ], + [ '`orientation` [horizontal]', { orientation: 'horizontal' } ], + [ '`orientation` [vertical]', { orientation: 'vertical' } ], + [ '`rtl`', { rtl: true } ], + [ '`shift`', { shift: true } ], + [ '`unstable_virtual`', { unstable_virtual: true } ], + [ '`wrap` [boolean]', { wrap: true } ], + [ '`wrap` [horizontal]', { wrap: 'horizontal' } ], + [ '`wrap` [vertical]', { wrap: 'vertical' } ], + ] )( '%s', ( __, initialState ) => { + const state = renderState( initialState as InitialState ); + + expect( state.current ).toEqual( { + ...DEFAULT_STATE, + ...initialState, + } ); + } ); + } ); + + describe( 'Actions', () => { + describe( 'Basic setters', () => { + test( '`setBaseId`', () => { + const state = renderState(); + act( () => state.current.setBaseId( 'test' ) ); + expect( state.current.baseId ).toBe( 'test' ); + } ); + + test( '`setCurrentId`', () => { + const state = renderState(); + act( () => state.current.setCurrentId( 'test' ) ); + expect( state.current.currentId ).toBe( 'test' ); + } ); + + test( '`setLoop`', () => { + const state = renderState(); + act( () => state.current.setLoop( true ) ); + expect( state.current.loop ).toBe( true ); + } ); + + test( '`setOrientation`', () => { + const state = renderState(); + act( () => state.current.setOrientation( 'horizontal' ) ); + expect( state.current.orientation ).toBe( 'horizontal' ); + } ); + + test( '`setRTL`', () => { + const state = renderState(); + act( () => state.current.setRTL( true ) ); + expect( state.current.rtl ).toBe( true ); + } ); + + test( '`setShift`', () => { + const state = renderState(); + act( () => state.current.setShift( true ) ); + expect( state.current.shift ).toBe( true ); + } ); + + test( '`setWrap`', () => { + const state = renderState(); + act( () => state.current.setWrap( true ) ); + expect( state.current.wrap ).toBe( true ); + } ); + } ); + + describe( 'Unstable setters', () => { + test( '`unstable_setHasActiveWidget`', () => { + const state = renderState(); + + act( () => state.current.unstable_setHasActiveWidget( true ) ); + expect( state.current.unstable_hasActiveWidget ).toBe( true ); + + act( () => state.current.unstable_setHasActiveWidget( false ) ); + expect( state.current.unstable_hasActiveWidget ).toBe( false ); + } ); + + test( '`unstable_setIncludesBaseElement`', () => { + const state = renderState(); + + act( () => + state.current.unstable_setIncludesBaseElement( true ) + ); + expect( state.current.unstable_includesBaseElement ).toBe( + true + ); + + act( () => + state.current.unstable_setIncludesBaseElement( false ) + ); + expect( state.current.unstable_includesBaseElement ).toBe( + false + ); + } ); + + test( '`unstable_setVirtual`', () => { + const state = renderState(); + + act( () => state.current.unstable_setVirtual( true ) ); + expect( state.current.unstable_virtual ).toBe( true ); + + act( () => state.current.unstable_setVirtual( false ) ); + expect( state.current.unstable_virtual ).toBe( false ); + } ); + } ); + + describe( 'Registration handlers', () => { + test( '`registerItem`', () => { + const state = renderState(); + + act( () => + state.current.registerItem( { + id: 'test', + ref: createRef(), + } ) + ); + + expect( state.current.items ).toMatchObject( [ + { id: 'test' }, + ] ); + } ); + + test( '`registerGroup`', () => { + const state = renderState(); + act( () => + state.current.registerGroup( { + id: 'test', + ref: createRef(), + } ) + ); + + expect( state.current.groups ).toMatchObject( [ + { id: 'test' }, + ] ); + } ); + } ); + + describe( 'Movement', () => { + test( '`move` [specified]', () => { + const state = renderState(); + initialiseItems( state.current ); + act( () => state.current.move( 'test-2' ) ); + + expect( state.current.currentId ).toBe( 'test-2' ); + } ); + + test( '`move` [unspecified]', () => { + const state = renderState(); + initialiseItems( state.current ); + act( () => state.current.move( null ) ); + + expect( state.current.currentId ).toBe( null ); + } ); + + test( '`first`', () => { + const state = renderState(); + initialiseItems( state.current ); + act( () => state.current.first() ); + + expect( state.current.currentId ).toBe( 'test-1' ); + } ); + + test( '`last`', () => { + const state = renderState(); + initialiseItems( state.current ); + act( () => state.current.last() ); + + expect( state.current.currentId ).toBe( 'test-3' ); + } ); + + test( '`previous`', () => { + const state = renderState( { + currentId: 'test-3', + } ); + initialiseItems( state.current ); + act( () => state.current.previous() ); + + expect( state.current.currentId ).toBe( 'test-2' ); + } ); + + test( '`previous` [all the way]', () => { + const state = renderState( { + currentId: 'test-3', + } ); + initialiseItems( state.current ); + act( () => state.current.previous( true ) ); + + expect( state.current.currentId ).toBe( 'test-1' ); + } ); + + test( '`next`', () => { + const state = renderState( { + currentId: 'test-1', + } ); + initialiseItems( state.current ); + act( () => state.current.next() ); + + expect( state.current.currentId ).toBe( 'test-2' ); + } ); + + test( '`next` [all the way]', () => { + const state = renderState( { + currentId: 'test-1', + } ); + initialiseItems( state.current ); + act( () => state.current.next( true ) ); + + expect( state.current.currentId ).toBe( 'test-3' ); + } ); + + test( '`up`', () => { + const state = renderState( { + currentId: 'test-3', + } ); + initialiseGroups( state.current ); + act( () => state.current.up() ); + + expect( state.current.currentId ).toBe( 'test-2' ); + } ); + + test( '`up` [all the way]', () => { + const state = renderState( { + currentId: 'test-3', + } ); + initialiseGroups( state.current ); + act( () => state.current.up( true ) ); + + expect( state.current.currentId ).toBe( 'test-1' ); + } ); + + test( '`down`', () => { + const state = renderState( { + currentId: 'test-1', + } ); + initialiseGroups( state.current ); + act( () => state.current.down() ); + + expect( state.current.currentId ).toBe( 'test-2' ); + } ); + + test( '`down` [all the way]', () => { + const state = renderState( { + currentId: 'test-1', + } ); + initialiseGroups( state.current ); + act( () => state.current.down( true ) ); + + expect( state.current.currentId ).toBe( 'test-3' ); + } ); + } ); + + describe( 'Other', () => { + test( '`sort`', () => { + const state = renderState(); + initialiseItems( state.current ); + + expect( state.current.items ).toMatchObject( [ + { id: 'test-1' }, + { id: 'test-2' }, + { id: 'test-3' }, + ] ); + + for ( const item of state.current.items ) { + if ( item.ref.current ) { + item.ref.current.id = + '' + ( 4 - parseInt( item.ref.current.id, 10 ) ); + } + } + + act( () => state.current.sort() ); + + expect( state.current.items ).toMatchObject( [ + { id: 'test-3' }, + { id: 'test-2' }, + { id: 'test-1' }, + ] ); + } ); + + test( '`reset`', () => { + const state = renderState(); + + act( () => { + state.current.setLoop( true ); + state.current.setCurrentId( 'test' ); + } ); + + expect( state.current ).toMatchObject( { + loop: true, + currentId: 'test', + } ); + + act( () => state.current.reset() ); + + expect( state.current ).toMatchObject( { + loop: false, + currentId: undefined, + } ); + } ); + } ); + } ); +} ); diff --git a/packages/components/src/composite/types.ts b/packages/components/src/composite/types.ts new file mode 100644 index 00000000000000..c76675f80c3d00 --- /dev/null +++ b/packages/components/src/composite/types.ts @@ -0,0 +1,345 @@ +type IdActions = { + /** + * Sets `baseId`. + */ + setBaseId: React.Dispatch< React.SetStateAction< string > >; +}; + +type IdState = { + baseId: string; + /** + * @private + */ + unstable_idCountRef: React.MutableRefObject< number >; +}; + +type IdInitialState = Partial< Pick< IdState, 'baseId' > >; + +type IdStateReturn = IdState & IdActions; + +type Group = { + id: string; + ref: React.RefObject< HTMLElement >; +}; + +type Item = { + id: string | null; + ref: React.RefObject< HTMLElement >; + groupId?: Group[ 'id' ]; + disabled?: boolean; +}; + +type Orientation = 'horizontal' | 'vertical'; + +type CompositeState = IdState & { + /** + * If enabled, the composite element will act as an + * [aria-activedescendant](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_focus_activedescendant) + * container instead of + * [roving tabindex](https://www.w3.org/TR/wai-aria-practices/#kbd_roving_tabindex). + * DOM focus will remain on the composite while its items receive virtual focus. + * @default false + */ + unstable_virtual: boolean; + /** + * Determines how `next` and `previous` functions will behave. If `rtl` is + * set to `true`, they will be inverted. This only affects the composite + * widget behavior. You still need to set `dir="rtl"` on HTML/CSS. + * @default false + */ + rtl: boolean; + /** + * Defines the orientation of the composite widget. If the composite has a + * single row or column (one-dimensional), the `orientation` value determines + * which arrow keys can be used to move focus: + * - `undefined`: all arrow keys work. + * - `horizontal`: only left and right arrow keys work. + * - `vertical`: only up and down arrow keys work. + * + * It doesn't have any effect on two-dimensional composites. + * @default undefined + */ + orientation?: Orientation; + /** + * Lists all the composite items with their `id`, DOM `ref`, `disabled` state + * and `groupId` if any. This state is automatically updated when + * `registerItem` and `unregisterItem` are called. + * @example + * const composite = useCompositeState(); + * composite.items.forEach((item) => { + * const { id, ref, disabled, groupId } = item; + * ... + * }); + */ + items: Item[]; + /** + * Lists all the composite groups with their `id` and DOM `ref`. This state + * is automatically updated when `registerGroup` and `unregisterGroup` are + * called. + * @example + * const composite = useCompositeState(); + * composite.groups.forEach((group) => { + * const { id, ref } = group; + * ... + * }); + */ + groups: Group[]; + /** + * The current focused item `id`. + * - `undefined` will automatically focus the first enabled composite item. + * - `null` will focus the base composite element and users will be able to + * navigate out of it using arrow keys. + * - If `currentId` is initially set to `null`, the base composite element + * itself will have focus and users will be able to navigate to it using + * arrow keys. + * @default undefined + * @example + * // First enabled item has initial focus + * useCompositeState(); + * // Base composite element has initial focus + * useCompositeState({ currentId: null }); + * // Specific composite item element has initial focus + * useCompositeState({ currentId: "item-id" }); + */ + currentId?: string | null; + /** + * On one-dimensional composites: + * - `true` loops from the last item to the first item and vice-versa. + * - `horizontal` loops only if `orientation` is `horizontal` or not set. + * - `vertical` loops only if `orientation` is `vertical` or not set. + * - If `currentId` is initially set to `null`, the composite element will + * be focused in between the last and first items. + * + * On two-dimensional composites: + * - `true` loops from the last row/column item to the first item in the + * same row/column and vice-versa. If it's the last item in the last row, it + * moves to the first item in the first row and vice-versa. + * - `horizontal` loops only from the last row item to the first item in + * the same row. + * - `vertical` loops only from the last column item to the first item in + * the column row. + * - If `currentId` is initially set to `null`, vertical loop will have no + * effect as moving down from the last row or up from the first row will + * focus the composite element. + * - If `wrap` matches the value of `loop`, it'll wrap between the last + * item in the last row or column and the first item in the first row or + * column and vice-versa. + * @default false + */ + loop: boolean | Orientation; + /** + * **Has effect only on two-dimensional composites**. If enabled, moving to + * the next item from the last one in a row or column will focus the first + * item in the next row or column and vice-versa. + * - `true` wraps between rows and columns. + * - `horizontal` wraps only between rows. + * - `vertical` wraps only between columns. + * - If `loop` matches the value of `wrap`, it'll wrap between the last + * item in the last row or column and the first item in the first row or + * column and vice-versa. + * @default false + */ + wrap: boolean | Orientation; + /** + * **Has effect only on two-dimensional composites**. If enabled, moving up + * or down when there's no next item or the next item is disabled will shift + * to the item right before it. + * @default false + */ + shift: boolean; + /** + * Stores the number of moves that have been performed by calling `move`, + * `next`, `previous`, `up`, `down`, `first` or `last`. + * @default 0 + */ + unstable_moves: number; + /** + * @default false + * @private + */ + unstable_hasActiveWidget: boolean; + /** + * @default false + * @private + */ + unstable_includesBaseElement: boolean; +}; + +export type CompositeInitialState = IdInitialState & + Partial< + Pick< + CompositeState, + | 'unstable_virtual' + | 'rtl' + | 'orientation' + | 'currentId' + | 'loop' + | 'wrap' + | 'shift' + | 'unstable_includesBaseElement' + > + >; + +type CompositeActions = IdActions & { + /** + * Registers a composite item. + * @example + * const ref = React.useRef(); + * const composite = useCompositeState(); + * React.useEffect(() => { + * composite.registerItem({ ref, id: "id" }); + * return () => composite.unregisterItem("id"); + * }, []); + */ + registerItem: ( item: Item ) => void; + /** + * Unregisters a composite item. + * @example + * const ref = React.useRef(); + * const composite = useCompositeState(); + * React.useEffect(() => { + * composite.registerItem({ ref, id: "id" }); + * return () => composite.unregisterItem("id"); + * }, []); + */ + unregisterItem: ( id: string ) => void; + /** + * Registers a composite group. + * @example + * const ref = React.useRef(); + * const composite = useCompositeState(); + * React.useEffect(() => { + * composite.registerGroup({ ref, id: "id" }); + * return () => composite.unregisterGroup("id"); + * }, []); + */ + registerGroup: ( group: Group ) => void; + /** + * Unregisters a composite group. + * @example + * const ref = React.useRef(); + * const composite = useCompositeState(); + * React.useEffect(() => { + * composite.registerGroup({ ref, id: "id" }); + * return () => composite.unregisterGroup("id"); + * }, []); + */ + unregisterGroup: ( id: string ) => void; + /** + * Moves focus to a given item ID. + * @example + * const composite = useCompositeState(); + * composite.move("item-2"); // focus item 2 + */ + move: ( id: string | null ) => void; + /** + * Moves focus to the next item. + */ + next: ( unstable_allTheWay?: boolean ) => void; + /** + * Moves focus to the previous item. + */ + previous: ( unstable_allTheWay?: boolean ) => void; + /** + * Moves focus to the item above. + */ + up: ( unstable_allTheWay?: boolean ) => void; + /** + * Moves focus to the item below. + */ + down: ( unstable_allTheWay?: boolean ) => void; + /** + * Moves focus to the first item. + */ + first: () => void; + /** + * Moves focus to the last item. + */ + last: () => void; + /** + * Sorts the `composite.items` based on the items position in the DOM. This + * is especially useful after modifying the composite items order in the DOM. + * Most of the time, though, you don't need to manually call this function as + * the re-ordering happens automatically. + */ + sort: () => void; + /** + * Sets `virtual`. + */ + unstable_setVirtual: React.Dispatch< + React.SetStateAction< CompositeState[ 'unstable_virtual' ] > + >; + /** + * Sets `rtl`. + * @example + * const composite = useCompositeState({ rtl: true }); + * composite.setRTL(false); + */ + setRTL: React.Dispatch< React.SetStateAction< CompositeState[ 'rtl' ] > >; + /** + * Sets `orientation`. + */ + setOrientation: React.Dispatch< + React.SetStateAction< CompositeState[ 'orientation' ] > + >; + /** + * Sets `currentId`. This is different from `composite.move` as this only + * updates the `currentId` state without moving focus. When the composite + * widget gets focused by the user, the item referred by the `currentId` + * state will get focus. + * @example + * const composite = useCompositeState({ currentId: "item-1" }); + * // Updates `composite.currentId` to `item-2` + * composite.setCurrentId("item-2"); + */ + setCurrentId: React.Dispatch< + React.SetStateAction< CompositeState[ 'currentId' ] > + >; + /** + * Sets `loop`. + */ + setLoop: React.Dispatch< React.SetStateAction< CompositeState[ 'loop' ] > >; + /** + * Sets `wrap`. + */ + setWrap: React.Dispatch< React.SetStateAction< CompositeState[ 'wrap' ] > >; + /** + * Sets `shift`. + */ + setShift: React.Dispatch< + React.SetStateAction< CompositeState[ 'shift' ] > + >; + /** + * Resets to initial state. + * @example + * // On initial render, currentId will be item-1 and loop will be true + * const composite = useCompositeState({ + * currentId: "item-1", + * loop: true, + * }); + * // On next render, currentId will be item-2 and loop will be false + * composite.setCurrentId("item-2"); + * composite.setLoop(false); + * // On next render, currentId will be item-1 and loop will be true + * composite.reset(); + */ + reset: () => void; + /** + * Sets `includesBaseElement`. + * @private + */ + unstable_setIncludesBaseElement: React.Dispatch< + React.SetStateAction< CompositeState[ 'unstable_includesBaseElement' ] > + >; + /** + * Sets `hasActiveWidget`. + * @private + */ + unstable_setHasActiveWidget: React.Dispatch< + React.SetStateAction< CompositeState[ 'unstable_hasActiveWidget' ] > + >; +}; + +export type CompositeStateReturn = IdStateReturn & + CompositeState & + CompositeActions; From ccf3e27152fb41fd1f075020c44091f5bee711a8 Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Wed, 29 Nov 2023 14:23:26 +0000 Subject: [PATCH 02/12] Updating CHANGELOG.md --- packages/components/CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 08082f99a00b92..1393795fdf27ff 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -20,10 +20,11 @@ ### Internal - `Slot`: add `style` prop to `bubblesVirtually` version ([#56428](https://github.com/WordPress/gutenberg/pull/56428)) +- Introduce experimental new version of `CustomSelectControl` based on `ariakit` ([#55790](https://github.com/WordPress/gutenberg/pull/55790)) -### Internal +### Code Quality -- Introduce experimental new version of `CustomSelectControl` based on `ariakit` ([#55790](https://github.com/WordPress/gutenberg/pull/55790)) +- `Composite`: add unit tests for `useCompositeState` ([#56645](https://github.com/WordPress/gutenberg/pull/56645)). ## 25.12.0 (2023-11-16) From 53a5dfb47c8c882fd9a2ebea5e5fd3324f6ec915 Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Mon, 4 Dec 2023 20:04:47 +0000 Subject: [PATCH 03/12] Adding usage tests --- .../components/src/composite/test/index.tsx | 810 +++++++++++------- 1 file changed, 511 insertions(+), 299 deletions(-) diff --git a/packages/components/src/composite/test/index.tsx b/packages/components/src/composite/test/index.tsx index eaeffda502fd74..6926a2d0eab48f 100644 --- a/packages/components/src/composite/test/index.tsx +++ b/packages/components/src/composite/test/index.tsx @@ -4,7 +4,8 @@ /** * External dependencies */ -import { act, renderHook } from '@testing-library/react'; +import { act, renderHook, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; /** * WordPress dependencies @@ -14,9 +15,23 @@ import { createRef } from '@wordpress/element'; /** * Internal dependencies */ -import { useCompositeState as reakit } from '..'; +import { + Composite as ReakitComposite, + CompositeGroup as ReakitCompositeGroup, + CompositeItem as ReakitCompositeItem, + useCompositeState as ReakitUseCompositeState, +} from '..'; import type { CompositeInitialState, CompositeStateReturn } from '../types'; +const COMPOSITE_SUITES = { + reakit: { + Composite: ReakitComposite, + CompositeGroup: ReakitCompositeGroup, + CompositeItem: ReakitCompositeItem, + useCompositeState: ReakitUseCompositeState, + }, +}; + type InitialState = Partial< CompositeInitialState >; const DEFAULT_BASE_ID = 'base'; @@ -120,325 +135,522 @@ function initialiseGroups( } ); } -describe.each( [ - [ 'reakit', reakit ], - // [ 'ariakit', ariakit ] to be implemented -] )( 'Validate %s implementation', ( _, useCompositeState ) => { - function renderState( { - baseId = DEFAULT_BASE_ID, - ...additionalState - }: InitialState = {} ) { - const initialState = { baseId, ...additionalState }; - return renderHook( () => useCompositeState( initialState ) ).result; +async function key( code: string, modifier?: string ) { + if ( modifier ) { + return await userEvent.keyboard( + `[${ modifier }>][${ code }][/${ code }]` + ); } + return await userEvent.keyboard( `[${ code }]` ); +} - describe( 'State', () => { - test( 'No initial state', () => { - const state = renderState(); - expect( state.current ).toEqual( DEFAULT_STATE ); - } ); - - test.each( [ - [ '`baseId`', { baseId: 'test' } ], - [ '`currentId`', { currentId: 'test' } ], - [ '`loop` [boolean]', { loop: true } ], - [ '`loop` [horizontal]', { loop: 'horizontal' } ], - [ '`loop` [vertical]', { loop: 'vertical' } ], - [ '`orientation` [horizontal]', { orientation: 'horizontal' } ], - [ '`orientation` [vertical]', { orientation: 'vertical' } ], - [ '`rtl`', { rtl: true } ], - [ '`shift`', { shift: true } ], - [ '`unstable_virtual`', { unstable_virtual: true } ], - [ '`wrap` [boolean]', { wrap: true } ], - [ '`wrap` [horizontal]', { wrap: 'horizontal' } ], - [ '`wrap` [vertical]', { wrap: 'vertical' } ], - ] )( '%s', ( __, initialState ) => { - const state = renderState( initialState as InitialState ); - - expect( state.current ).toEqual( { - ...DEFAULT_STATE, - ...initialState, - } ); - } ); - } ); - - describe( 'Actions', () => { - describe( 'Basic setters', () => { - test( '`setBaseId`', () => { - const state = renderState(); - act( () => state.current.setBaseId( 'test' ) ); - expect( state.current.baseId ).toBe( 'test' ); - } ); - - test( '`setCurrentId`', () => { - const state = renderState(); - act( () => state.current.setCurrentId( 'test' ) ); - expect( state.current.currentId ).toBe( 'test' ); - } ); - - test( '`setLoop`', () => { - const state = renderState(); - act( () => state.current.setLoop( true ) ); - expect( state.current.loop ).toBe( true ); - } ); - - test( '`setOrientation`', () => { - const state = renderState(); - act( () => state.current.setOrientation( 'horizontal' ) ); - expect( state.current.orientation ).toBe( 'horizontal' ); - } ); - - test( '`setRTL`', () => { - const state = renderState(); - act( () => state.current.setRTL( true ) ); - expect( state.current.rtl ).toBe( true ); - } ); - - test( '`setShift`', () => { - const state = renderState(); - act( () => state.current.setShift( true ) ); - expect( state.current.shift ).toBe( true ); - } ); - - test( '`setWrap`', () => { - const state = renderState(); - act( () => state.current.setWrap( true ) ); - expect( state.current.wrap ).toBe( true ); - } ); - } ); - - describe( 'Unstable setters', () => { - test( '`unstable_setHasActiveWidget`', () => { - const state = renderState(); - - act( () => state.current.unstable_setHasActiveWidget( true ) ); - expect( state.current.unstable_hasActiveWidget ).toBe( true ); - - act( () => state.current.unstable_setHasActiveWidget( false ) ); - expect( state.current.unstable_hasActiveWidget ).toBe( false ); - } ); - - test( '`unstable_setIncludesBaseElement`', () => { - const state = renderState(); - - act( () => - state.current.unstable_setIncludesBaseElement( true ) - ); - expect( state.current.unstable_includesBaseElement ).toBe( - true - ); - - act( () => - state.current.unstable_setIncludesBaseElement( false ) - ); - expect( state.current.unstable_includesBaseElement ).toBe( - false - ); - } ); - - test( '`unstable_setVirtual`', () => { - const state = renderState(); - - act( () => state.current.unstable_setVirtual( true ) ); - expect( state.current.unstable_virtual ).toBe( true ); - - act( () => state.current.unstable_setVirtual( false ) ); - expect( state.current.unstable_virtual ).toBe( false ); - } ); - } ); - - describe( 'Registration handlers', () => { - test( '`registerItem`', () => { - const state = renderState(); - - act( () => - state.current.registerItem( { - id: 'test', - ref: createRef(), - } ) - ); - - expect( state.current.items ).toMatchObject( [ - { id: 'test' }, - ] ); - } ); - - test( '`registerGroup`', () => { - const state = renderState(); - act( () => - state.current.registerGroup( { - id: 'test', - ref: createRef(), - } ) - ); - - expect( state.current.groups ).toMatchObject( [ - { id: 'test' }, - ] ); - } ); - } ); - - describe( 'Movement', () => { - test( '`move` [specified]', () => { - const state = renderState(); - initialiseItems( state.current ); - act( () => state.current.move( 'test-2' ) ); - - expect( state.current.currentId ).toBe( 'test-2' ); - } ); - - test( '`move` [unspecified]', () => { - const state = renderState(); - initialiseItems( state.current ); - act( () => state.current.move( null ) ); - - expect( state.current.currentId ).toBe( null ); - } ); - - test( '`first`', () => { - const state = renderState(); - initialiseItems( state.current ); - act( () => state.current.first() ); - - expect( state.current.currentId ).toBe( 'test-1' ); - } ); - - test( '`last`', () => { - const state = renderState(); - initialiseItems( state.current ); - act( () => state.current.last() ); - - expect( state.current.currentId ).toBe( 'test-3' ); - } ); +describe.each( Object.entries( COMPOSITE_SUITES ) )( + 'Validate %s implementation', + ( _, { Composite, CompositeGroup, CompositeItem, useCompositeState } ) => { + function renderState( { + baseId = DEFAULT_BASE_ID, + ...additionalState + }: InitialState = {} ) { + const initialState = { baseId, ...additionalState }; + return renderHook( () => useCompositeState( initialState ) ).result; + } - test( '`previous`', () => { - const state = renderState( { - currentId: 'test-3', + describe( 'API', () => { + describe( 'State', () => { + test( 'No initial state', () => { + const state = renderState(); + expect( state.current ).toEqual( DEFAULT_STATE ); } ); - initialiseItems( state.current ); - act( () => state.current.previous() ); - - expect( state.current.currentId ).toBe( 'test-2' ); - } ); - test( '`previous` [all the way]', () => { - const state = renderState( { - currentId: 'test-3', + test.each( [ + [ '`baseId`', { baseId: 'test' } ], + [ '`currentId`', { currentId: 'test' } ], + [ '`loop` [boolean]', { loop: true } ], + [ '`loop` [horizontal]', { loop: 'horizontal' } ], + [ '`loop` [vertical]', { loop: 'vertical' } ], + [ + '`orientation` [horizontal]', + { orientation: 'horizontal' }, + ], + [ '`orientation` [vertical]', { orientation: 'vertical' } ], + [ '`rtl`', { rtl: true } ], + [ '`shift`', { shift: true } ], + [ '`unstable_virtual`', { unstable_virtual: true } ], + [ '`wrap` [boolean]', { wrap: true } ], + [ '`wrap` [horizontal]', { wrap: 'horizontal' } ], + [ '`wrap` [vertical]', { wrap: 'vertical' } ], + ] )( '%s', ( __, initialState ) => { + const state = renderState( initialState as InitialState ); + + expect( state.current ).toEqual( { + ...DEFAULT_STATE, + ...initialState, + } ); } ); - initialiseItems( state.current ); - act( () => state.current.previous( true ) ); - - expect( state.current.currentId ).toBe( 'test-1' ); } ); - test( '`next`', () => { - const state = renderState( { - currentId: 'test-1', + describe( 'Actions', () => { + describe( 'Basic setters', () => { + test( '`setBaseId`', () => { + const state = renderState(); + act( () => state.current.setBaseId( 'test' ) ); + expect( state.current.baseId ).toBe( 'test' ); + } ); + + test( '`setCurrentId`', () => { + const state = renderState(); + act( () => state.current.setCurrentId( 'test' ) ); + expect( state.current.currentId ).toBe( 'test' ); + } ); + + test( '`setLoop`', () => { + const state = renderState(); + act( () => state.current.setLoop( true ) ); + expect( state.current.loop ).toBe( true ); + } ); + + test( '`setOrientation`', () => { + const state = renderState(); + act( () => + state.current.setOrientation( 'horizontal' ) + ); + expect( state.current.orientation ).toBe( + 'horizontal' + ); + } ); + + test( '`setRTL`', () => { + const state = renderState(); + act( () => state.current.setRTL( true ) ); + expect( state.current.rtl ).toBe( true ); + } ); + + test( '`setShift`', () => { + const state = renderState(); + act( () => state.current.setShift( true ) ); + expect( state.current.shift ).toBe( true ); + } ); + + test( '`setWrap`', () => { + const state = renderState(); + act( () => state.current.setWrap( true ) ); + expect( state.current.wrap ).toBe( true ); + } ); } ); - initialiseItems( state.current ); - act( () => state.current.next() ); - expect( state.current.currentId ).toBe( 'test-2' ); - } ); - - test( '`next` [all the way]', () => { - const state = renderState( { - currentId: 'test-1', + describe( 'Unstable setters', () => { + test( '`unstable_setHasActiveWidget`', () => { + const state = renderState(); + + act( () => + state.current.unstable_setHasActiveWidget( true ) + ); + expect( state.current.unstable_hasActiveWidget ).toBe( + true + ); + + act( () => + state.current.unstable_setHasActiveWidget( false ) + ); + expect( state.current.unstable_hasActiveWidget ).toBe( + false + ); + } ); + + test( '`unstable_setIncludesBaseElement`', () => { + const state = renderState(); + + act( () => + state.current.unstable_setIncludesBaseElement( + true + ) + ); + expect( + state.current.unstable_includesBaseElement + ).toBe( true ); + + act( () => + state.current.unstable_setIncludesBaseElement( + false + ) + ); + expect( + state.current.unstable_includesBaseElement + ).toBe( false ); + } ); + + test( '`unstable_setVirtual`', () => { + const state = renderState(); + + act( () => state.current.unstable_setVirtual( true ) ); + expect( state.current.unstable_virtual ).toBe( true ); + + act( () => state.current.unstable_setVirtual( false ) ); + expect( state.current.unstable_virtual ).toBe( false ); + } ); } ); - initialiseItems( state.current ); - act( () => state.current.next( true ) ); - expect( state.current.currentId ).toBe( 'test-3' ); - } ); - - test( '`up`', () => { - const state = renderState( { - currentId: 'test-3', - } ); - initialiseGroups( state.current ); - act( () => state.current.up() ); - - expect( state.current.currentId ).toBe( 'test-2' ); - } ); - - test( '`up` [all the way]', () => { - const state = renderState( { - currentId: 'test-3', + describe( 'Registration handlers', () => { + test( '`registerItem`', () => { + const state = renderState(); + + act( () => + state.current.registerItem( { + id: 'test', + ref: createRef(), + } ) + ); + + expect( state.current.items ).toMatchObject( [ + { id: 'test' }, + ] ); + } ); + + test( '`registerGroup`', () => { + const state = renderState(); + act( () => + state.current.registerGroup( { + id: 'test', + ref: createRef(), + } ) + ); + + expect( state.current.groups ).toMatchObject( [ + { id: 'test' }, + ] ); + } ); } ); - initialiseGroups( state.current ); - act( () => state.current.up( true ) ); - expect( state.current.currentId ).toBe( 'test-1' ); - } ); - - test( '`down`', () => { - const state = renderState( { - currentId: 'test-1', + describe( 'Movement', () => { + test( '`move` [specified]', () => { + const state = renderState(); + initialiseItems( state.current ); + act( () => state.current.move( 'test-2' ) ); + + expect( state.current.currentId ).toBe( 'test-2' ); + } ); + + test( '`move` [unspecified]', () => { + const state = renderState(); + initialiseItems( state.current ); + act( () => state.current.move( null ) ); + + expect( state.current.currentId ).toBe( null ); + } ); + + test( '`first`', () => { + const state = renderState(); + initialiseItems( state.current ); + act( () => state.current.first() ); + + expect( state.current.currentId ).toBe( 'test-1' ); + } ); + + test( '`last`', () => { + const state = renderState(); + initialiseItems( state.current ); + act( () => state.current.last() ); + + expect( state.current.currentId ).toBe( 'test-3' ); + } ); + + test( '`previous`', () => { + const state = renderState( { + currentId: 'test-3', + } ); + initialiseItems( state.current ); + act( () => state.current.previous() ); + + expect( state.current.currentId ).toBe( 'test-2' ); + } ); + + test( '`previous` [all the way]', () => { + const state = renderState( { + currentId: 'test-3', + } ); + initialiseItems( state.current ); + act( () => state.current.previous( true ) ); + + expect( state.current.currentId ).toBe( 'test-1' ); + } ); + + test( '`next`', () => { + const state = renderState( { + currentId: 'test-1', + } ); + initialiseItems( state.current ); + act( () => state.current.next() ); + + expect( state.current.currentId ).toBe( 'test-2' ); + } ); + + test( '`next` [all the way]', () => { + const state = renderState( { + currentId: 'test-1', + } ); + initialiseItems( state.current ); + act( () => state.current.next( true ) ); + + expect( state.current.currentId ).toBe( 'test-3' ); + } ); + + test( '`up`', () => { + const state = renderState( { + currentId: 'test-3', + } ); + initialiseGroups( state.current ); + act( () => state.current.up() ); + + expect( state.current.currentId ).toBe( 'test-2' ); + } ); + + test( '`up` [all the way]', () => { + const state = renderState( { + currentId: 'test-3', + } ); + initialiseGroups( state.current ); + act( () => state.current.up( true ) ); + + expect( state.current.currentId ).toBe( 'test-1' ); + } ); + + test( '`down`', () => { + const state = renderState( { + currentId: 'test-1', + } ); + initialiseGroups( state.current ); + act( () => state.current.down() ); + + expect( state.current.currentId ).toBe( 'test-2' ); + } ); + + test( '`down` [all the way]', () => { + const state = renderState( { + currentId: 'test-1', + } ); + initialiseGroups( state.current ); + act( () => state.current.down( true ) ); + + expect( state.current.currentId ).toBe( 'test-3' ); + } ); } ); - initialiseGroups( state.current ); - act( () => state.current.down() ); - expect( state.current.currentId ).toBe( 'test-2' ); - } ); - - test( '`down` [all the way]', () => { - const state = renderState( { - currentId: 'test-1', + describe( 'Other', () => { + test( '`sort`', () => { + const state = renderState(); + initialiseItems( state.current ); + + expect( state.current.items ).toMatchObject( [ + { id: 'test-1' }, + { id: 'test-2' }, + { id: 'test-3' }, + ] ); + + for ( const item of state.current.items ) { + if ( item.ref.current ) { + item.ref.current.id = + '' + + ( 4 - parseInt( item.ref.current.id, 10 ) ); + } + } + + act( () => state.current.sort() ); + + expect( state.current.items ).toMatchObject( [ + { id: 'test-3' }, + { id: 'test-2' }, + { id: 'test-1' }, + ] ); + } ); + + test( '`reset`', () => { + const state = renderState(); + + act( () => { + state.current.setLoop( true ); + state.current.setCurrentId( 'test' ); + } ); + + expect( state.current ).toMatchObject( { + loop: true, + currentId: 'test', + } ); + + act( () => state.current.reset() ); + + expect( state.current ).toMatchObject( { + loop: false, + currentId: undefined, + } ); + } ); } ); - initialiseGroups( state.current ); - act( () => state.current.down( true ) ); - - expect( state.current.currentId ).toBe( 'test-3' ); } ); } ); - describe( 'Other', () => { - test( '`sort`', () => { - const state = renderState(); - initialiseItems( state.current ); - - expect( state.current.items ).toMatchObject( [ - { id: 'test-1' }, - { id: 'test-2' }, - { id: 'test-3' }, - ] ); - - for ( const item of state.current.items ) { - if ( item.ref.current ) { - item.ref.current.id = - '' + ( 4 - parseInt( item.ref.current.id, 10 ) ); - } - } - - act( () => state.current.sort() ); - - expect( state.current.items ).toMatchObject( [ - { id: 'test-3' }, - { id: 'test-2' }, - { id: 'test-1' }, - ] ); + describe( 'Usage', () => { + test( 'Renders as a single tab stop', async () => { + const Test = () => { + const composite = useCompositeState(); + return ( + <> + + + + Item 1 + + + Item 2 + + + Item 3 + + + + + ); + }; + render( ); + + await userEvent.tab(); + expect( screen.getByText( 'Before' ) ).toHaveFocus(); + await userEvent.tab(); + expect( screen.getByText( 'Item 1' ) ).toHaveFocus(); + await userEvent.tab(); + expect( screen.getByText( 'After' ) ).toHaveFocus(); + await userEvent.tab( { shift: true } ); + expect( screen.getByText( 'Item 1' ) ).toHaveFocus(); } ); - test( '`reset`', () => { - const state = renderState(); - - act( () => { - state.current.setLoop( true ); - state.current.setCurrentId( 'test' ); - } ); - - expect( state.current ).toMatchObject( { - loop: true, - currentId: 'test', - } ); - - act( () => state.current.reset() ); + test( 'Works in one dimension', async () => { + const Test = () => { + const composite = useCompositeState(); + return ( + + + Item 1 + + + Item 2 + + + Item 3 + + + ); + }; + render( ); + + const item1 = screen.getByText( 'Item 1' ); + const item2 = screen.getByText( 'Item 2' ); + const item3 = screen.getByText( 'Item 3' ); + + await userEvent.tab(); + expect( item1 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( item2 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( item3 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( item3 ).toHaveFocus(); + await key( 'ArrowUp' ); + expect( item2 ).toHaveFocus(); + await key( 'ArrowUp' ); + expect( item1 ).toHaveFocus(); + await key( 'ArrowUp' ); + expect( item1 ).toHaveFocus(); + await key( 'ArrowRight' ); + expect( item2 ).toHaveFocus(); + await key( 'ArrowRight' ); + expect( item3 ).toHaveFocus(); + await key( 'ArrowLeft' ); + expect( item2 ).toHaveFocus(); + await key( 'ArrowLeft' ); + expect( item1 ).toHaveFocus(); + await key( 'End' ); + expect( item3 ).toHaveFocus(); + await key( 'Home' ); + expect( item1 ).toHaveFocus(); + await key( 'PageDown' ); + expect( item3 ).toHaveFocus(); + await key( 'PageUp' ); + expect( item1 ).toHaveFocus(); + } ); - expect( state.current ).toMatchObject( { - loop: false, - currentId: undefined, - } ); + test( 'Works in two dimensions', async () => { + const Test = () => { + const composite = useCompositeState(); + + return ( + + + + Item A1 + + + Item A2 + + + Item A3 + + + + + Item B1 + + + Item B2 + + + Item B3 + + + + + Item C1 + + + Item C2 + + + Item C3 + + + + ); + }; + render( ); + + const itemA1 = screen.getByText( 'Item A1' ); + const itemA2 = screen.getByText( 'Item A2' ); + const itemA3 = screen.getByText( 'Item A3' ); + const itemB1 = screen.getByText( 'Item B1' ); + const itemB2 = screen.getByText( 'Item B2' ); + const itemC1 = screen.getByText( 'Item C1' ); + const itemC3 = screen.getByText( 'Item C3' ); + + await userEvent.tab(); + expect( itemA1 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( itemB1 ).toHaveFocus(); + await key( 'ArrowRight' ); + expect( itemB2 ).toHaveFocus(); + await key( 'ArrowUp' ); + expect( itemA2 ).toHaveFocus(); + await key( 'ArrowLeft' ); + expect( itemA1 ).toHaveFocus(); + await key( 'End' ); + expect( itemA3 ).toHaveFocus(); + await key( 'PageDown' ); + expect( itemC3 ).toHaveFocus(); + await key( 'Home' ); + expect( itemC1 ).toHaveFocus(); + await key( 'PageUp' ); + expect( itemA1 ).toHaveFocus(); + await key( 'End', 'ControlLeft' ); + expect( itemC3 ).toHaveFocus(); + await key( 'Home', 'ControlLeft' ); + expect( itemA1 ).toHaveFocus(); } ); } ); - } ); -} ); + } +); From 6ea08c6098c7dc27b9d68d0a269c3a9719689f89 Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Mon, 4 Dec 2023 20:55:14 +0000 Subject: [PATCH 04/12] Adding disabled item tests --- .../components/src/composite/test/index.tsx | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/packages/components/src/composite/test/index.tsx b/packages/components/src/composite/test/index.tsx index 6926a2d0eab48f..7fe11be666d6a5 100644 --- a/packages/components/src/composite/test/index.tsx +++ b/packages/components/src/composite/test/index.tsx @@ -651,6 +651,71 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( await key( 'Home', 'ControlLeft' ); expect( itemA1 ).toHaveFocus(); } ); + + test( 'Ignores disabled items', async () => { + const Test = () => { + const composite = useCompositeState(); + return ( + + + Item 1 + + + Item 2 + + + Item 3 + + + ); + }; + render( ); + + const item1 = screen.getByText( 'Item 1' ); + const item2 = screen.getByText( 'Item 2' ); + const item3 = screen.getByText( 'Item 3' ); + + expect( item2 ).toBeDisabled(); + + await userEvent.tab(); + expect( item1 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( item2 ).not.toHaveFocus(); + expect( item3 ).toHaveFocus(); + } ); + + test( 'Includes focusable disabled items', async () => { + const Test = () => { + const composite = useCompositeState(); + return ( + + + Item 1 + + + Item 2 + + + Item 3 + + + ); + }; + render( ); + + const item1 = screen.getByText( 'Item 1' ); + const item2 = screen.getByText( 'Item 2' ); + const item3 = screen.getByText( 'Item 3' ); + + expect( item2 ).toBeEnabled(); + expect( item2 ).toHaveAttribute( 'aria-disabled', 'true' ); + + await userEvent.tab(); + expect( item1 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( item2 ).toHaveFocus(); + expect( item3 ).not.toHaveFocus(); + } ); } ); } ); From 5e45b4dc112be5f50e001bd8191de4610075030f Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Mon, 4 Dec 2023 22:52:35 +0000 Subject: [PATCH 05/12] Adding/updating usage tests --- .../components/src/composite/test/index.tsx | 495 ++++++++++++------ 1 file changed, 338 insertions(+), 157 deletions(-) diff --git a/packages/components/src/composite/test/index.tsx b/packages/components/src/composite/test/index.tsx index 7fe11be666d6a5..ea326797861feb 100644 --- a/packages/components/src/composite/test/index.tsx +++ b/packages/components/src/composite/test/index.tsx @@ -488,171 +488,120 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( } ); describe( 'Usage', () => { - test( 'Renders as a single tab stop', async () => { - const Test = () => { - const composite = useCompositeState(); - return ( - <> - - - - Item 1 - - - Item 2 - - - Item 3 - - - - - ); + const OneDimensionalTest = ( + initialState?: CompositeInitialState + ) => { + const composite = useCompositeState( initialState ); + return ( + + Item 1 + Item 2 + Item 3 + + ); + }; + + const getOneDimensionalItems = () => { + return { + item1: screen.getByText( 'Item 1' ), + item2: screen.getByText( 'Item 2' ), + item3: screen.getByText( 'Item 3' ), }; - render( ); - - await userEvent.tab(); - expect( screen.getByText( 'Before' ) ).toHaveFocus(); - await userEvent.tab(); - expect( screen.getByText( 'Item 1' ) ).toHaveFocus(); - await userEvent.tab(); - expect( screen.getByText( 'After' ) ).toHaveFocus(); - await userEvent.tab( { shift: true } ); - expect( screen.getByText( 'Item 1' ) ).toHaveFocus(); - } ); - - test( 'Works in one dimension', async () => { - const Test = () => { - const composite = useCompositeState(); - return ( - + }; + + const initialiseOneDimensionalTest = ( + initialState?: CompositeInitialState + ) => { + render( ); + return getOneDimensionalItems(); + }; + + const TwoDimensionalTest = ( + initialState?: CompositeInitialState + ) => { + const composite = useCompositeState( initialState ); + + return ( + + - Item 1 + Item A1 - Item 2 + Item A2 - Item 3 + Item A3 - - ); + + + + Item B1 + + + Item B2 + + + Item B3 + + + + + Item C1 + + + Item C2 + + + Item C3 + + + + ); + }; + + const getTwoDimensionalItems = () => { + return { + itemA1: screen.getByText( 'Item A1' ), + itemA2: screen.getByText( 'Item A2' ), + itemA3: screen.getByText( 'Item A3' ), + itemB1: screen.getByText( 'Item B1' ), + itemB2: screen.getByText( 'Item B2' ), + itemB3: screen.getByText( 'Item B3' ), + itemC1: screen.getByText( 'Item C1' ), + itemC2: screen.getByText( 'Item C2' ), + itemC3: screen.getByText( 'Item C3' ), }; - render( ); - - const item1 = screen.getByText( 'Item 1' ); - const item2 = screen.getByText( 'Item 2' ); - const item3 = screen.getByText( 'Item 3' ); - - await userEvent.tab(); - expect( item1 ).toHaveFocus(); - await key( 'ArrowDown' ); - expect( item2 ).toHaveFocus(); - await key( 'ArrowDown' ); - expect( item3 ).toHaveFocus(); - await key( 'ArrowDown' ); - expect( item3 ).toHaveFocus(); - await key( 'ArrowUp' ); - expect( item2 ).toHaveFocus(); - await key( 'ArrowUp' ); - expect( item1 ).toHaveFocus(); - await key( 'ArrowUp' ); - expect( item1 ).toHaveFocus(); - await key( 'ArrowRight' ); - expect( item2 ).toHaveFocus(); - await key( 'ArrowRight' ); - expect( item3 ).toHaveFocus(); - await key( 'ArrowLeft' ); - expect( item2 ).toHaveFocus(); - await key( 'ArrowLeft' ); - expect( item1 ).toHaveFocus(); - await key( 'End' ); - expect( item3 ).toHaveFocus(); - await key( 'Home' ); - expect( item1 ).toHaveFocus(); - await key( 'PageDown' ); - expect( item3 ).toHaveFocus(); - await key( 'PageUp' ); - expect( item1 ).toHaveFocus(); - } ); + }; - test( 'Works in two dimensions', async () => { - const Test = () => { - const composite = useCompositeState(); + const initialiseTwoDimensionalTest = ( + initialState?: CompositeInitialState + ) => { + render( ); + return getTwoDimensionalItems(); + }; - return ( - - - - Item A1 - - - Item A2 - - - Item A3 - - - - - Item B1 - - - Item B2 - - - Item B3 - - - - - Item C1 - - - Item C2 - - - Item C3 - - - - ); - }; + test( 'Renders as a single tab stop', async () => { + const Test = () => ( + <> + + + + + ); render( ); - const itemA1 = screen.getByText( 'Item A1' ); - const itemA2 = screen.getByText( 'Item A2' ); - const itemA3 = screen.getByText( 'Item A3' ); - const itemB1 = screen.getByText( 'Item B1' ); - const itemB2 = screen.getByText( 'Item B2' ); - const itemC1 = screen.getByText( 'Item C1' ); - const itemC3 = screen.getByText( 'Item C3' ); - await userEvent.tab(); - expect( itemA1 ).toHaveFocus(); - await key( 'ArrowDown' ); - expect( itemB1 ).toHaveFocus(); - await key( 'ArrowRight' ); - expect( itemB2 ).toHaveFocus(); - await key( 'ArrowUp' ); - expect( itemA2 ).toHaveFocus(); - await key( 'ArrowLeft' ); - expect( itemA1 ).toHaveFocus(); - await key( 'End' ); - expect( itemA3 ).toHaveFocus(); - await key( 'PageDown' ); - expect( itemC3 ).toHaveFocus(); - await key( 'Home' ); - expect( itemC1 ).toHaveFocus(); - await key( 'PageUp' ); - expect( itemA1 ).toHaveFocus(); - await key( 'End', 'ControlLeft' ); - expect( itemC3 ).toHaveFocus(); - await key( 'Home', 'ControlLeft' ); - expect( itemA1 ).toHaveFocus(); + expect( screen.getByText( 'Before' ) ).toHaveFocus(); + await userEvent.tab(); + expect( screen.getByText( 'Item 1' ) ).toHaveFocus(); + await userEvent.tab(); + expect( screen.getByText( 'After' ) ).toHaveFocus(); + await userEvent.tab( { shift: true } ); + expect( screen.getByText( 'Item 1' ) ).toHaveFocus(); } ); - test( 'Ignores disabled items', async () => { + test( 'Excludes disabled items', async () => { const Test = () => { const composite = useCompositeState(); return ( @@ -671,9 +620,7 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( }; render( ); - const item1 = screen.getByText( 'Item 1' ); - const item2 = screen.getByText( 'Item 2' ); - const item3 = screen.getByText( 'Item 3' ); + const { item1, item2, item3 } = getOneDimensionalItems(); expect( item2 ).toBeDisabled(); @@ -702,10 +649,7 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( ); }; render( ); - - const item1 = screen.getByText( 'Item 1' ); - const item2 = screen.getByText( 'Item 2' ); - const item3 = screen.getByText( 'Item 3' ); + const { item1, item2, item3 } = getOneDimensionalItems(); expect( item2 ).toBeEnabled(); expect( item2 ).toHaveAttribute( 'aria-disabled', 'true' ); @@ -716,6 +660,243 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( expect( item2 ).toHaveFocus(); expect( item3 ).not.toHaveFocus(); } ); + + describe( 'In one dimension', () => { + test( 'All directions work with no orientation', async () => { + const { item1, item2, item3 } = + initialiseOneDimensionalTest(); + + await userEvent.tab(); + expect( item1 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( item2 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( item3 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( item3 ).toHaveFocus(); + await key( 'ArrowUp' ); + expect( item2 ).toHaveFocus(); + await key( 'ArrowUp' ); + expect( item1 ).toHaveFocus(); + await key( 'ArrowUp' ); + expect( item1 ).toHaveFocus(); + await key( 'ArrowRight' ); + expect( item2 ).toHaveFocus(); + await key( 'ArrowRight' ); + expect( item3 ).toHaveFocus(); + await key( 'ArrowLeft' ); + expect( item2 ).toHaveFocus(); + await key( 'ArrowLeft' ); + expect( item1 ).toHaveFocus(); + await key( 'End' ); + expect( item3 ).toHaveFocus(); + await key( 'Home' ); + expect( item1 ).toHaveFocus(); + await key( 'PageDown' ); + expect( item3 ).toHaveFocus(); + await key( 'PageUp' ); + expect( item1 ).toHaveFocus(); + } ); + + test( 'Only left/right work with horizontal orientation', async () => { + const { item1, item2, item3 } = + initialiseOneDimensionalTest( { + orientation: 'horizontal', + } ); + + await userEvent.tab(); + expect( item1 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( item1 ).toHaveFocus(); + await key( 'ArrowRight' ); + expect( item2 ).toHaveFocus(); + await key( 'ArrowRight' ); + expect( item3 ).toHaveFocus(); + await key( 'ArrowUp' ); + expect( item3 ).toHaveFocus(); + await key( 'ArrowLeft' ); + expect( item2 ).toHaveFocus(); + await key( 'ArrowLeft' ); + expect( item1 ).toHaveFocus(); + await key( 'End' ); + expect( item3 ).toHaveFocus(); + await key( 'Home' ); + expect( item1 ).toHaveFocus(); + await key( 'PageDown' ); + expect( item3 ).toHaveFocus(); + await key( 'PageUp' ); + expect( item1 ).toHaveFocus(); + } ); + + test( 'Only up/down work with vertical orientation', async () => { + const { item1, item2, item3 } = + initialiseOneDimensionalTest( { + orientation: 'vertical', + } ); + + await userEvent.tab(); + expect( item1 ).toHaveFocus(); + await key( 'ArrowRight' ); + expect( item1 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( item2 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( item3 ).toHaveFocus(); + await key( 'ArrowLeft' ); + expect( item3 ).toHaveFocus(); + await key( 'ArrowUp' ); + expect( item2 ).toHaveFocus(); + await key( 'ArrowUp' ); + expect( item1 ).toHaveFocus(); + await key( 'End' ); + expect( item3 ).toHaveFocus(); + await key( 'Home' ); + expect( item1 ).toHaveFocus(); + await key( 'PageDown' ); + expect( item3 ).toHaveFocus(); + await key( 'PageUp' ); + expect( item1 ).toHaveFocus(); + } ); + + test( 'Focus wraps with loop enabled', async () => { + const { item1, item2, item3 } = + initialiseOneDimensionalTest( { + loop: true, + } ); + + await userEvent.tab(); + expect( item1 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( item2 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( item3 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( item1 ).toHaveFocus(); + await key( 'ArrowUp' ); + expect( item3 ).toHaveFocus(); + await key( 'ArrowRight' ); + expect( item1 ).toHaveFocus(); + await key( 'ArrowLeft' ); + expect( item3 ).toHaveFocus(); + } ); + } ); + + describe( 'In two dimensions', () => { + test( 'All directions work as standard', async () => { + const { + itemA1, + itemA2, + itemA3, + itemB1, + itemB2, + itemC1, + itemC3, + } = initialiseTwoDimensionalTest(); + + await userEvent.tab(); + expect( itemA1 ).toHaveFocus(); + await key( 'ArrowUp' ); + expect( itemA1 ).toHaveFocus(); + await key( 'ArrowLeft' ); + expect( itemA1 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( itemB1 ).toHaveFocus(); + await key( 'ArrowRight' ); + expect( itemB2 ).toHaveFocus(); + await key( 'ArrowUp' ); + expect( itemA2 ).toHaveFocus(); + await key( 'ArrowLeft' ); + expect( itemA1 ).toHaveFocus(); + await key( 'End' ); + expect( itemA3 ).toHaveFocus(); + await key( 'PageDown' ); + expect( itemC3 ).toHaveFocus(); + await key( 'ArrowRight' ); + expect( itemC3 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( itemC3 ).toHaveFocus(); + await key( 'Home' ); + expect( itemC1 ).toHaveFocus(); + await key( 'PageUp' ); + expect( itemA1 ).toHaveFocus(); + await key( 'End', 'ControlLeft' ); + expect( itemC3 ).toHaveFocus(); + await key( 'Home', 'ControlLeft' ); + expect( itemA1 ).toHaveFocus(); + } ); + + test( 'Focus wraps around rows/columns with loop enabled', async () => { + const { itemA1, itemA2, itemA3, itemB1, itemC1, itemC3 } = + initialiseTwoDimensionalTest( { loop: true } ); + + await userEvent.tab(); + expect( itemA1 ).toHaveFocus(); + await key( 'ArrowRight' ); + expect( itemA2 ).toHaveFocus(); + await key( 'ArrowRight' ); + expect( itemA3 ).toHaveFocus(); + await key( 'ArrowRight' ); + expect( itemA1 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( itemB1 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( itemC1 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( itemA1 ).toHaveFocus(); + await key( 'ArrowLeft' ); + expect( itemA3 ).toHaveFocus(); + await key( 'ArrowUp' ); + expect( itemC3 ).toHaveFocus(); + } ); + + test( 'Focus moves between rows/columns with wrap enabled', async () => { + const { itemA1, itemA2, itemA3, itemB1, itemC1, itemC3 } = + initialiseTwoDimensionalTest( { wrap: true } ); + + await userEvent.tab(); + expect( itemA1 ).toHaveFocus(); + await key( 'ArrowRight' ); + expect( itemA2 ).toHaveFocus(); + await key( 'ArrowRight' ); + expect( itemA3 ).toHaveFocus(); + await key( 'ArrowRight' ); + expect( itemB1 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( itemC1 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( itemA2 ).toHaveFocus(); + await key( 'ArrowLeft' ); + expect( itemA1 ).toHaveFocus(); + await key( 'ArrowLeft' ); + expect( itemA1 ).toHaveFocus(); + await key( 'ArrowUp' ); + expect( itemA1 ).toHaveFocus(); + await key( 'End', 'ControlLeft' ); + expect( itemC3 ).toHaveFocus(); + await key( 'ArrowRight' ); + expect( itemC3 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( itemC3 ).toHaveFocus(); + } ); + + test( 'Focus wraps around start/end with loop and wrap enabled', async () => { + const { itemA1, itemC3 } = initialiseTwoDimensionalTest( { + loop: true, + wrap: true, + } ); + + await userEvent.tab(); + expect( itemA1 ).toHaveFocus(); + await key( 'ArrowLeft' ); + expect( itemC3 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( itemA1 ).toHaveFocus(); + await key( 'ArrowUp' ); + expect( itemC3 ).toHaveFocus(); + await key( 'ArrowRight' ); + expect( itemA1 ).toHaveFocus(); + } ); + } ); } ); } ); From a79af37c2dca99010f8153bfb6866bd12eb04e03 Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Mon, 4 Dec 2023 23:18:59 +0000 Subject: [PATCH 06/12] Adding shift tests --- .../components/src/composite/test/index.tsx | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/packages/components/src/composite/test/index.tsx b/packages/components/src/composite/test/index.tsx index ea326797861feb..48341992460d02 100644 --- a/packages/components/src/composite/test/index.tsx +++ b/packages/components/src/composite/test/index.tsx @@ -581,6 +581,46 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( return getTwoDimensionalItems(); }; + const initialiseShiftTest = ( shift: boolean ) => { + const Test = () => { + const composite = useCompositeState( { shift } ); + + return ( + + + + Item A1 + + + + + Item B1 + + + Item B2 + + + + + Item C1 + + + Item C2 + + + + ); + }; + render( ); + return { + itemA1: screen.getByText( 'Item A1' ), + itemB1: screen.getByText( 'Item B1' ), + itemB2: screen.getByText( 'Item B2' ), + itemC1: screen.getByText( 'Item C1' ), + itemC2: screen.getByText( 'Item C2' ), + }; + }; + test( 'Renders as a single tab stop', async () => { const Test = () => ( <> @@ -896,6 +936,46 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( await key( 'ArrowRight' ); expect( itemA1 ).toHaveFocus(); } ); + + test( 'Focus shifts if vertical neighbour unavailable when shift enabled', async () => { + const { itemA1, itemB1, itemB2, itemC1 } = + initialiseShiftTest( true ); + + await userEvent.tab(); + expect( itemA1 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( itemB1 ).toHaveFocus(); + await key( 'ArrowRight' ); + expect( itemB2 ).toHaveFocus(); + await key( 'ArrowUp' ); + // A2 doesn't exist + expect( itemA1 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( itemB1 ).toHaveFocus(); + await key( 'ArrowRight' ); + expect( itemB2 ).toHaveFocus(); + await key( 'ArrowDown' ); + // C2 is disabled + expect( itemC1 ).toHaveFocus(); + } ); + + test( 'Focus does not shift if vertical neighbour unavailable when shift not enabled', async () => { + const { itemA1, itemB1, itemB2 } = + initialiseShiftTest( false ); + + await userEvent.tab(); + expect( itemA1 ).toHaveFocus(); + await key( 'ArrowDown' ); + expect( itemB1 ).toHaveFocus(); + await key( 'ArrowRight' ); + expect( itemB2 ).toHaveFocus(); + await key( 'ArrowUp' ); + // A2 doesn't exist + expect( itemB2 ).toHaveFocus(); + await key( 'ArrowDown' ); + // C2 is disabled + expect( itemB2 ).toHaveFocus(); + } ); } ); } ); } From c397ce2e9df91b604a7faff9844b5a6eef4b0525 Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Mon, 4 Dec 2023 23:38:40 +0000 Subject: [PATCH 07/12] Tidying --- .../components/src/composite/test/index.tsx | 153 +++++++++--------- 1 file changed, 79 insertions(+), 74 deletions(-) diff --git a/packages/components/src/composite/test/index.tsx b/packages/components/src/composite/test/index.tsx index 48341992460d02..bfea43a3e1db0e 100644 --- a/packages/components/src/composite/test/index.tsx +++ b/packages/components/src/composite/test/index.tsx @@ -88,62 +88,6 @@ const DEFAULT_STATE = { ...DEFAULT_ACTIONS, }; -function createItemRef( index: number ) { - const node = document.createElement( 'div' ); - node.id = '' + index; - node.compareDocumentPosition = ( other ): number => { - return ( other as HTMLElement ).id < node.id - ? node.DOCUMENT_POSITION_PRECEDING - : node.DOCUMENT_POSITION_FOLLOWING; - }; - const ref: React.MutableRefObject< HTMLElement | null > = createRef(); - ref.current = node; - return ref; -} - -function createItem( stem: string, index: number, groupId?: string ) { - return { - id: `${ stem }-${ index }`, - ref: createItemRef( index ), - groupId, - }; -} - -function initialiseItems( - context: CompositeStateReturn, - stem = 'test', - count = 3 -) { - act( () => { - for ( let index = 1; index <= count; index++ ) { - context.registerItem( createItem( stem, index ) ); - } - } ); -} - -function initialiseGroups( - context: CompositeStateReturn, - stem = 'test', - count = 3 -) { - act( () => { - for ( let index = 1; index <= count; index++ ) { - const id = `${ stem }-group-${ index }`; - context.registerGroup( { id, ref: createRef() } ); - context.registerItem( createItem( stem, index, id ) ); - } - } ); -} - -async function key( code: string, modifier?: string ) { - if ( modifier ) { - return await userEvent.keyboard( - `[${ modifier }>][${ code }][/${ code }]` - ); - } - return await userEvent.keyboard( `[${ code }]` ); -} - describe.each( Object.entries( COMPOSITE_SUITES ) )( 'Validate %s implementation', ( _, { Composite, CompositeGroup, CompositeItem, useCompositeState } ) => { @@ -156,6 +100,58 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( } describe( 'API', () => { + function createItemRef( index: number ) { + const node = document.createElement( 'div' ); + node.id = '' + index; + node.compareDocumentPosition = ( other ): number => { + return ( other as HTMLElement ).id < node.id + ? node.DOCUMENT_POSITION_PRECEDING + : node.DOCUMENT_POSITION_FOLLOWING; + }; + const ref: React.MutableRefObject< HTMLElement | null > = + createRef(); + ref.current = node; + return ref; + } + + function createItem( + stem: string, + index: number, + groupId?: string + ) { + return { + id: `${ stem }-${ index }`, + ref: createItemRef( index ), + groupId, + }; + } + + function initialiseItems( + context: CompositeStateReturn, + stem = 'test', + count = 3 + ) { + act( () => { + for ( let index = 1; index <= count; index++ ) { + context.registerItem( createItem( stem, index ) ); + } + } ); + } + + function initialiseGroups( + context: CompositeStateReturn, + stem = 'test', + count = 3 + ) { + act( () => { + for ( let index = 1; index <= count; index++ ) { + const id = `${ stem }-group-${ index }`; + context.registerGroup( { id, ref: createRef() } ); + context.registerItem( createItem( stem, index, id ) ); + } + } ); + } + describe( 'State', () => { test( 'No initial state', () => { const state = renderState(); @@ -488,9 +484,18 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( } ); describe( 'Usage', () => { - const OneDimensionalTest = ( + async function key( code: string, modifier?: string ) { + if ( modifier ) { + return await userEvent.keyboard( + `[${ modifier }>][${ code }][/${ code }]` + ); + } + return await userEvent.keyboard( `[${ code }]` ); + } + + function OneDimensionalTest( initialState?: CompositeInitialState - ) => { + ) { const composite = useCompositeState( initialState ); return ( @@ -499,26 +504,26 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( Item 3 ); - }; + } - const getOneDimensionalItems = () => { + function getOneDimensionalItems() { return { item1: screen.getByText( 'Item 1' ), item2: screen.getByText( 'Item 2' ), item3: screen.getByText( 'Item 3' ), }; - }; + } - const initialiseOneDimensionalTest = ( + function initialiseOneDimensionalTest( initialState?: CompositeInitialState - ) => { + ) { render( ); return getOneDimensionalItems(); - }; + } - const TwoDimensionalTest = ( + function TwoDimensionalTest( initialState?: CompositeInitialState - ) => { + ) { const composite = useCompositeState( initialState ); return ( @@ -558,9 +563,9 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( ); - }; + } - const getTwoDimensionalItems = () => { + function getTwoDimensionalItems() { return { itemA1: screen.getByText( 'Item A1' ), itemA2: screen.getByText( 'Item A2' ), @@ -572,16 +577,16 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( itemC2: screen.getByText( 'Item C2' ), itemC3: screen.getByText( 'Item C3' ), }; - }; + } - const initialiseTwoDimensionalTest = ( + function initialiseTwoDimensionalTest( initialState?: CompositeInitialState - ) => { + ) { render( ); return getTwoDimensionalItems(); - }; + } - const initialiseShiftTest = ( shift: boolean ) => { + function initialiseShiftTest( shift: boolean ) { const Test = () => { const composite = useCompositeState( { shift } ); @@ -619,7 +624,7 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( itemC1: screen.getByText( 'Item C1' ), itemC2: screen.getByText( 'Item C2' ), }; - }; + } test( 'Renders as a single tab stop', async () => { const Test = () => ( From b6e0d9e43f6b49f0ab44dbad40c0ed1c2f6a0f85 Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Tue, 12 Dec 2023 09:50:08 +0000 Subject: [PATCH 08/12] Removing types and API coverage from tests --- .../components/src/composite/test/index.tsx | 601 +++--------------- packages/components/src/composite/types.ts | 345 ---------- 2 files changed, 75 insertions(+), 871 deletions(-) delete mode 100644 packages/components/src/composite/types.ts diff --git a/packages/components/src/composite/test/index.tsx b/packages/components/src/composite/test/index.tsx index bfea43a3e1db0e..48c39cbe877e5f 100644 --- a/packages/components/src/composite/test/index.tsx +++ b/packages/components/src/composite/test/index.tsx @@ -1,17 +1,9 @@ -// eslint-disable-next-line eslint-comments/disable-enable-pair -/* eslint-disable testing-library/render-result-naming-convention */ - /** * External dependencies */ -import { act, renderHook, render, screen } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -/** - * WordPress dependencies - */ -import { createRef } from '@wordpress/element'; - /** * Internal dependencies */ @@ -21,7 +13,6 @@ import { CompositeItem as ReakitCompositeItem, useCompositeState as ReakitUseCompositeState, } from '..'; -import type { CompositeInitialState, CompositeStateReturn } from '../types'; const COMPOSITE_SUITES = { reakit: { @@ -32,458 +23,46 @@ const COMPOSITE_SUITES = { }, }; -type InitialState = Partial< CompositeInitialState >; - -const DEFAULT_BASE_ID = 'base'; - -const DEFAULT_INITIAL_STATE: CompositeInitialState = { - baseId: DEFAULT_BASE_ID, - currentId: undefined, - loop: false, - orientation: undefined, - rtl: false, - shift: false, - unstable_virtual: false, - wrap: false, -}; - -const DEFAULT_PROPS = { - ...DEFAULT_INITIAL_STATE, - groups: [], - items: [], - unstable_hasActiveWidget: false, - unstable_idCountRef: { current: 0 }, - unstable_includesBaseElement: false, - unstable_moves: 0, -}; - -const DEFAULT_ACTIONS = { - down: expect.any( Function ), - first: expect.any( Function ), - last: expect.any( Function ), - move: expect.any( Function ), - next: expect.any( Function ), - previous: expect.any( Function ), - registerGroup: expect.any( Function ), - registerItem: expect.any( Function ), - reset: expect.any( Function ), - setBaseId: expect.any( Function ), - setCurrentId: expect.any( Function ), - setLoop: expect.any( Function ), - setOrientation: expect.any( Function ), - setRTL: expect.any( Function ), - setShift: expect.any( Function ), - setWrap: expect.any( Function ), - sort: expect.any( Function ), - unregisterGroup: expect.any( Function ), - unregisterItem: expect.any( Function ), - unstable_setHasActiveWidget: expect.any( Function ), - unstable_setIncludesBaseElement: expect.any( Function ), - unstable_setVirtual: expect.any( Function ), - up: expect.any( Function ), -}; +type InitialState = Parameters< typeof ReakitUseCompositeState >[ 0 ]; -const DEFAULT_STATE = { - ...DEFAULT_PROPS, - ...DEFAULT_ACTIONS, -}; +// It was decided not to test the full API, instead opting +// to cover basic usage, with a view to adding broader support +// for the original API should the need arise. As such we are +// only testing here for standard usage. +// See https://github.com/WordPress/gutenberg/pull/56645 describe.each( Object.entries( COMPOSITE_SUITES ) )( 'Validate %s implementation', ( _, { Composite, CompositeGroup, CompositeItem, useCompositeState } ) => { - function renderState( { - baseId = DEFAULT_BASE_ID, - ...additionalState - }: InitialState = {} ) { - const initialState = { baseId, ...additionalState }; - return renderHook( () => useCompositeState( initialState ) ).result; + function useSpreadProps( initialState?: InitialState ) { + return useCompositeState( initialState ); } - describe( 'API', () => { - function createItemRef( index: number ) { - const node = document.createElement( 'div' ); - node.id = '' + index; - node.compareDocumentPosition = ( other ): number => { - return ( other as HTMLElement ).id < node.id - ? node.DOCUMENT_POSITION_PRECEDING - : node.DOCUMENT_POSITION_FOLLOWING; - }; - const ref: React.MutableRefObject< HTMLElement | null > = - createRef(); - ref.current = node; - return ref; - } - - function createItem( - stem: string, - index: number, - groupId?: string - ) { - return { - id: `${ stem }-${ index }`, - ref: createItemRef( index ), - groupId, - }; - } - - function initialiseItems( - context: CompositeStateReturn, - stem = 'test', - count = 3 - ) { - act( () => { - for ( let index = 1; index <= count; index++ ) { - context.registerItem( createItem( stem, index ) ); - } - } ); - } - - function initialiseGroups( - context: CompositeStateReturn, - stem = 'test', - count = 3 - ) { - act( () => { - for ( let index = 1; index <= count; index++ ) { - const id = `${ stem }-group-${ index }`; - context.registerGroup( { id, ref: createRef() } ); - context.registerItem( createItem( stem, index, id ) ); - } - } ); - } - - describe( 'State', () => { - test( 'No initial state', () => { - const state = renderState(); - expect( state.current ).toEqual( DEFAULT_STATE ); - } ); - - test.each( [ - [ '`baseId`', { baseId: 'test' } ], - [ '`currentId`', { currentId: 'test' } ], - [ '`loop` [boolean]', { loop: true } ], - [ '`loop` [horizontal]', { loop: 'horizontal' } ], - [ '`loop` [vertical]', { loop: 'vertical' } ], - [ - '`orientation` [horizontal]', - { orientation: 'horizontal' }, - ], - [ '`orientation` [vertical]', { orientation: 'vertical' } ], - [ '`rtl`', { rtl: true } ], - [ '`shift`', { shift: true } ], - [ '`unstable_virtual`', { unstable_virtual: true } ], - [ '`wrap` [boolean]', { wrap: true } ], - [ '`wrap` [horizontal]', { wrap: 'horizontal' } ], - [ '`wrap` [vertical]', { wrap: 'vertical' } ], - ] )( '%s', ( __, initialState ) => { - const state = renderState( initialState as InitialState ); - - expect( state.current ).toEqual( { - ...DEFAULT_STATE, - ...initialState, - } ); - } ); - } ); - - describe( 'Actions', () => { - describe( 'Basic setters', () => { - test( '`setBaseId`', () => { - const state = renderState(); - act( () => state.current.setBaseId( 'test' ) ); - expect( state.current.baseId ).toBe( 'test' ); - } ); - - test( '`setCurrentId`', () => { - const state = renderState(); - act( () => state.current.setCurrentId( 'test' ) ); - expect( state.current.currentId ).toBe( 'test' ); - } ); - - test( '`setLoop`', () => { - const state = renderState(); - act( () => state.current.setLoop( true ) ); - expect( state.current.loop ).toBe( true ); - } ); - - test( '`setOrientation`', () => { - const state = renderState(); - act( () => - state.current.setOrientation( 'horizontal' ) - ); - expect( state.current.orientation ).toBe( - 'horizontal' - ); - } ); - - test( '`setRTL`', () => { - const state = renderState(); - act( () => state.current.setRTL( true ) ); - expect( state.current.rtl ).toBe( true ); - } ); - - test( '`setShift`', () => { - const state = renderState(); - act( () => state.current.setShift( true ) ); - expect( state.current.shift ).toBe( true ); - } ); - - test( '`setWrap`', () => { - const state = renderState(); - act( () => state.current.setWrap( true ) ); - expect( state.current.wrap ).toBe( true ); - } ); - } ); - - describe( 'Unstable setters', () => { - test( '`unstable_setHasActiveWidget`', () => { - const state = renderState(); - - act( () => - state.current.unstable_setHasActiveWidget( true ) - ); - expect( state.current.unstable_hasActiveWidget ).toBe( - true - ); - - act( () => - state.current.unstable_setHasActiveWidget( false ) - ); - expect( state.current.unstable_hasActiveWidget ).toBe( - false - ); - } ); - - test( '`unstable_setIncludesBaseElement`', () => { - const state = renderState(); - - act( () => - state.current.unstable_setIncludesBaseElement( - true - ) - ); - expect( - state.current.unstable_includesBaseElement - ).toBe( true ); - - act( () => - state.current.unstable_setIncludesBaseElement( - false - ) - ); - expect( - state.current.unstable_includesBaseElement - ).toBe( false ); - } ); - - test( '`unstable_setVirtual`', () => { - const state = renderState(); - - act( () => state.current.unstable_setVirtual( true ) ); - expect( state.current.unstable_virtual ).toBe( true ); - - act( () => state.current.unstable_setVirtual( false ) ); - expect( state.current.unstable_virtual ).toBe( false ); - } ); - } ); - - describe( 'Registration handlers', () => { - test( '`registerItem`', () => { - const state = renderState(); - - act( () => - state.current.registerItem( { - id: 'test', - ref: createRef(), - } ) - ); - - expect( state.current.items ).toMatchObject( [ - { id: 'test' }, - ] ); - } ); - - test( '`registerGroup`', () => { - const state = renderState(); - act( () => - state.current.registerGroup( { - id: 'test', - ref: createRef(), - } ) - ); - - expect( state.current.groups ).toMatchObject( [ - { id: 'test' }, - ] ); - } ); - } ); - - describe( 'Movement', () => { - test( '`move` [specified]', () => { - const state = renderState(); - initialiseItems( state.current ); - act( () => state.current.move( 'test-2' ) ); - - expect( state.current.currentId ).toBe( 'test-2' ); - } ); - - test( '`move` [unspecified]', () => { - const state = renderState(); - initialiseItems( state.current ); - act( () => state.current.move( null ) ); - - expect( state.current.currentId ).toBe( null ); - } ); - - test( '`first`', () => { - const state = renderState(); - initialiseItems( state.current ); - act( () => state.current.first() ); - - expect( state.current.currentId ).toBe( 'test-1' ); - } ); - - test( '`last`', () => { - const state = renderState(); - initialiseItems( state.current ); - act( () => state.current.last() ); - - expect( state.current.currentId ).toBe( 'test-3' ); - } ); - - test( '`previous`', () => { - const state = renderState( { - currentId: 'test-3', - } ); - initialiseItems( state.current ); - act( () => state.current.previous() ); - - expect( state.current.currentId ).toBe( 'test-2' ); - } ); - - test( '`previous` [all the way]', () => { - const state = renderState( { - currentId: 'test-3', - } ); - initialiseItems( state.current ); - act( () => state.current.previous( true ) ); - - expect( state.current.currentId ).toBe( 'test-1' ); - } ); - - test( '`next`', () => { - const state = renderState( { - currentId: 'test-1', - } ); - initialiseItems( state.current ); - act( () => state.current.next() ); - - expect( state.current.currentId ).toBe( 'test-2' ); - } ); - - test( '`next` [all the way]', () => { - const state = renderState( { - currentId: 'test-1', - } ); - initialiseItems( state.current ); - act( () => state.current.next( true ) ); - - expect( state.current.currentId ).toBe( 'test-3' ); - } ); - - test( '`up`', () => { - const state = renderState( { - currentId: 'test-3', - } ); - initialiseGroups( state.current ); - act( () => state.current.up() ); - - expect( state.current.currentId ).toBe( 'test-2' ); - } ); - - test( '`up` [all the way]', () => { - const state = renderState( { - currentId: 'test-3', - } ); - initialiseGroups( state.current ); - act( () => state.current.up( true ) ); - - expect( state.current.currentId ).toBe( 'test-1' ); - } ); - - test( '`down`', () => { - const state = renderState( { - currentId: 'test-1', - } ); - initialiseGroups( state.current ); - act( () => state.current.down() ); - - expect( state.current.currentId ).toBe( 'test-2' ); - } ); - - test( '`down` [all the way]', () => { - const state = renderState( { - currentId: 'test-1', - } ); - initialiseGroups( state.current ); - act( () => state.current.down( true ) ); - - expect( state.current.currentId ).toBe( 'test-3' ); - } ); - } ); - - describe( 'Other', () => { - test( '`sort`', () => { - const state = renderState(); - initialiseItems( state.current ); - - expect( state.current.items ).toMatchObject( [ - { id: 'test-1' }, - { id: 'test-2' }, - { id: 'test-3' }, - ] ); - - for ( const item of state.current.items ) { - if ( item.ref.current ) { - item.ref.current.id = - '' + - ( 4 - parseInt( item.ref.current.id, 10 ) ); - } - } - - act( () => state.current.sort() ); - - expect( state.current.items ).toMatchObject( [ - { id: 'test-3' }, - { id: 'test-2' }, - { id: 'test-1' }, - ] ); - } ); - - test( '`reset`', () => { - const state = renderState(); - - act( () => { - state.current.setLoop( true ); - state.current.setCurrentId( 'test' ); - } ); - - expect( state.current ).toMatchObject( { - loop: true, - currentId: 'test', - } ); - - act( () => state.current.reset() ); + function useStateProps( initialState?: InitialState ) { + return { + state: useCompositeState( initialState ), + }; + } - expect( state.current ).toMatchObject( { - loop: false, - currentId: undefined, - } ); - } ); - } ); - } ); - } ); + function useCustomProps( initialState?: InitialState ) { + const state = useCompositeState( initialState ); + const { up, down, previous, next, move } = state; + + return { + ...state, + up: jest.fn( up ), + down: jest.fn( down ), + previous: jest.fn( previous ), + next: jest.fn( next ), + move: jest.fn( move ), + }; + } - describe( 'Usage', () => { + describe.each( [ + [ 'With "spread" state', useSpreadProps ], + [ 'With `state` prop', useStateProps ], + [ 'With custom props', useCustomProps ], + ] )( '%s', ( __, useProps ) => { async function key( code: string, modifier?: string ) { if ( modifier ) { return await userEvent.keyboard( @@ -493,15 +72,13 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( return await userEvent.keyboard( `[${ code }]` ); } - function OneDimensionalTest( - initialState?: CompositeInitialState - ) { - const composite = useCompositeState( initialState ); + function OneDimensionalTest( initialState?: InitialState ) { + const props = useProps( initialState ); return ( - - Item 1 - Item 2 - Item 3 + + Item 1 + Item 2 + Item 3 ); } @@ -515,51 +92,31 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( } function initialiseOneDimensionalTest( - initialState?: CompositeInitialState + initialState?: InitialState ) { render( ); return getOneDimensionalItems(); } - function TwoDimensionalTest( - initialState?: CompositeInitialState - ) { - const composite = useCompositeState( initialState ); + function TwoDimensionalTest( initialState?: InitialState ) { + const props = useProps( initialState ); return ( - - - - Item A1 - - - Item A2 - - - Item A3 - + + + Item A1 + Item A2 + Item A3 - - - Item B1 - - - Item B2 - - - Item B3 - + + Item B1 + Item B2 + Item B3 - - - Item C1 - - - Item C2 - - - Item C3 - + + Item C1 + Item C2 + Item C3 ); @@ -580,7 +137,7 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( } function initialiseTwoDimensionalTest( - initialState?: CompositeInitialState + initialState?: InitialState ) { render( ); return getTwoDimensionalItems(); @@ -588,28 +145,28 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( function initialiseShiftTest( shift: boolean ) { const Test = () => { - const composite = useCompositeState( { shift } ); + const props = useProps( { shift } ); return ( - - - + + + Item A1 - - + + Item B1 - + Item B2 - - + + Item C1 - + Item C2 @@ -648,18 +205,14 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( test( 'Excludes disabled items', async () => { const Test = () => { - const composite = useCompositeState(); + const props = useProps(); return ( - - - Item 1 - - + + Item 1 + Item 2 - - Item 3 - + Item 3 ); }; @@ -678,18 +231,14 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( test( 'Includes focusable disabled items', async () => { const Test = () => { - const composite = useCompositeState(); + const props = useProps(); return ( - - - Item 1 - - + + Item 1 + Item 2 - - Item 3 - + Item 3 ); }; diff --git a/packages/components/src/composite/types.ts b/packages/components/src/composite/types.ts deleted file mode 100644 index c76675f80c3d00..00000000000000 --- a/packages/components/src/composite/types.ts +++ /dev/null @@ -1,345 +0,0 @@ -type IdActions = { - /** - * Sets `baseId`. - */ - setBaseId: React.Dispatch< React.SetStateAction< string > >; -}; - -type IdState = { - baseId: string; - /** - * @private - */ - unstable_idCountRef: React.MutableRefObject< number >; -}; - -type IdInitialState = Partial< Pick< IdState, 'baseId' > >; - -type IdStateReturn = IdState & IdActions; - -type Group = { - id: string; - ref: React.RefObject< HTMLElement >; -}; - -type Item = { - id: string | null; - ref: React.RefObject< HTMLElement >; - groupId?: Group[ 'id' ]; - disabled?: boolean; -}; - -type Orientation = 'horizontal' | 'vertical'; - -type CompositeState = IdState & { - /** - * If enabled, the composite element will act as an - * [aria-activedescendant](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_focus_activedescendant) - * container instead of - * [roving tabindex](https://www.w3.org/TR/wai-aria-practices/#kbd_roving_tabindex). - * DOM focus will remain on the composite while its items receive virtual focus. - * @default false - */ - unstable_virtual: boolean; - /** - * Determines how `next` and `previous` functions will behave. If `rtl` is - * set to `true`, they will be inverted. This only affects the composite - * widget behavior. You still need to set `dir="rtl"` on HTML/CSS. - * @default false - */ - rtl: boolean; - /** - * Defines the orientation of the composite widget. If the composite has a - * single row or column (one-dimensional), the `orientation` value determines - * which arrow keys can be used to move focus: - * - `undefined`: all arrow keys work. - * - `horizontal`: only left and right arrow keys work. - * - `vertical`: only up and down arrow keys work. - * - * It doesn't have any effect on two-dimensional composites. - * @default undefined - */ - orientation?: Orientation; - /** - * Lists all the composite items with their `id`, DOM `ref`, `disabled` state - * and `groupId` if any. This state is automatically updated when - * `registerItem` and `unregisterItem` are called. - * @example - * const composite = useCompositeState(); - * composite.items.forEach((item) => { - * const { id, ref, disabled, groupId } = item; - * ... - * }); - */ - items: Item[]; - /** - * Lists all the composite groups with their `id` and DOM `ref`. This state - * is automatically updated when `registerGroup` and `unregisterGroup` are - * called. - * @example - * const composite = useCompositeState(); - * composite.groups.forEach((group) => { - * const { id, ref } = group; - * ... - * }); - */ - groups: Group[]; - /** - * The current focused item `id`. - * - `undefined` will automatically focus the first enabled composite item. - * - `null` will focus the base composite element and users will be able to - * navigate out of it using arrow keys. - * - If `currentId` is initially set to `null`, the base composite element - * itself will have focus and users will be able to navigate to it using - * arrow keys. - * @default undefined - * @example - * // First enabled item has initial focus - * useCompositeState(); - * // Base composite element has initial focus - * useCompositeState({ currentId: null }); - * // Specific composite item element has initial focus - * useCompositeState({ currentId: "item-id" }); - */ - currentId?: string | null; - /** - * On one-dimensional composites: - * - `true` loops from the last item to the first item and vice-versa. - * - `horizontal` loops only if `orientation` is `horizontal` or not set. - * - `vertical` loops only if `orientation` is `vertical` or not set. - * - If `currentId` is initially set to `null`, the composite element will - * be focused in between the last and first items. - * - * On two-dimensional composites: - * - `true` loops from the last row/column item to the first item in the - * same row/column and vice-versa. If it's the last item in the last row, it - * moves to the first item in the first row and vice-versa. - * - `horizontal` loops only from the last row item to the first item in - * the same row. - * - `vertical` loops only from the last column item to the first item in - * the column row. - * - If `currentId` is initially set to `null`, vertical loop will have no - * effect as moving down from the last row or up from the first row will - * focus the composite element. - * - If `wrap` matches the value of `loop`, it'll wrap between the last - * item in the last row or column and the first item in the first row or - * column and vice-versa. - * @default false - */ - loop: boolean | Orientation; - /** - * **Has effect only on two-dimensional composites**. If enabled, moving to - * the next item from the last one in a row or column will focus the first - * item in the next row or column and vice-versa. - * - `true` wraps between rows and columns. - * - `horizontal` wraps only between rows. - * - `vertical` wraps only between columns. - * - If `loop` matches the value of `wrap`, it'll wrap between the last - * item in the last row or column and the first item in the first row or - * column and vice-versa. - * @default false - */ - wrap: boolean | Orientation; - /** - * **Has effect only on two-dimensional composites**. If enabled, moving up - * or down when there's no next item or the next item is disabled will shift - * to the item right before it. - * @default false - */ - shift: boolean; - /** - * Stores the number of moves that have been performed by calling `move`, - * `next`, `previous`, `up`, `down`, `first` or `last`. - * @default 0 - */ - unstable_moves: number; - /** - * @default false - * @private - */ - unstable_hasActiveWidget: boolean; - /** - * @default false - * @private - */ - unstable_includesBaseElement: boolean; -}; - -export type CompositeInitialState = IdInitialState & - Partial< - Pick< - CompositeState, - | 'unstable_virtual' - | 'rtl' - | 'orientation' - | 'currentId' - | 'loop' - | 'wrap' - | 'shift' - | 'unstable_includesBaseElement' - > - >; - -type CompositeActions = IdActions & { - /** - * Registers a composite item. - * @example - * const ref = React.useRef(); - * const composite = useCompositeState(); - * React.useEffect(() => { - * composite.registerItem({ ref, id: "id" }); - * return () => composite.unregisterItem("id"); - * }, []); - */ - registerItem: ( item: Item ) => void; - /** - * Unregisters a composite item. - * @example - * const ref = React.useRef(); - * const composite = useCompositeState(); - * React.useEffect(() => { - * composite.registerItem({ ref, id: "id" }); - * return () => composite.unregisterItem("id"); - * }, []); - */ - unregisterItem: ( id: string ) => void; - /** - * Registers a composite group. - * @example - * const ref = React.useRef(); - * const composite = useCompositeState(); - * React.useEffect(() => { - * composite.registerGroup({ ref, id: "id" }); - * return () => composite.unregisterGroup("id"); - * }, []); - */ - registerGroup: ( group: Group ) => void; - /** - * Unregisters a composite group. - * @example - * const ref = React.useRef(); - * const composite = useCompositeState(); - * React.useEffect(() => { - * composite.registerGroup({ ref, id: "id" }); - * return () => composite.unregisterGroup("id"); - * }, []); - */ - unregisterGroup: ( id: string ) => void; - /** - * Moves focus to a given item ID. - * @example - * const composite = useCompositeState(); - * composite.move("item-2"); // focus item 2 - */ - move: ( id: string | null ) => void; - /** - * Moves focus to the next item. - */ - next: ( unstable_allTheWay?: boolean ) => void; - /** - * Moves focus to the previous item. - */ - previous: ( unstable_allTheWay?: boolean ) => void; - /** - * Moves focus to the item above. - */ - up: ( unstable_allTheWay?: boolean ) => void; - /** - * Moves focus to the item below. - */ - down: ( unstable_allTheWay?: boolean ) => void; - /** - * Moves focus to the first item. - */ - first: () => void; - /** - * Moves focus to the last item. - */ - last: () => void; - /** - * Sorts the `composite.items` based on the items position in the DOM. This - * is especially useful after modifying the composite items order in the DOM. - * Most of the time, though, you don't need to manually call this function as - * the re-ordering happens automatically. - */ - sort: () => void; - /** - * Sets `virtual`. - */ - unstable_setVirtual: React.Dispatch< - React.SetStateAction< CompositeState[ 'unstable_virtual' ] > - >; - /** - * Sets `rtl`. - * @example - * const composite = useCompositeState({ rtl: true }); - * composite.setRTL(false); - */ - setRTL: React.Dispatch< React.SetStateAction< CompositeState[ 'rtl' ] > >; - /** - * Sets `orientation`. - */ - setOrientation: React.Dispatch< - React.SetStateAction< CompositeState[ 'orientation' ] > - >; - /** - * Sets `currentId`. This is different from `composite.move` as this only - * updates the `currentId` state without moving focus. When the composite - * widget gets focused by the user, the item referred by the `currentId` - * state will get focus. - * @example - * const composite = useCompositeState({ currentId: "item-1" }); - * // Updates `composite.currentId` to `item-2` - * composite.setCurrentId("item-2"); - */ - setCurrentId: React.Dispatch< - React.SetStateAction< CompositeState[ 'currentId' ] > - >; - /** - * Sets `loop`. - */ - setLoop: React.Dispatch< React.SetStateAction< CompositeState[ 'loop' ] > >; - /** - * Sets `wrap`. - */ - setWrap: React.Dispatch< React.SetStateAction< CompositeState[ 'wrap' ] > >; - /** - * Sets `shift`. - */ - setShift: React.Dispatch< - React.SetStateAction< CompositeState[ 'shift' ] > - >; - /** - * Resets to initial state. - * @example - * // On initial render, currentId will be item-1 and loop will be true - * const composite = useCompositeState({ - * currentId: "item-1", - * loop: true, - * }); - * // On next render, currentId will be item-2 and loop will be false - * composite.setCurrentId("item-2"); - * composite.setLoop(false); - * // On next render, currentId will be item-1 and loop will be true - * composite.reset(); - */ - reset: () => void; - /** - * Sets `includesBaseElement`. - * @private - */ - unstable_setIncludesBaseElement: React.Dispatch< - React.SetStateAction< CompositeState[ 'unstable_includesBaseElement' ] > - >; - /** - * Sets `hasActiveWidget`. - * @private - */ - unstable_setHasActiveWidget: React.Dispatch< - React.SetStateAction< CompositeState[ 'unstable_hasActiveWidget' ] > - >; -}; - -export type CompositeStateReturn = IdStateReturn & - CompositeState & - CompositeActions; From 4bc54cba7243fe25395fdc8b5f8507e607c0692c Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Wed, 13 Dec 2023 13:20:26 +0000 Subject: [PATCH 09/12] Using `userEvent.setup()` rather than `userEvent` directly --- .../components/src/composite/test/index.tsx | 232 +++++++++--------- 1 file changed, 118 insertions(+), 114 deletions(-) diff --git a/packages/components/src/composite/test/index.tsx b/packages/components/src/composite/test/index.tsx index 48c39cbe877e5f..6b2521bed3cfda 100644 --- a/packages/components/src/composite/test/index.tsx +++ b/packages/components/src/composite/test/index.tsx @@ -63,15 +63,6 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( [ 'With `state` prop', useStateProps ], [ 'With custom props', useCustomProps ], ] )( '%s', ( __, useProps ) => { - async function key( code: string, modifier?: string ) { - if ( modifier ) { - return await userEvent.keyboard( - `[${ modifier }>][${ code }][/${ code }]` - ); - } - return await userEvent.keyboard( `[${ code }]` ); - } - function OneDimensionalTest( initialState?: InitialState ) { const props = useProps( initialState ); return ( @@ -184,6 +175,7 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( } test( 'Renders as a single tab stop', async () => { + const user = userEvent.setup(); const Test = () => ( <> @@ -193,17 +185,18 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( ); render( ); - await userEvent.tab(); + await user.tab(); expect( screen.getByText( 'Before' ) ).toHaveFocus(); - await userEvent.tab(); + await user.tab(); expect( screen.getByText( 'Item 1' ) ).toHaveFocus(); - await userEvent.tab(); + await user.tab(); expect( screen.getByText( 'After' ) ).toHaveFocus(); - await userEvent.tab( { shift: true } ); + await user.tab( { shift: true } ); expect( screen.getByText( 'Item 1' ) ).toHaveFocus(); } ); test( 'Excludes disabled items', async () => { + const user = userEvent.setup(); const Test = () => { const props = useProps(); return ( @@ -222,14 +215,15 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( expect( item2 ).toBeDisabled(); - await userEvent.tab(); + await user.tab(); expect( item1 ).toHaveFocus(); - await key( 'ArrowDown' ); + await user.keyboard( '[ArrowDown]' ); expect( item2 ).not.toHaveFocus(); expect( item3 ).toHaveFocus(); } ); test( 'Includes focusable disabled items', async () => { + const user = userEvent.setup(); const Test = () => { const props = useProps(); return ( @@ -248,135 +242,140 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( expect( item2 ).toBeEnabled(); expect( item2 ).toHaveAttribute( 'aria-disabled', 'true' ); - await userEvent.tab(); + await user.tab(); expect( item1 ).toHaveFocus(); - await key( 'ArrowDown' ); + await user.keyboard( '[ArrowDown]' ); expect( item2 ).toHaveFocus(); expect( item3 ).not.toHaveFocus(); } ); describe( 'In one dimension', () => { test( 'All directions work with no orientation', async () => { + const user = userEvent.setup(); const { item1, item2, item3 } = initialiseOneDimensionalTest(); - await userEvent.tab(); + await user.tab(); expect( item1 ).toHaveFocus(); - await key( 'ArrowDown' ); + await user.keyboard( '[ArrowDown]' ); expect( item2 ).toHaveFocus(); - await key( 'ArrowDown' ); + await user.keyboard( '[ArrowDown]' ); expect( item3 ).toHaveFocus(); - await key( 'ArrowDown' ); + await user.keyboard( '[ArrowDown]' ); expect( item3 ).toHaveFocus(); - await key( 'ArrowUp' ); + await user.keyboard( '[ArrowUp]' ); expect( item2 ).toHaveFocus(); - await key( 'ArrowUp' ); + await user.keyboard( '[ArrowUp]' ); expect( item1 ).toHaveFocus(); - await key( 'ArrowUp' ); + await user.keyboard( '[ArrowUp]' ); expect( item1 ).toHaveFocus(); - await key( 'ArrowRight' ); + await user.keyboard( '[ArrowRight]' ); expect( item2 ).toHaveFocus(); - await key( 'ArrowRight' ); + await user.keyboard( '[ArrowRight]' ); expect( item3 ).toHaveFocus(); - await key( 'ArrowLeft' ); + await user.keyboard( '[ArrowLeft]' ); expect( item2 ).toHaveFocus(); - await key( 'ArrowLeft' ); + await user.keyboard( '[ArrowLeft]' ); expect( item1 ).toHaveFocus(); - await key( 'End' ); + await user.keyboard( '[End]' ); expect( item3 ).toHaveFocus(); - await key( 'Home' ); + await user.keyboard( '[Home]' ); expect( item1 ).toHaveFocus(); - await key( 'PageDown' ); + await user.keyboard( '[PageDown]' ); expect( item3 ).toHaveFocus(); - await key( 'PageUp' ); + await user.keyboard( '[PageUp]' ); expect( item1 ).toHaveFocus(); } ); test( 'Only left/right work with horizontal orientation', async () => { + const user = userEvent.setup(); const { item1, item2, item3 } = initialiseOneDimensionalTest( { orientation: 'horizontal', } ); - await userEvent.tab(); + await user.tab(); expect( item1 ).toHaveFocus(); - await key( 'ArrowDown' ); + await user.keyboard( '[ArrowDown]' ); expect( item1 ).toHaveFocus(); - await key( 'ArrowRight' ); + await user.keyboard( '[ArrowRight]' ); expect( item2 ).toHaveFocus(); - await key( 'ArrowRight' ); + await user.keyboard( '[ArrowRight]' ); expect( item3 ).toHaveFocus(); - await key( 'ArrowUp' ); + await user.keyboard( '[ArrowUp]' ); expect( item3 ).toHaveFocus(); - await key( 'ArrowLeft' ); + await user.keyboard( '[ArrowLeft]' ); expect( item2 ).toHaveFocus(); - await key( 'ArrowLeft' ); + await user.keyboard( '[ArrowLeft]' ); expect( item1 ).toHaveFocus(); - await key( 'End' ); + await user.keyboard( '[End]' ); expect( item3 ).toHaveFocus(); - await key( 'Home' ); + await user.keyboard( '[Home]' ); expect( item1 ).toHaveFocus(); - await key( 'PageDown' ); + await user.keyboard( '[PageDown]' ); expect( item3 ).toHaveFocus(); - await key( 'PageUp' ); + await user.keyboard( '[PageUp]' ); expect( item1 ).toHaveFocus(); } ); test( 'Only up/down work with vertical orientation', async () => { + const user = userEvent.setup(); const { item1, item2, item3 } = initialiseOneDimensionalTest( { orientation: 'vertical', } ); - await userEvent.tab(); + await user.tab(); expect( item1 ).toHaveFocus(); - await key( 'ArrowRight' ); + await user.keyboard( '[ArrowRight]' ); expect( item1 ).toHaveFocus(); - await key( 'ArrowDown' ); + await user.keyboard( '[ArrowDown]' ); expect( item2 ).toHaveFocus(); - await key( 'ArrowDown' ); + await user.keyboard( '[ArrowDown]' ); expect( item3 ).toHaveFocus(); - await key( 'ArrowLeft' ); + await user.keyboard( '[ArrowLeft]' ); expect( item3 ).toHaveFocus(); - await key( 'ArrowUp' ); + await user.keyboard( '[ArrowUp]' ); expect( item2 ).toHaveFocus(); - await key( 'ArrowUp' ); + await user.keyboard( '[ArrowUp]' ); expect( item1 ).toHaveFocus(); - await key( 'End' ); + await user.keyboard( '[End]' ); expect( item3 ).toHaveFocus(); - await key( 'Home' ); + await user.keyboard( '[Home]' ); expect( item1 ).toHaveFocus(); - await key( 'PageDown' ); + await user.keyboard( '[PageDown]' ); expect( item3 ).toHaveFocus(); - await key( 'PageUp' ); + await user.keyboard( '[PageUp]' ); expect( item1 ).toHaveFocus(); } ); test( 'Focus wraps with loop enabled', async () => { + const user = userEvent.setup(); const { item1, item2, item3 } = initialiseOneDimensionalTest( { loop: true, } ); - await userEvent.tab(); + await user.tab(); expect( item1 ).toHaveFocus(); - await key( 'ArrowDown' ); + await user.keyboard( '[ArrowDown]' ); expect( item2 ).toHaveFocus(); - await key( 'ArrowDown' ); + await user.keyboard( '[ArrowDown]' ); expect( item3 ).toHaveFocus(); - await key( 'ArrowDown' ); + await user.keyboard( '[ArrowDown]' ); expect( item1 ).toHaveFocus(); - await key( 'ArrowUp' ); + await user.keyboard( '[ArrowUp]' ); expect( item3 ).toHaveFocus(); - await key( 'ArrowRight' ); + await user.keyboard( '[ArrowRight]' ); expect( item1 ).toHaveFocus(); - await key( 'ArrowLeft' ); + await user.keyboard( '[ArrowLeft]' ); expect( item3 ).toHaveFocus(); } ); } ); describe( 'In two dimensions', () => { test( 'All directions work as standard', async () => { + const user = userEvent.setup(); const { itemA1, itemA2, @@ -387,146 +386,151 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( itemC3, } = initialiseTwoDimensionalTest(); - await userEvent.tab(); + await user.tab(); expect( itemA1 ).toHaveFocus(); - await key( 'ArrowUp' ); + await user.keyboard( '[ArrowUp]' ); expect( itemA1 ).toHaveFocus(); - await key( 'ArrowLeft' ); + await user.keyboard( '[ArrowLeft]' ); expect( itemA1 ).toHaveFocus(); - await key( 'ArrowDown' ); + await user.keyboard( '[ArrowDown]' ); expect( itemB1 ).toHaveFocus(); - await key( 'ArrowRight' ); + await user.keyboard( '[ArrowRight]' ); expect( itemB2 ).toHaveFocus(); - await key( 'ArrowUp' ); + await user.keyboard( '[ArrowUp]' ); expect( itemA2 ).toHaveFocus(); - await key( 'ArrowLeft' ); + await user.keyboard( '[ArrowLeft]' ); expect( itemA1 ).toHaveFocus(); - await key( 'End' ); + await user.keyboard( '[End]' ); expect( itemA3 ).toHaveFocus(); - await key( 'PageDown' ); + await user.keyboard( '[PageDown]' ); expect( itemC3 ).toHaveFocus(); - await key( 'ArrowRight' ); + await user.keyboard( '[ArrowRight]' ); expect( itemC3 ).toHaveFocus(); - await key( 'ArrowDown' ); + await user.keyboard( '[ArrowDown]' ); expect( itemC3 ).toHaveFocus(); - await key( 'Home' ); + await user.keyboard( '[Home]' ); expect( itemC1 ).toHaveFocus(); - await key( 'PageUp' ); + await user.keyboard( '[PageUp]' ); expect( itemA1 ).toHaveFocus(); - await key( 'End', 'ControlLeft' ); + await user.keyboard( '{Control>}[End]{/Control}' ); expect( itemC3 ).toHaveFocus(); - await key( 'Home', 'ControlLeft' ); + await user.keyboard( '{Control>}[Home]{/Control}' ); expect( itemA1 ).toHaveFocus(); } ); test( 'Focus wraps around rows/columns with loop enabled', async () => { + const user = userEvent.setup(); const { itemA1, itemA2, itemA3, itemB1, itemC1, itemC3 } = initialiseTwoDimensionalTest( { loop: true } ); - await userEvent.tab(); + await user.tab(); expect( itemA1 ).toHaveFocus(); - await key( 'ArrowRight' ); + await user.keyboard( '[ArrowRight]' ); expect( itemA2 ).toHaveFocus(); - await key( 'ArrowRight' ); + await user.keyboard( '[ArrowRight]' ); expect( itemA3 ).toHaveFocus(); - await key( 'ArrowRight' ); + await user.keyboard( '[ArrowRight]' ); expect( itemA1 ).toHaveFocus(); - await key( 'ArrowDown' ); + await user.keyboard( '[ArrowDown]' ); expect( itemB1 ).toHaveFocus(); - await key( 'ArrowDown' ); + await user.keyboard( '[ArrowDown]' ); expect( itemC1 ).toHaveFocus(); - await key( 'ArrowDown' ); + await user.keyboard( '[ArrowDown]' ); expect( itemA1 ).toHaveFocus(); - await key( 'ArrowLeft' ); + await user.keyboard( '[ArrowLeft]' ); expect( itemA3 ).toHaveFocus(); - await key( 'ArrowUp' ); + await user.keyboard( '[ArrowUp]' ); expect( itemC3 ).toHaveFocus(); } ); test( 'Focus moves between rows/columns with wrap enabled', async () => { + const user = userEvent.setup(); const { itemA1, itemA2, itemA3, itemB1, itemC1, itemC3 } = initialiseTwoDimensionalTest( { wrap: true } ); - await userEvent.tab(); + await user.tab(); expect( itemA1 ).toHaveFocus(); - await key( 'ArrowRight' ); + await user.keyboard( '[ArrowRight]' ); expect( itemA2 ).toHaveFocus(); - await key( 'ArrowRight' ); + await user.keyboard( '[ArrowRight]' ); expect( itemA3 ).toHaveFocus(); - await key( 'ArrowRight' ); + await user.keyboard( '[ArrowRight]' ); expect( itemB1 ).toHaveFocus(); - await key( 'ArrowDown' ); + await user.keyboard( '[ArrowDown]' ); expect( itemC1 ).toHaveFocus(); - await key( 'ArrowDown' ); + await user.keyboard( '[ArrowDown]' ); expect( itemA2 ).toHaveFocus(); - await key( 'ArrowLeft' ); + await user.keyboard( '[ArrowLeft]' ); expect( itemA1 ).toHaveFocus(); - await key( 'ArrowLeft' ); + await user.keyboard( '[ArrowLeft]' ); expect( itemA1 ).toHaveFocus(); - await key( 'ArrowUp' ); + await user.keyboard( '[ArrowUp]' ); expect( itemA1 ).toHaveFocus(); - await key( 'End', 'ControlLeft' ); + await user.keyboard( '{Control>}[End]{/Control}' ); expect( itemC3 ).toHaveFocus(); - await key( 'ArrowRight' ); + await user.keyboard( '[ArrowRight]' ); expect( itemC3 ).toHaveFocus(); - await key( 'ArrowDown' ); + await user.keyboard( '[ArrowDown]' ); expect( itemC3 ).toHaveFocus(); } ); test( 'Focus wraps around start/end with loop and wrap enabled', async () => { + const user = userEvent.setup(); const { itemA1, itemC3 } = initialiseTwoDimensionalTest( { loop: true, wrap: true, } ); - await userEvent.tab(); + await user.tab(); expect( itemA1 ).toHaveFocus(); - await key( 'ArrowLeft' ); + await user.keyboard( '[ArrowLeft]' ); expect( itemC3 ).toHaveFocus(); - await key( 'ArrowDown' ); + await user.keyboard( '[ArrowDown]' ); expect( itemA1 ).toHaveFocus(); - await key( 'ArrowUp' ); + await user.keyboard( '[ArrowUp]' ); expect( itemC3 ).toHaveFocus(); - await key( 'ArrowRight' ); + await user.keyboard( '[ArrowRight]' ); expect( itemA1 ).toHaveFocus(); } ); test( 'Focus shifts if vertical neighbour unavailable when shift enabled', async () => { + const user = userEvent.setup(); const { itemA1, itemB1, itemB2, itemC1 } = initialiseShiftTest( true ); - await userEvent.tab(); + await user.tab(); expect( itemA1 ).toHaveFocus(); - await key( 'ArrowDown' ); + await user.keyboard( '[ArrowDown]' ); expect( itemB1 ).toHaveFocus(); - await key( 'ArrowRight' ); + await user.keyboard( '[ArrowRight]' ); expect( itemB2 ).toHaveFocus(); - await key( 'ArrowUp' ); + await user.keyboard( '[ArrowUp]' ); // A2 doesn't exist expect( itemA1 ).toHaveFocus(); - await key( 'ArrowDown' ); + await user.keyboard( '[ArrowDown]' ); expect( itemB1 ).toHaveFocus(); - await key( 'ArrowRight' ); + await user.keyboard( '[ArrowRight]' ); expect( itemB2 ).toHaveFocus(); - await key( 'ArrowDown' ); + await user.keyboard( '[ArrowDown]' ); // C2 is disabled expect( itemC1 ).toHaveFocus(); } ); test( 'Focus does not shift if vertical neighbour unavailable when shift not enabled', async () => { + const user = userEvent.setup(); const { itemA1, itemB1, itemB2 } = initialiseShiftTest( false ); - await userEvent.tab(); + await user.tab(); expect( itemA1 ).toHaveFocus(); - await key( 'ArrowDown' ); + await user.keyboard( '[ArrowDown]' ); expect( itemB1 ).toHaveFocus(); - await key( 'ArrowRight' ); + await user.keyboard( '[ArrowRight]' ); expect( itemB2 ).toHaveFocus(); - await key( 'ArrowUp' ); + await user.keyboard( '[ArrowUp]' ); // A2 doesn't exist expect( itemB2 ).toHaveFocus(); - await key( 'ArrowDown' ); + await user.keyboard( '[ArrowDown]' ); // C2 is disabled expect( itemB2 ).toHaveFocus(); } ); From dff2367c20f907c5ff5acc411db7bb9fc7d78046 Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Wed, 13 Dec 2023 14:12:18 +0000 Subject: [PATCH 10/12] Adding tests for `baseId`, `currentId` and `rtl` --- .../components/src/composite/test/index.tsx | 596 ++++++++++-------- 1 file changed, 325 insertions(+), 271 deletions(-) diff --git a/packages/components/src/composite/test/index.tsx b/packages/components/src/composite/test/index.tsx index 6b2521bed3cfda..e156359ad2e32e 100644 --- a/packages/components/src/composite/test/index.tsx +++ b/packages/components/src/composite/test/index.tsx @@ -134,9 +134,9 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( return getTwoDimensionalItems(); } - function initialiseShiftTest( shift: boolean ) { + function initialiseShiftTest( shift: boolean, rtl: boolean ) { const Test = () => { - const props = useProps( { shift } ); + const props = useProps( { rtl, shift } ); return ( @@ -249,290 +249,344 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( expect( item3 ).not.toHaveFocus(); } ); - describe( 'In one dimension', () => { - test( 'All directions work with no orientation', async () => { - const user = userEvent.setup(); - const { item1, item2, item3 } = - initialiseOneDimensionalTest(); - - await user.tab(); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item2 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( item2 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[ArrowRight]' ); - expect( item2 ).toHaveFocus(); - await user.keyboard( '[ArrowRight]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[ArrowLeft]' ); - expect( item2 ).toHaveFocus(); - await user.keyboard( '[ArrowLeft]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[End]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[Home]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[PageDown]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[PageUp]' ); - expect( item1 ).toHaveFocus(); + test( 'Supports `baseId`', async () => { + const { item1, item2, item3 } = initialiseOneDimensionalTest( { + baseId: 'test-id', } ); - test( 'Only left/right work with horizontal orientation', async () => { - const user = userEvent.setup(); - const { item1, item2, item3 } = - initialiseOneDimensionalTest( { - orientation: 'horizontal', - } ); - - await user.tab(); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[ArrowRight]' ); - expect( item2 ).toHaveFocus(); - await user.keyboard( '[ArrowRight]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[ArrowLeft]' ); - expect( item2 ).toHaveFocus(); - await user.keyboard( '[ArrowLeft]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[End]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[Home]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[PageDown]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[PageUp]' ); - expect( item1 ).toHaveFocus(); - } ); + expect( item1.id ).toMatch( 'test-id-1' ); + expect( item2.id ).toMatch( 'test-id-2' ); + expect( item3.id ).toMatch( 'test-id-3' ); + } ); - test( 'Only up/down work with vertical orientation', async () => { - const user = userEvent.setup(); - const { item1, item2, item3 } = - initialiseOneDimensionalTest( { - orientation: 'vertical', - } ); - - await user.tab(); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[ArrowRight]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item2 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[ArrowLeft]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( item2 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[End]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[Home]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[PageDown]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[PageUp]' ); - expect( item1 ).toHaveFocus(); + test( 'Supports `currentId`', async () => { + const user = userEvent.setup(); + const { item2 } = initialiseOneDimensionalTest( { + baseId: 'test-id', + currentId: 'test-id-2', } ); - test( 'Focus wraps with loop enabled', async () => { - const user = userEvent.setup(); - const { item1, item2, item3 } = - initialiseOneDimensionalTest( { - loop: true, - } ); - - await user.tab(); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item2 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[ArrowRight]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[ArrowLeft]' ); - expect( item3 ).toHaveFocus(); - } ); + await user.tab(); + expect( item2 ).toHaveFocus(); } ); - describe( 'In two dimensions', () => { - test( 'All directions work as standard', async () => { - const user = userEvent.setup(); - const { - itemA1, - itemA2, - itemA3, - itemB1, - itemB2, - itemC1, - itemC3, - } = initialiseTwoDimensionalTest(); - - await user.tab(); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowLeft]' ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemB1 ).toHaveFocus(); - await user.keyboard( '[ArrowRight]' ); - expect( itemB2 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( itemA2 ).toHaveFocus(); - await user.keyboard( '[ArrowLeft]' ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[End]' ); - expect( itemA3 ).toHaveFocus(); - await user.keyboard( '[PageDown]' ); - expect( itemC3 ).toHaveFocus(); - await user.keyboard( '[ArrowRight]' ); - expect( itemC3 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemC3 ).toHaveFocus(); - await user.keyboard( '[Home]' ); - expect( itemC1 ).toHaveFocus(); - await user.keyboard( '[PageUp]' ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '{Control>}[End]{/Control}' ); - expect( itemC3 ).toHaveFocus(); - await user.keyboard( '{Control>}[Home]{/Control}' ); - expect( itemA1 ).toHaveFocus(); - } ); + describe.each( [ + [ + 'When LTR', + false, + { previous: 'ArrowLeft', next: 'ArrowRight' }, + ], + [ + 'When RTL', + true, + { previous: 'ArrowRight', next: 'ArrowLeft' }, + ], + ] )( '%s', ( _when, rtl, { previous, next } ) => { + describe( 'In one dimension', () => { + test( 'All directions work with no orientation', async () => { + const user = userEvent.setup(); + const { item1, item2, item3 } = + initialiseOneDimensionalTest( { rtl } ); + + await user.tab(); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item2 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( item2 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( item1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( item2 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( item3 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( item2 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[End]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[Home]' ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[PageDown]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[PageUp]' ); + expect( item1 ).toHaveFocus(); + } ); - test( 'Focus wraps around rows/columns with loop enabled', async () => { - const user = userEvent.setup(); - const { itemA1, itemA2, itemA3, itemB1, itemC1, itemC3 } = - initialiseTwoDimensionalTest( { loop: true } ); - - await user.tab(); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowRight]' ); - expect( itemA2 ).toHaveFocus(); - await user.keyboard( '[ArrowRight]' ); - expect( itemA3 ).toHaveFocus(); - await user.keyboard( '[ArrowRight]' ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemB1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemC1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowLeft]' ); - expect( itemA3 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( itemC3 ).toHaveFocus(); - } ); + test( 'Only left/right work with horizontal orientation', async () => { + const user = userEvent.setup(); + const { item1, item2, item3 } = + initialiseOneDimensionalTest( { + rtl, + orientation: 'horizontal', + } ); + + await user.tab(); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( item2 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( item2 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[End]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[Home]' ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[PageDown]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[PageUp]' ); + expect( item1 ).toHaveFocus(); + } ); + + test( 'Only up/down work with vertical orientation', async () => { + const user = userEvent.setup(); + const { item1, item2, item3 } = + initialiseOneDimensionalTest( { + rtl, + orientation: 'vertical', + } ); + + await user.tab(); + expect( item1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item2 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( item2 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[End]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[Home]' ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[PageDown]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[PageUp]' ); + expect( item1 ).toHaveFocus(); + } ); - test( 'Focus moves between rows/columns with wrap enabled', async () => { - const user = userEvent.setup(); - const { itemA1, itemA2, itemA3, itemB1, itemC1, itemC3 } = - initialiseTwoDimensionalTest( { wrap: true } ); - - await user.tab(); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowRight]' ); - expect( itemA2 ).toHaveFocus(); - await user.keyboard( '[ArrowRight]' ); - expect( itemA3 ).toHaveFocus(); - await user.keyboard( '[ArrowRight]' ); - expect( itemB1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemC1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemA2 ).toHaveFocus(); - await user.keyboard( '[ArrowLeft]' ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowLeft]' ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '{Control>}[End]{/Control}' ); - expect( itemC3 ).toHaveFocus(); - await user.keyboard( '[ArrowRight]' ); - expect( itemC3 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemC3 ).toHaveFocus(); + test( 'Focus wraps with loop enabled', async () => { + const user = userEvent.setup(); + const { item1, item2, item3 } = + initialiseOneDimensionalTest( { + rtl, + loop: true, + } ); + + await user.tab(); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item2 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( item1 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( item3 ).toHaveFocus(); + } ); } ); - test( 'Focus wraps around start/end with loop and wrap enabled', async () => { - const user = userEvent.setup(); - const { itemA1, itemC3 } = initialiseTwoDimensionalTest( { - loop: true, - wrap: true, + describe( 'In two dimensions', () => { + test( 'All directions work as standard', async () => { + const user = userEvent.setup(); + const { + itemA1, + itemA2, + itemA3, + itemB1, + itemB2, + itemC1, + itemC3, + } = initialiseTwoDimensionalTest( { rtl } ); + + await user.tab(); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemB1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemB2 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( itemA2 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[End]' ); + expect( itemA3 ).toHaveFocus(); + await user.keyboard( '[PageDown]' ); + expect( itemC3 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemC3 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemC3 ).toHaveFocus(); + await user.keyboard( '[Home]' ); + expect( itemC1 ).toHaveFocus(); + await user.keyboard( '[PageUp]' ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '{Control>}[End]{/Control}' ); + expect( itemC3 ).toHaveFocus(); + await user.keyboard( '{Control>}[Home]{/Control}' ); + expect( itemA1 ).toHaveFocus(); } ); - await user.tab(); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowLeft]' ); - expect( itemC3 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( itemC3 ).toHaveFocus(); - await user.keyboard( '[ArrowRight]' ); - expect( itemA1 ).toHaveFocus(); - } ); + test( 'Focus wraps around rows/columns with loop enabled', async () => { + const user = userEvent.setup(); + const { + itemA1, + itemA2, + itemA3, + itemB1, + itemC1, + itemC3, + } = initialiseTwoDimensionalTest( { rtl, loop: true } ); + + await user.tab(); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemA2 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemA3 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemB1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemC1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( itemA3 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( itemC3 ).toHaveFocus(); + } ); - test( 'Focus shifts if vertical neighbour unavailable when shift enabled', async () => { - const user = userEvent.setup(); - const { itemA1, itemB1, itemB2, itemC1 } = - initialiseShiftTest( true ); - - await user.tab(); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemB1 ).toHaveFocus(); - await user.keyboard( '[ArrowRight]' ); - expect( itemB2 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - // A2 doesn't exist - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemB1 ).toHaveFocus(); - await user.keyboard( '[ArrowRight]' ); - expect( itemB2 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - // C2 is disabled - expect( itemC1 ).toHaveFocus(); - } ); + test( 'Focus moves between rows/columns with wrap enabled', async () => { + const user = userEvent.setup(); + const { + itemA1, + itemA2, + itemA3, + itemB1, + itemC1, + itemC3, + } = initialiseTwoDimensionalTest( { rtl, wrap: true } ); + + await user.tab(); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemA2 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemA3 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemB1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemC1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemA2 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '{Control>}[End]{/Control}' ); + expect( itemC3 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemC3 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemC3 ).toHaveFocus(); + } ); - test( 'Focus does not shift if vertical neighbour unavailable when shift not enabled', async () => { - const user = userEvent.setup(); - const { itemA1, itemB1, itemB2 } = - initialiseShiftTest( false ); - - await user.tab(); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemB1 ).toHaveFocus(); - await user.keyboard( '[ArrowRight]' ); - expect( itemB2 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - // A2 doesn't exist - expect( itemB2 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - // C2 is disabled - expect( itemB2 ).toHaveFocus(); + test( 'Focus wraps around start/end with loop and wrap enabled', async () => { + const user = userEvent.setup(); + const { itemA1, itemC3 } = initialiseTwoDimensionalTest( + { + rtl, + loop: true, + wrap: true, + } + ); + + await user.tab(); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( itemC3 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( itemC3 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemA1 ).toHaveFocus(); + } ); + + test( 'Focus shifts if vertical neighbour unavailable when shift enabled', async () => { + const user = userEvent.setup(); + const { itemA1, itemB1, itemB2, itemC1 } = + initialiseShiftTest( true, rtl ); + + await user.tab(); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemB1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemB2 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + // A2 doesn't exist + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemB1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemB2 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + // C2 is disabled + expect( itemC1 ).toHaveFocus(); + } ); + + test( 'Focus does not shift if vertical neighbour unavailable when shift not enabled', async () => { + const user = userEvent.setup(); + const { itemA1, itemB1, itemB2 } = initialiseShiftTest( + false, + rtl + ); + + await user.tab(); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemB1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemB2 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + // A2 doesn't exist + expect( itemB2 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + // C2 is disabled + expect( itemB2 ).toHaveFocus(); + } ); } ); } ); } ); From 817b2df9ea01ceeae9c9c7c6535cd6d72571a754 Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Fri, 15 Dec 2023 17:34:50 +0000 Subject: [PATCH 11/12] Removing custom prop tests --- packages/components/src/composite/test/index.tsx | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/packages/components/src/composite/test/index.tsx b/packages/components/src/composite/test/index.tsx index e156359ad2e32e..64ae7f312f14cb 100644 --- a/packages/components/src/composite/test/index.tsx +++ b/packages/components/src/composite/test/index.tsx @@ -44,24 +44,9 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( }; } - function useCustomProps( initialState?: InitialState ) { - const state = useCompositeState( initialState ); - const { up, down, previous, next, move } = state; - - return { - ...state, - up: jest.fn( up ), - down: jest.fn( down ), - previous: jest.fn( previous ), - next: jest.fn( next ), - move: jest.fn( move ), - }; - } - describe.each( [ [ 'With "spread" state', useSpreadProps ], [ 'With `state` prop', useStateProps ], - [ 'With custom props', useCustomProps ], ] )( '%s', ( __, useProps ) => { function OneDimensionalTest( initialState?: InitialState ) { const props = useProps( initialState ); From 908d6a20f562c7f7e820db5821dcde49bc83f43c Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Sat, 16 Dec 2023 01:40:13 +0000 Subject: [PATCH 12/12] Refactoring, reducing test overhead --- .../components/src/composite/test/index.tsx | 823 +++++++++--------- 1 file changed, 410 insertions(+), 413 deletions(-) diff --git a/packages/components/src/composite/test/index.tsx b/packages/components/src/composite/test/index.tsx index 64ae7f312f14cb..02fe6c3d1d60ab 100644 --- a/packages/components/src/composite/test/index.tsx +++ b/packages/components/src/composite/test/index.tsx @@ -44,119 +44,109 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( }; } - describe.each( [ - [ 'With "spread" state', useSpreadProps ], - [ 'With `state` prop', useStateProps ], - ] )( '%s', ( __, useProps ) => { - function OneDimensionalTest( initialState?: InitialState ) { - const props = useProps( initialState ); - return ( - - Item 1 - Item 2 - Item 3 - - ); - } - - function getOneDimensionalItems() { - return { - item1: screen.getByText( 'Item 1' ), - item2: screen.getByText( 'Item 2' ), - item3: screen.getByText( 'Item 3' ), - }; - } + function OneDimensionalTest( { ...props } ) { + return ( + + Item 1 + Item 2 + Item 3 + + ); + } - function initialiseOneDimensionalTest( - initialState?: InitialState - ) { - render( ); - return getOneDimensionalItems(); - } + function getOneDimensionalItems() { + return { + item1: screen.getByText( 'Item 1' ), + item2: screen.getByText( 'Item 2' ), + item3: screen.getByText( 'Item 3' ), + }; + } - function TwoDimensionalTest( initialState?: InitialState ) { - const props = useProps( initialState ); - - return ( - - - Item A1 - Item A2 - Item A3 - - - Item B1 - Item B2 - Item B3 - - - Item C1 - Item C2 - Item C3 - - - ); - } + function TwoDimensionalTest( { ...props } ) { + return ( + + + Item A1 + Item A2 + Item A3 + + + Item B1 + Item B2 + Item B3 + + + Item C1 + Item C2 + Item C3 + + + ); + } - function getTwoDimensionalItems() { - return { - itemA1: screen.getByText( 'Item A1' ), - itemA2: screen.getByText( 'Item A2' ), - itemA3: screen.getByText( 'Item A3' ), - itemB1: screen.getByText( 'Item B1' ), - itemB2: screen.getByText( 'Item B2' ), - itemB3: screen.getByText( 'Item B3' ), - itemC1: screen.getByText( 'Item C1' ), - itemC2: screen.getByText( 'Item C2' ), - itemC3: screen.getByText( 'Item C3' ), - }; - } + function getTwoDimensionalItems() { + return { + itemA1: screen.getByText( 'Item A1' ), + itemA2: screen.getByText( 'Item A2' ), + itemA3: screen.getByText( 'Item A3' ), + itemB1: screen.getByText( 'Item B1' ), + itemB2: screen.getByText( 'Item B2' ), + itemB3: screen.getByText( 'Item B3' ), + itemC1: screen.getByText( 'Item C1' ), + itemC2: screen.getByText( 'Item C2' ), + itemC3: screen.getByText( 'Item C3' ), + }; + } - function initialiseTwoDimensionalTest( - initialState?: InitialState - ) { - render( ); - return getTwoDimensionalItems(); - } + function ShiftTest( { ...props } ) { + return ( + + + Item A1 + + + Item B1 + Item B2 + + + Item C1 + + Item C2 + + + + ); + } - function initialiseShiftTest( shift: boolean, rtl: boolean ) { - const Test = () => { - const props = useProps( { rtl, shift } ); + function getShiftTestItems() { + return { + itemA1: screen.getByText( 'Item A1' ), + itemB1: screen.getByText( 'Item B1' ), + itemB2: screen.getByText( 'Item B2' ), + itemC1: screen.getByText( 'Item C1' ), + itemC2: screen.getByText( 'Item C2' ), + }; + } - return ( - - - - Item A1 - - - - - Item B1 - - - Item B2 - - - - - Item C1 - - - Item C2 - - - - ); - }; + describe.each( [ + [ 'With spread state', useSpreadProps ], + [ 'With `state` prop', useStateProps ], + ] )( '%s', ( __, useProps ) => { + function useOneDimensionalTest( initialState?: InitialState ) { + const Test = () => ( + + ); render( ); - return { - itemA1: screen.getByText( 'Item A1' ), - itemB1: screen.getByText( 'Item B1' ), - itemB2: screen.getByText( 'Item B2' ), - itemC1: screen.getByText( 'Item C1' ), - itemC2: screen.getByText( 'Item C2' ), - }; + return getOneDimensionalItems(); } test( 'Renders as a single tab stop', async () => { @@ -164,7 +154,7 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( const Test = () => ( <> - + ); @@ -185,7 +175,10 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( const Test = () => { const props = useProps(); return ( - + Item 1 Item 2 @@ -212,7 +205,10 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( const Test = () => { const props = useProps(); return ( - + Item 1 Item 2 @@ -235,7 +231,7 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( } ); test( 'Supports `baseId`', async () => { - const { item1, item2, item3 } = initialiseOneDimensionalTest( { + const { item1, item2, item3 } = useOneDimensionalTest( { baseId: 'test-id', } ); @@ -246,7 +242,7 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( test( 'Supports `currentId`', async () => { const user = userEvent.setup(); - const { item2 } = initialiseOneDimensionalTest( { + const { item2 } = useOneDimensionalTest( { baseId: 'test-id', currentId: 'test-id-2', } ); @@ -254,324 +250,325 @@ describe.each( Object.entries( COMPOSITE_SUITES ) )( await user.tab(); expect( item2 ).toHaveFocus(); } ); + } ); - describe.each( [ - [ - 'When LTR', - false, - { previous: 'ArrowLeft', next: 'ArrowRight' }, - ], - [ - 'When RTL', - true, - { previous: 'ArrowRight', next: 'ArrowLeft' }, - ], - ] )( '%s', ( _when, rtl, { previous, next } ) => { - describe( 'In one dimension', () => { - test( 'All directions work with no orientation', async () => { - const user = userEvent.setup(); - const { item1, item2, item3 } = - initialiseOneDimensionalTest( { rtl } ); - - await user.tab(); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item2 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( item2 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( item2 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( item3 ).toHaveFocus(); - await user.keyboard( `[${ previous }]` ); - expect( item2 ).toHaveFocus(); - await user.keyboard( `[${ previous }]` ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[End]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[Home]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[PageDown]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[PageUp]' ); - expect( item1 ).toHaveFocus(); - } ); + describe.each( [ + [ + 'When LTR', + false, + { previous: 'ArrowLeft', next: 'ArrowRight' }, + ], + [ 'When RTL', true, { previous: 'ArrowRight', next: 'ArrowLeft' } ], + ] )( '%s', ( _when, rtl, { previous, next } ) => { + function useOneDimensionalTest( initialState?: InitialState ) { + const Test = () => ( + + ); + render( ); + return getOneDimensionalItems(); + } - test( 'Only left/right work with horizontal orientation', async () => { - const user = userEvent.setup(); - const { item1, item2, item3 } = - initialiseOneDimensionalTest( { - rtl, - orientation: 'horizontal', - } ); - - await user.tab(); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( item2 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( `[${ previous }]` ); - expect( item2 ).toHaveFocus(); - await user.keyboard( `[${ previous }]` ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[End]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[Home]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[PageDown]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[PageUp]' ); - expect( item1 ).toHaveFocus(); - } ); + function useTwoDimensionalTest( initialState?: InitialState ) { + const Test = () => ( + + ); + render( ); + return getTwoDimensionalItems(); + } - test( 'Only up/down work with vertical orientation', async () => { - const user = userEvent.setup(); - const { item1, item2, item3 } = - initialiseOneDimensionalTest( { - rtl, - orientation: 'vertical', - } ); - - await user.tab(); - expect( item1 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item2 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( `[${ previous }]` ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( item2 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[End]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[Home]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[PageDown]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[PageUp]' ); - expect( item1 ).toHaveFocus(); - } ); + function useShiftTest( shift: boolean ) { + const Test = () => ( + + ); + render( ); + return getShiftTestItems(); + } - test( 'Focus wraps with loop enabled', async () => { - const user = userEvent.setup(); - const { item1, item2, item3 } = - initialiseOneDimensionalTest( { - rtl, - loop: true, - } ); - - await user.tab(); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item2 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( item1 ).toHaveFocus(); - await user.keyboard( `[${ previous }]` ); - expect( item3 ).toHaveFocus(); + describe( 'In one dimension', () => { + test( 'All directions work with no orientation', async () => { + const user = userEvent.setup(); + const { item1, item2, item3 } = useOneDimensionalTest( { + rtl, } ); + + await user.tab(); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item2 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( item2 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( item1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( item2 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( item3 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( item2 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[End]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[Home]' ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[PageDown]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[PageUp]' ); + expect( item1 ).toHaveFocus(); } ); - describe( 'In two dimensions', () => { - test( 'All directions work as standard', async () => { - const user = userEvent.setup(); - const { - itemA1, - itemA2, - itemA3, - itemB1, - itemB2, - itemC1, - itemC3, - } = initialiseTwoDimensionalTest( { rtl } ); - - await user.tab(); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( `[${ previous }]` ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemB1 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemB2 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( itemA2 ).toHaveFocus(); - await user.keyboard( `[${ previous }]` ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[End]' ); - expect( itemA3 ).toHaveFocus(); - await user.keyboard( '[PageDown]' ); - expect( itemC3 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemC3 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemC3 ).toHaveFocus(); - await user.keyboard( '[Home]' ); - expect( itemC1 ).toHaveFocus(); - await user.keyboard( '[PageUp]' ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '{Control>}[End]{/Control}' ); - expect( itemC3 ).toHaveFocus(); - await user.keyboard( '{Control>}[Home]{/Control}' ); - expect( itemA1 ).toHaveFocus(); + test( 'Only left/right work with horizontal orientation', async () => { + const user = userEvent.setup(); + const { item1, item2, item3 } = useOneDimensionalTest( { + rtl, + orientation: 'horizontal', } ); - test( 'Focus wraps around rows/columns with loop enabled', async () => { - const user = userEvent.setup(); - const { - itemA1, - itemA2, - itemA3, - itemB1, - itemC1, - itemC3, - } = initialiseTwoDimensionalTest( { rtl, loop: true } ); - - await user.tab(); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemA2 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemA3 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemB1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemC1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( `[${ previous }]` ); - expect( itemA3 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( itemC3 ).toHaveFocus(); - } ); + await user.tab(); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( item2 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( item2 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[End]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[Home]' ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[PageDown]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[PageUp]' ); + expect( item1 ).toHaveFocus(); + } ); - test( 'Focus moves between rows/columns with wrap enabled', async () => { - const user = userEvent.setup(); - const { - itemA1, - itemA2, - itemA3, - itemB1, - itemC1, - itemC3, - } = initialiseTwoDimensionalTest( { rtl, wrap: true } ); - - await user.tab(); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemA2 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemA3 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemB1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemC1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemA2 ).toHaveFocus(); - await user.keyboard( `[${ previous }]` ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( `[${ previous }]` ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '{Control>}[End]{/Control}' ); - expect( itemC3 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemC3 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemC3 ).toHaveFocus(); + test( 'Only up/down work with vertical orientation', async () => { + const user = userEvent.setup(); + const { item1, item2, item3 } = useOneDimensionalTest( { + rtl, + orientation: 'vertical', } ); - test( 'Focus wraps around start/end with loop and wrap enabled', async () => { - const user = userEvent.setup(); - const { itemA1, itemC3 } = initialiseTwoDimensionalTest( - { - rtl, - loop: true, - wrap: true, - } - ); - - await user.tab(); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( `[${ previous }]` ); - expect( itemC3 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( itemC3 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemA1 ).toHaveFocus(); - } ); + await user.tab(); + expect( item1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item2 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( item2 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[End]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[Home]' ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[PageDown]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[PageUp]' ); + expect( item1 ).toHaveFocus(); + } ); - test( 'Focus shifts if vertical neighbour unavailable when shift enabled', async () => { - const user = userEvent.setup(); - const { itemA1, itemB1, itemB2, itemC1 } = - initialiseShiftTest( true, rtl ); - - await user.tab(); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemB1 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemB2 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - // A2 doesn't exist - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemB1 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemB2 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - // C2 is disabled - expect( itemC1 ).toHaveFocus(); + test( 'Focus wraps with loop enabled', async () => { + const user = userEvent.setup(); + const { item1, item2, item3 } = useOneDimensionalTest( { + rtl, + loop: true, } ); - test( 'Focus does not shift if vertical neighbour unavailable when shift not enabled', async () => { - const user = userEvent.setup(); - const { itemA1, itemB1, itemB2 } = initialiseShiftTest( - false, - rtl - ); - - await user.tab(); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemB1 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemB2 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - // A2 doesn't exist - expect( itemB2 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - // C2 is disabled - expect( itemB2 ).toHaveFocus(); + await user.tab(); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item2 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( item1 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( item3 ).toHaveFocus(); + } ); + } ); + + describe( 'In two dimensions', () => { + test( 'All directions work as standard', async () => { + const user = userEvent.setup(); + const { + itemA1, + itemA2, + itemA3, + itemB1, + itemB2, + itemC1, + itemC3, + } = useTwoDimensionalTest( { rtl } ); + + await user.tab(); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemB1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemB2 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( itemA2 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[End]' ); + expect( itemA3 ).toHaveFocus(); + await user.keyboard( '[PageDown]' ); + expect( itemC3 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemC3 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemC3 ).toHaveFocus(); + await user.keyboard( '[Home]' ); + expect( itemC1 ).toHaveFocus(); + await user.keyboard( '[PageUp]' ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '{Control>}[End]{/Control}' ); + expect( itemC3 ).toHaveFocus(); + await user.keyboard( '{Control>}[Home]{/Control}' ); + expect( itemA1 ).toHaveFocus(); + } ); + + test( 'Focus wraps around rows/columns with loop enabled', async () => { + const user = userEvent.setup(); + const { itemA1, itemA2, itemA3, itemB1, itemC1, itemC3 } = + useTwoDimensionalTest( { rtl, loop: true } ); + + await user.tab(); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemA2 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemA3 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemB1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemC1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( itemA3 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( itemC3 ).toHaveFocus(); + } ); + + test( 'Focus moves between rows/columns with wrap enabled', async () => { + const user = userEvent.setup(); + const { itemA1, itemA2, itemA3, itemB1, itemC1, itemC3 } = + useTwoDimensionalTest( { rtl, wrap: true } ); + + await user.tab(); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemA2 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemA3 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemB1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemC1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemA2 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '{Control>}[End]{/Control}' ); + expect( itemC3 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemC3 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemC3 ).toHaveFocus(); + } ); + + test( 'Focus wraps around start/end with loop and wrap enabled', async () => { + const user = userEvent.setup(); + const { itemA1, itemC3 } = useTwoDimensionalTest( { + rtl, + loop: true, + wrap: true, } ); + + await user.tab(); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( itemC3 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( itemC3 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemA1 ).toHaveFocus(); + } ); + + test( 'Focus shifts if vertical neighbour unavailable when shift enabled', async () => { + const user = userEvent.setup(); + const { itemA1, itemB1, itemB2, itemC1 } = + useShiftTest( true ); + + await user.tab(); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemB1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemB2 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + // A2 doesn't exist + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemB1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemB2 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + // C2 is disabled + expect( itemC1 ).toHaveFocus(); + } ); + + test( 'Focus does not shift if vertical neighbour unavailable when shift not enabled', async () => { + const user = userEvent.setup(); + const { itemA1, itemB1, itemB2 } = useShiftTest( false ); + + await user.tab(); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemB1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemB2 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + // A2 doesn't exist + expect( itemB2 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + // C2 is disabled + expect( itemB2 ).toHaveFocus(); } ); } ); } );