From 0991467c3db768644203ccaec9d5de0b124cd687 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Tue, 1 Oct 2024 17:21:09 -0700 Subject: [PATCH 01/19] scaffolding and documenting tester api --- .../@react-aria/test-utils/src/combobox.ts | 75 +++++++++++++------ .../@react-aria/test-utils/src/gridlist.ts | 47 ++++++++---- packages/@react-aria/test-utils/src/menu.ts | 63 ++++++++++++---- packages/@react-aria/test-utils/src/user.ts | 21 +++++- .../dev/docs/pages/react-aria/testing.mdx | 63 ++++++++++++++++ .../dev/docs/pages/react-spectrum/testing.mdx | 2 +- 6 files changed, 218 insertions(+), 53 deletions(-) create mode 100644 packages/dev/docs/pages/react-aria/testing.mdx diff --git a/packages/@react-aria/test-utils/src/combobox.ts b/packages/@react-aria/test-utils/src/combobox.ts index ede87adc37f..871cec21351 100644 --- a/packages/@react-aria/test-utils/src/combobox.ts +++ b/packages/@react-aria/test-utils/src/combobox.ts @@ -52,11 +52,17 @@ export class ComboBoxTester { } } - setInteractionType = (type: UserOpts['interactionType']) => { + /** + * Set the interaction type used by the combobox tester. + */ + setInteractionType(type: UserOpts['interactionType']) { this._interactionType = type; - }; + } - open = async (opts: {triggerBehavior?: 'focus' | 'manual', interactionType?: UserOpts['interactionType']} = {}) => { + /** + * Opens the combobox dropdown using the interaction type set on the combobox tester. + */ + async open(opts: {triggerBehavior?: 'focus' | 'manual', interactionType?: UserOpts['interactionType']} = {}) { let {triggerBehavior = 'manual', interactionType = this._interactionType} = opts; let trigger = this.trigger; let combobox = this.combobox; @@ -96,9 +102,13 @@ export class ComboBoxTester { return true; } }); - }; + } - selectOption = async (opts: {option?: HTMLElement, optionText?: string, triggerBehavior?: 'focus' | 'manual', interactionType?: UserOpts['interactionType']} = {}) => { + /** + * Selects the desired combobox option using the interaction type set on the combobox tester. If necessary, will open the combobox dropdown beforehand. + * The desired option can be targeted via the option's node or the option's text. + */ + async selectOption(opts: {option?: HTMLElement, optionText?: string, triggerBehavior?: 'focus' | 'manual', interactionType?: UserOpts['interactionType']} = {}) { let {optionText, option, triggerBehavior, interactionType = this._interactionType} = opts; if (!this.combobox.getAttribute('aria-controls')) { await this.open({triggerBehavior}); @@ -130,9 +140,12 @@ export class ComboBoxTester { } else { throw new Error("Attempted to select a option in the combobox, but the listbox wasn't found."); } - }; + } - close = async () => { + /** + * Closes the combobox dropdown. + */ + async close() { let listbox = this.listbox; if (listbox) { act(() => this.combobox.focus()); @@ -146,22 +159,46 @@ export class ComboBoxTester { } }); } - }; + } - get combobox() { + /** + * Returns the combobox. + */ + get combobox(): HTMLElement | null { return this._combobox; } - get trigger() { + /** + * Returns the combobox trigger button if present. + */ + get trigger(): HTMLElement | null { return this._trigger; } - get listbox() { + /** + * Returns the combobox's listbox if present. + */ + get listbox(): HTMLElement | null { let listBoxId = this.combobox.getAttribute('aria-controls'); return listBoxId ? document.getElementById(listBoxId) || undefined : undefined; } - options = (opts: {element?: HTMLElement} = {}): HTMLElement[] | never[] => { + /** + * Returns the combobox's sections if present. + */ + get sections(): HTMLElement[] { + let listbox = this.listbox; + if (listbox) { + return within(listbox).queryAllByRole('group'); + } else { + return []; + } + } + + /** + * Returns the combobox's options if present. Can be filtered to a subsection of the listbox if provided. + */ + options(opts: {element?: HTMLElement} = {}): HTMLElement[] { let {element} = opts; element = element || this.listbox; let options = []; @@ -170,18 +207,12 @@ export class ComboBoxTester { } return options; - }; - - get sections() { - let listbox = this.listbox; - if (listbox) { - return within(listbox).queryAllByRole('group'); - } else { - return []; - } } - get focusedOption() { + /** + * Returns the currently focused option in the combobox's dropdown if any. + */ + get focusedOption(): HTMLElement | null { let focusedOptionId = this.combobox.getAttribute('aria-activedescendant'); return focusedOptionId ? document.getElementById(focusedOptionId) : undefined; } diff --git a/packages/@react-aria/test-utils/src/gridlist.ts b/packages/@react-aria/test-utils/src/gridlist.ts index 7043c97d32b..ce8d7a4b718 100644 --- a/packages/@react-aria/test-utils/src/gridlist.ts +++ b/packages/@react-aria/test-utils/src/gridlist.ts @@ -30,16 +30,22 @@ export class GridListTester { this._gridlist = root; } - setInteractionType = (type: UserOpts['interactionType']) => { + /** + * Set the interaction type used by the gridlist tester. + */ + setInteractionType(type: UserOpts['interactionType']) { this._interactionType = type; - }; + } // TODO: support long press? This is also pretty much the same as table's toggleRowSelection so maybe can share // For now, don't include long press, see if people need it or if we should just expose long press as a separate util if it isn't very common // If the current way of passing in the user specified advance timers is ok, then I'd be find including long press // Maybe also support an option to force the click to happen on a specific part of the element (checkbox or row). That way // the user can test a specific type of interaction? - toggleRowSelection = async (opts: {index?: number, text?: string, interactionType?: UserOpts['interactionType']} = {}) => { + /** + * Toggles the selection for the specified gridlist row using the interaction type set on the gridlist tester. + */ + async toggleRowSelection(opts: {index?: number, text?: string, interactionType?: UserOpts['interactionType']} = {}) { let {index, text, interactionType = this._interactionType} = opts; let row = this.findRow({index, text}); @@ -50,11 +56,11 @@ export class GridListTester { let cell = within(row).getAllByRole('gridcell')[0]; await pressElement(this.user, cell, interactionType); } - }; + } // TODO: pretty much the same as table except it uses this.gridlist. Make common between the two by accepting an option for // an element? - findRow = (opts: {index?: number, text?: string}) => { + private findRow(opts: {index?: number, text?: string}) { let { index, text @@ -71,11 +77,14 @@ export class GridListTester { } return row; - }; + } // TODO: There is a more difficult use case where the row has/behaves as link, don't think we have a good way to determine that unless the // user specificlly tells us - triggerRowAction = async (opts: {index?: number, text?: string, needsDoubleClick?: boolean, interactionType?: UserOpts['interactionType']}) => { + /** + * Triggers the action for the specified gridlist row using the interaction type set on the gridlist tester. + */ + async triggerRowAction(opts: {index?: number, text?: string, needsDoubleClick?: boolean, interactionType?: UserOpts['interactionType']}) { let { index, text, @@ -94,23 +103,35 @@ export class GridListTester { await pressElement(this.user, row, interactionType); } } - }; + } // TODO: do we really need this getter? Theoretically the user already has the reference to the gridlist - get gridlist() { + /** + * Returns the gridlist. + */ + get gridlist(): HTMLElement | null { return this._gridlist; } - get rows() { + /** + * Returns the gridlist's rows if any. + */ + get rows(): HTMLElement[] { return within(this?.gridlist).queryAllByRole('row'); } - get selectedRows() { + /** + * Returns the gridlist's selected rows if any. + */ + get selectedRows(): HTMLElement[] { return this.rows.filter(row => row.getAttribute('aria-selected') === 'true'); } - cells = (opts: {element?: HTMLElement} = {}) => { + /** + * Returns the gridlist's cells if any. Can be filtered against a specific row if provided. + */ + cells(opts: {element?: HTMLElement} = {}): HTMLElement[] | null { let {element} = opts; return within(element || this.gridlist).queryAllByRole('gridcell'); - }; + } } diff --git a/packages/@react-aria/test-utils/src/menu.ts b/packages/@react-aria/test-utils/src/menu.ts index 68f2a948cb3..9508cd7126b 100644 --- a/packages/@react-aria/test-utils/src/menu.ts +++ b/packages/@react-aria/test-utils/src/menu.ts @@ -43,13 +43,19 @@ export class MenuTester { } } - setInteractionType = (type: UserOpts['interactionType']) => { + /** + * Set the interaction type used by the menu tester. + */ + setInteractionType(type: UserOpts['interactionType']) { this._interactionType = type; - }; + } // TODO: this has been common to select as well, maybe make select use it? Or make a generic method. Will need to make error messages generic // One difference will be that it supports long press as well - open = async (opts: {needsLongPress?: boolean, interactionType?: UserOpts['interactionType']} = {}) => { + /** + * Opens the menu using the interaction type set on the menu tester. + */ + async open(opts: {needsLongPress?: boolean, interactionType?: UserOpts['interactionType']} = {}) { let { needsLongPress, interactionType = this._interactionType @@ -91,11 +97,15 @@ export class MenuTester { } }); } - }; + } // TODO: also very similar to select, barring potential long press support // Close on select is also kinda specific? - selectOption = async (opts: {option?: HTMLElement, optionText?: string, menuSelectionMode?: 'single' | 'multiple', needsLongPress?: boolean, closesOnSelect?: boolean, interactionType?: UserOpts['interactionType']}) => { + /** + * Selects the desired menu option using the interaction type set on the menu tester. If necessary, will open the menu dropdown beforehand. + * The desired option can be targeted via the option's node or the option's text. + */ + async selectOption(opts: {option?: HTMLElement, optionText?: string, menuSelectionMode?: 'single' | 'multiple', needsLongPress?: boolean, closesOnSelect?: boolean, interactionType?: UserOpts['interactionType']}) { let { optionText, menuSelectionMode = 'single', @@ -146,10 +156,13 @@ export class MenuTester { } else { throw new Error("Attempted to select a option in the menu, but menu wasn't found."); } - }; + } // TODO: update this to remove needsLongPress if we wanna make the user call open first always - openSubmenu = async (opts: {submenuTrigger?: HTMLElement, submenuTriggerText?: string, needsLongPress?: boolean, interactionType?: UserOpts['interactionType']}): Promise => { + /** + * Opens the submenu using the interaction type set on the menu tester. The submenu trigger can be targeted via the trigger's node or the trigger's text. + */ + async openSubmenu(opts: {submenuTrigger?: HTMLElement, submenuTriggerText?: string, needsLongPress?: boolean, interactionType?: UserOpts['interactionType']}): Promise { let { submenuTrigger, submenuTriggerText, @@ -179,9 +192,12 @@ export class MenuTester { } return null; - }; + } - close = async () => { + /** + * Closes the menu. + */ + async close() { let menu = this.menu; if (menu) { act(() => menu.focus()); @@ -199,18 +215,27 @@ export class MenuTester { throw new Error('Expected the menu to not be in the document after closing it.'); } } - }; + } - get trigger() { + /** + * Returns the menu's trigger. + */ + get trigger(): HTMLElement { return this._trigger; } - get menu() { + /** + * Returns the menu if present. + */ + get menu(): HTMLElement | null { let menuId = this.trigger.getAttribute('aria-controls'); - return menuId ? document.getElementById(menuId) : undefined; + return menuId ? document.getElementById(menuId) : null; } - get options(): HTMLElement[] | never[] { + /** + * Returns the menu's options if present. + */ + get options(): HTMLElement[] { let menu = this.menu; let options = []; if (menu) { @@ -226,7 +251,10 @@ export class MenuTester { return options; } - get sections() { + /** + * Returns the menu's sections if any. + */ + get sections(): HTMLElement[] { let menu = this.menu; if (menu) { return within(menu).queryAllByRole('group'); @@ -235,7 +263,10 @@ export class MenuTester { } } - get submenuTriggers() { + /** + * Returns the menu's submenu triggers if any. + */ + get submenuTriggers(): HTMLElement[] { let options = this.options; if (options.length > 0) { return this.options.filter(item => item.getAttribute('aria-haspopup') != null); diff --git a/packages/@react-aria/test-utils/src/user.ts b/packages/@react-aria/test-utils/src/user.ts index 8f87e30d529..0b0e0b98470 100644 --- a/packages/@react-aria/test-utils/src/user.ts +++ b/packages/@react-aria/test-utils/src/user.ts @@ -21,10 +21,18 @@ import userEvent from '@testing-library/user-event'; // https://github.com/testing-library/dom-testing-library/issues/939#issuecomment-830771708 is an interesting way of allowing users to configure the timers // curent way is like https://testing-library.com/docs/user-event/options/#advancetimers, export interface UserOpts { + /** + * The interaction type (mouse, touch, keyboard) that the test util user will use when interacting with a component. This can be overridden + * at the aria pattern util level if needed. + * @default mouse + */ interactionType?: 'mouse' | 'touch' | 'keyboard', // If using fake timers user should provide something like (time) => jest.advanceTimersByTime(time))} // A real timer user would pass async () => await new Promise((resolve) => setTimeout(resolve, waitTime)) // Time is in ms. + /** + * A function used by the test utils to advance timers during interactions. Required for certain aria patterns (e.g. table). + */ advanceTimer?: (time?: number) => void | Promise } @@ -56,8 +64,16 @@ type ObjectOptionsTypes = let defaultAdvanceTimer = async (waitTime: number | undefined) => await new Promise((resolve) => setTimeout(resolve, waitTime)); export class User { - user; + private user; + /** + * The interaction type (mouse, touch, keyboard) that the test util user will use when interacting with a component. This can be overridden + * at the aria pattern util level if needed. + * @default mouse + */ interactionType: UserOpts['interactionType']; + /** + * A function used by the test utils to advance timers during interactions. Required for certain aria patterns (e.g. table). + */ advanceTimer: UserOpts['advanceTimer']; constructor(opts: UserOpts = {}) { @@ -67,6 +83,9 @@ export class User { this.advanceTimer = advanceTimer || defaultAdvanceTimer; } + /** + * Creates an aria pattern tester, inheriting the options provided to the original user. + */ createTester(patternName: T, opts: ObjectOptionsTypes): ObjectType { return new (keyToUtil)[patternName]({...opts, user: this.user, interactionType: this.interactionType, advanceTimer: this.advanceTimer}) as ObjectType; } diff --git a/packages/dev/docs/pages/react-aria/testing.mdx b/packages/dev/docs/pages/react-aria/testing.mdx new file mode 100644 index 00000000000..bf9e3b9b499 --- /dev/null +++ b/packages/dev/docs/pages/react-aria/testing.mdx @@ -0,0 +1,63 @@ +{/* Copyright 2024 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. */} + +import {Layout} from '@react-spectrum/docs'; +export default Layout; +import testUtilDocs from 'docs:@react-aria/test-utils'; +import combobox from 'docs:@react-aria/test-utils/src/combobox.ts'; +import gridlist from 'docs:@react-aria/test-utils/src/gridlist.ts'; +import menu from 'docs:@react-aria/test-utils/src/menu.ts'; +import select from 'docs:@react-aria/test-utils/src/select.ts'; +import table from 'docs:@react-aria/test-utils/src/table.ts'; +import {ClassAPI, FunctionAPI, InterfaceType, TypeContext} from '@react-spectrum/docs'; + +--- +category: Concepts +--- + +# Testing + +This page describes how to test an application built with React Aria. It documents the available testing utilities available +for each aria pattern and how they can be used to simulate common user interactions. + +## React Aria test utils + +### Introduction + +TODO Introduce the test utils purpose and motivation + +### Installation + +### Setup + +### Patterns +TODO perhaps these should live in each RAC pages specifically and then we link from here to those pages? + +TODO need to update this so PatternNames actually comes through with the expected values + + +#### Combobox + + + +#### Gridlist + + + +#### Menu + + + +#### Select + + + +#### Table + + diff --git a/packages/dev/docs/pages/react-spectrum/testing.mdx b/packages/dev/docs/pages/react-spectrum/testing.mdx index d52eddd236c..2232db8bf25 100644 --- a/packages/dev/docs/pages/react-spectrum/testing.mdx +++ b/packages/dev/docs/pages/react-spectrum/testing.mdx @@ -18,7 +18,7 @@ category: Concepts # Testing -This page describes how to test an application built with with React Spectrum, including how to +This page describes how to test an application built with React Spectrum, including how to query the DOM tree for elements and simulate user interactions. ## Introduction From 9def1bd2ee48e6b0dd4c0ac08f29034e19499a57 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Tue, 1 Oct 2024 17:32:33 -0700 Subject: [PATCH 02/19] fix strict mode --- packages/@react-aria/test-utils/src/combobox.ts | 13 ++++++------- packages/@react-aria/test-utils/src/gridlist.ts | 4 ++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/@react-aria/test-utils/src/combobox.ts b/packages/@react-aria/test-utils/src/combobox.ts index 871cec21351..8489233e19e 100644 --- a/packages/@react-aria/test-utils/src/combobox.ts +++ b/packages/@react-aria/test-utils/src/combobox.ts @@ -22,7 +22,7 @@ export class ComboBoxTester { private user; private _interactionType: UserOpts['interactionType']; private _combobox: HTMLElement; - private _trigger: HTMLElement | undefined; + private _trigger: HTMLElement | null; constructor(opts: ComboBoxOptions) { let {root, trigger, user, interactionType} = opts; @@ -164,7 +164,7 @@ export class ComboBoxTester { /** * Returns the combobox. */ - get combobox(): HTMLElement | null { + get combobox(): HTMLElement { return this._combobox; } @@ -180,7 +180,7 @@ export class ComboBoxTester { */ get listbox(): HTMLElement | null { let listBoxId = this.combobox.getAttribute('aria-controls'); - return listBoxId ? document.getElementById(listBoxId) || undefined : undefined; + return listBoxId ? document.getElementById(listBoxId) || null : null; } /** @@ -198,9 +198,8 @@ export class ComboBoxTester { /** * Returns the combobox's options if present. Can be filtered to a subsection of the listbox if provided. */ - options(opts: {element?: HTMLElement} = {}): HTMLElement[] { - let {element} = opts; - element = element || this.listbox; + options(opts: {element?: HTMLElement | null} = {}): HTMLElement[] { + let {element = this.listbox} = opts; let options = []; if (element) { options = within(element).queryAllByRole('option'); @@ -214,6 +213,6 @@ export class ComboBoxTester { */ get focusedOption(): HTMLElement | null { let focusedOptionId = this.combobox.getAttribute('aria-activedescendant'); - return focusedOptionId ? document.getElementById(focusedOptionId) : undefined; + return focusedOptionId ? document.getElementById(focusedOptionId) : null; } } diff --git a/packages/@react-aria/test-utils/src/gridlist.ts b/packages/@react-aria/test-utils/src/gridlist.ts index ce8d7a4b718..dfd93ef39c8 100644 --- a/packages/@react-aria/test-utils/src/gridlist.ts +++ b/packages/@react-aria/test-utils/src/gridlist.ts @@ -109,7 +109,7 @@ export class GridListTester { /** * Returns the gridlist. */ - get gridlist(): HTMLElement | null { + get gridlist(): HTMLElement { return this._gridlist; } @@ -130,7 +130,7 @@ export class GridListTester { /** * Returns the gridlist's cells if any. Can be filtered against a specific row if provided. */ - cells(opts: {element?: HTMLElement} = {}): HTMLElement[] | null { + cells(opts: {element?: HTMLElement} = {}): HTMLElement[] { let {element} = opts; return within(element || this.gridlist).queryAllByRole('gridcell'); } From d6e7578c4a89c058012d8a8a85f6d1ec1ec4c36d Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Thu, 3 Oct 2024 11:42:53 -0700 Subject: [PATCH 03/19] update utils for conformity and add intro --- .../@react-aria/test-utils/src/combobox.ts | 16 ++-- .../@react-aria/test-utils/src/gridlist.ts | 13 ++- packages/@react-aria/test-utils/src/menu.ts | 46 ++++----- packages/@react-aria/test-utils/src/select.ts | 96 ++++++++++++------- packages/@react-aria/test-utils/src/table.ts | 92 +++++++++++++----- .../menu/test/MenuTrigger.test.js | 20 ++-- .../picker/test/Picker.test.js | 22 ++--- .../dev/docs/pages/react-aria/testing.mdx | 18 +++- .../react-aria-components/test/Menu.test.js | 2 +- .../react-aria-components/test/Select.test.js | 2 +- .../react-aria-components/test/Table.test.js | 2 +- 11 files changed, 207 insertions(+), 122 deletions(-) diff --git a/packages/@react-aria/test-utils/src/combobox.ts b/packages/@react-aria/test-utils/src/combobox.ts index 8489233e19e..bfdde2d37f4 100644 --- a/packages/@react-aria/test-utils/src/combobox.ts +++ b/packages/@react-aria/test-utils/src/combobox.ts @@ -22,7 +22,7 @@ export class ComboBoxTester { private user; private _interactionType: UserOpts['interactionType']; private _combobox: HTMLElement; - private _trigger: HTMLElement | null; + private _trigger: HTMLElement; constructor(opts: ComboBoxOptions) { let {root, trigger, user, interactionType} = opts; @@ -60,7 +60,7 @@ export class ComboBoxTester { } /** - * Opens the combobox dropdown using the interaction type set on the combobox tester. + * Opens the combobox dropdown. Defaults to using the interaction type set on the combobox tester. */ async open(opts: {triggerBehavior?: 'focus' | 'manual', interactionType?: UserOpts['interactionType']} = {}) { let {triggerBehavior = 'manual', interactionType = this._interactionType} = opts; @@ -105,7 +105,7 @@ export class ComboBoxTester { } /** - * Selects the desired combobox option using the interaction type set on the combobox tester. If necessary, will open the combobox dropdown beforehand. + * Selects the desired combobox option. Defaults to using the interaction type set on the combobox tester. If necessary, will open the combobox dropdown beforehand. * The desired option can be targeted via the option's node or the option's text. */ async selectOption(opts: {option?: HTMLElement, optionText?: string, triggerBehavior?: 'focus' | 'manual', interactionType?: UserOpts['interactionType']} = {}) { @@ -171,7 +171,7 @@ export class ComboBoxTester { /** * Returns the combobox trigger button if present. */ - get trigger(): HTMLElement | null { + get trigger(): HTMLElement { return this._trigger; } @@ -188,17 +188,13 @@ export class ComboBoxTester { */ get sections(): HTMLElement[] { let listbox = this.listbox; - if (listbox) { - return within(listbox).queryAllByRole('group'); - } else { - return []; - } + return listbox ? within(listbox).queryAllByRole('group') : []; } /** * Returns the combobox's options if present. Can be filtered to a subsection of the listbox if provided. */ - options(opts: {element?: HTMLElement | null} = {}): HTMLElement[] { + options(opts: {element?: HTMLElement} = {}): HTMLElement[] { let {element = this.listbox} = opts; let options = []; if (element) { diff --git a/packages/@react-aria/test-utils/src/gridlist.ts b/packages/@react-aria/test-utils/src/gridlist.ts index dfd93ef39c8..8f6dfdd15e5 100644 --- a/packages/@react-aria/test-utils/src/gridlist.ts +++ b/packages/@react-aria/test-utils/src/gridlist.ts @@ -43,7 +43,7 @@ export class GridListTester { // Maybe also support an option to force the click to happen on a specific part of the element (checkbox or row). That way // the user can test a specific type of interaction? /** - * Toggles the selection for the specified gridlist row using the interaction type set on the gridlist tester. + * Toggles the selection for the specified gridlist row. Defaults to using the interaction type set on the gridlist tester. */ async toggleRowSelection(opts: {index?: number, text?: string, interactionType?: UserOpts['interactionType']} = {}) { let {index, text, interactionType = this._interactionType} = opts; @@ -60,7 +60,10 @@ export class GridListTester { // TODO: pretty much the same as table except it uses this.gridlist. Make common between the two by accepting an option for // an element? - private findRow(opts: {index?: number, text?: string}) { + /** + * Returns a row matching the specified index or text content. + */ + findRow(opts: {index?: number, text?: string}) { let { index, text @@ -82,7 +85,7 @@ export class GridListTester { // TODO: There is a more difficult use case where the row has/behaves as link, don't think we have a good way to determine that unless the // user specificlly tells us /** - * Triggers the action for the specified gridlist row using the interaction type set on the gridlist tester. + * Triggers the action for the specified gridlist row. Defaults to using the interaction type set on the gridlist tester. */ async triggerRowAction(opts: {index?: number, text?: string, needsDoubleClick?: boolean, interactionType?: UserOpts['interactionType']}) { let { @@ -131,7 +134,7 @@ export class GridListTester { * Returns the gridlist's cells if any. Can be filtered against a specific row if provided. */ cells(opts: {element?: HTMLElement} = {}): HTMLElement[] { - let {element} = opts; - return within(element || this.gridlist).queryAllByRole('gridcell'); + let {element = this.gridlist} = opts; + return within(element).queryAllByRole('gridcell'); } } diff --git a/packages/@react-aria/test-utils/src/menu.ts b/packages/@react-aria/test-utils/src/menu.ts index 9508cd7126b..041b3f05778 100644 --- a/packages/@react-aria/test-utils/src/menu.ts +++ b/packages/@react-aria/test-utils/src/menu.ts @@ -53,7 +53,7 @@ export class MenuTester { // TODO: this has been common to select as well, maybe make select use it? Or make a generic method. Will need to make error messages generic // One difference will be that it supports long press as well /** - * Opens the menu using the interaction type set on the menu tester. + * Opens the menu. Defaults to using the interaction type set on the menu tester. */ async open(opts: {needsLongPress?: boolean, interactionType?: UserOpts['interactionType']} = {}) { let { @@ -102,7 +102,7 @@ export class MenuTester { // TODO: also very similar to select, barring potential long press support // Close on select is also kinda specific? /** - * Selects the desired menu option using the interaction type set on the menu tester. If necessary, will open the menu dropdown beforehand. + * Selects the desired menu option. Defaults to using the interaction type set on the menu tester. If necessary, will open the menu dropdown beforehand. * The desired option can be targeted via the option's node or the option's text. */ async selectOption(opts: {option?: HTMLElement, optionText?: string, menuSelectionMode?: 'single' | 'multiple', needsLongPress?: boolean, closesOnSelect?: boolean, interactionType?: UserOpts['interactionType']}) { @@ -160,7 +160,7 @@ export class MenuTester { // TODO: update this to remove needsLongPress if we wanna make the user call open first always /** - * Opens the submenu using the interaction type set on the menu tester. The submenu trigger can be targeted via the trigger's node or the trigger's text. + * Opens the submenu. Defaults to using the interaction type set on the menu tester. The submenu trigger can be targeted via the trigger's node or the trigger's text. */ async openSubmenu(opts: {submenuTrigger?: HTMLElement, submenuTriggerText?: string, needsLongPress?: boolean, interactionType?: UserOpts['interactionType']}): Promise { let { @@ -233,17 +233,29 @@ export class MenuTester { } /** - * Returns the menu's options if present. + * Returns the menu's sections if any. */ - get options(): HTMLElement[] { + get sections(): HTMLElement[] { let menu = this.menu; - let options = []; if (menu) { - options = within(menu).queryAllByRole('menuitem'); + return within(menu).queryAllByRole('group'); + } else { + return []; + } + } + + /** + * Returns the menu's options if present. Can be filtered to a subsection of the menu if provided. + */ + options(opts: {element?: HTMLElement} = {}): HTMLElement[] { + let {element = this.menu} = opts; + let options = []; + if (element) { + options = within(element).queryAllByRole('menuitem'); if (options.length === 0) { - options = within(menu).queryAllByRole('menuitemradio'); + options = within(element).queryAllByRole('menuitemradio'); if (options.length === 0) { - options = within(menu).queryAllByRole('menuitemcheckbox'); + options = within(element).queryAllByRole('menuitemcheckbox'); } } } @@ -251,25 +263,13 @@ export class MenuTester { return options; } - /** - * Returns the menu's sections if any. - */ - get sections(): HTMLElement[] { - let menu = this.menu; - if (menu) { - return within(menu).queryAllByRole('group'); - } else { - return []; - } - } - /** * Returns the menu's submenu triggers if any. */ get submenuTriggers(): HTMLElement[] { - let options = this.options; + let options = this.options(); if (options.length > 0) { - return this.options.filter(item => item.getAttribute('aria-haspopup') != null); + return options.filter(item => item.getAttribute('aria-haspopup') != null); } return []; diff --git a/packages/@react-aria/test-utils/src/select.ts b/packages/@react-aria/test-utils/src/select.ts index 0773d55ccb3..689c022c804 100644 --- a/packages/@react-aria/test-utils/src/select.ts +++ b/packages/@react-aria/test-utils/src/select.ts @@ -33,12 +33,17 @@ export class SelectTester { } this._trigger = triggerButton; } - - setInteractionType = (type: UserOpts['interactionType']) => { + /** + * Set the interaction type used by the select tester. + */ + setInteractionType(type: UserOpts['interactionType']) { this._interactionType = type; - }; + } - open = async (opts: {interactionType?: UserOpts['interactionType']} = {}) => { + /** + * Opens the select. Defaults to using the interaction type set on the select tester. + */ + async open(opts: {interactionType?: UserOpts['interactionType']} = {}) { let { interactionType = this._interactionType } = opts; @@ -69,9 +74,37 @@ export class SelectTester { return true; } }); - }; + } + + /** + * Closes the select. + */ + async close() { + let listbox = this.listbox; + if (listbox) { + act(() => listbox.focus()); + await this.user.keyboard('[Escape]'); + } + + await waitFor(() => { + if (document.activeElement !== this._trigger) { + throw new Error(`Expected the document.activeElement after closing the select dropdown to be the select component trigger but got ${document.activeElement}`); + } else { + return true; + } + }); + + if (listbox && document.contains(listbox)) { + throw new Error('Expected the select element listbox to not be in the document after closing the dropdown.'); + } + } - selectOption = async (opts: {optionText: string, interactionType?: UserOpts['interactionType']}) => { + // TODO: update this so it also can take the option node instead of just text, might already have been added in Rob's PR + /** + * Selects the desired select option. Defaults to using the interaction type set on the select tester. If necessary, will open the select dropdown beforehand. + * The desired option can be targeted via the option's text. + */ + async selectOption(opts: {optionText: string, interactionType?: UserOpts['interactionType']}) { let { optionText, interactionType = this._interactionType @@ -115,43 +148,40 @@ export class SelectTester { } } } - }; + } - close = async () => { - let listbox = this.listbox; - if (listbox) { - act(() => listbox.focus()); - await this.user.keyboard('[Escape]'); + /** + * Returns the select's options if present. Can be filtered to a subsection of the listbox if provided. + */ + options(opts: {element?: HTMLElement} = {}): HTMLElement[] { + let {element = this.listbox} = opts; + let options = []; + if (element) { + options = within(element).queryAllByRole('option'); } - await waitFor(() => { - if (document.activeElement !== this._trigger) { - throw new Error(`Expected the document.activeElement after closing the select dropdown to be the select component trigger but got ${document.activeElement}`); - } else { - return true; - } - }); - - if (listbox && document.contains(listbox)) { - throw new Error('Expected the select element listbox to not be in the document after closing the dropdown.'); - } - }; + return options; + } - get trigger() { + /** + * Returns the select's trigger. + */ + get trigger(): HTMLElement { return this._trigger; } - get listbox() { + /** + * Returns the select's listbox if present. + */ + get listbox(): HTMLElement | null { let listBoxId = this.trigger.getAttribute('aria-controls'); - return listBoxId ? document.getElementById(listBoxId) : undefined; - } - - get options() { - let listbox = this.listbox; - return listbox ? within(listbox).queryAllByRole('option') : []; + return listBoxId ? document.getElementById(listBoxId) : null; } - get sections() { + /** + * Returns the select's sections if present. + */ + get sections(): HTMLElement[] { let listbox = this.listbox; return listbox ? within(listbox).queryAllByRole('group') : []; } diff --git a/packages/@react-aria/test-utils/src/table.ts b/packages/@react-aria/test-utils/src/table.ts index 1f019706ae3..2fa7aec5117 100644 --- a/packages/@react-aria/test-utils/src/table.ts +++ b/packages/@react-aria/test-utils/src/table.ts @@ -35,11 +35,17 @@ export class TableTester { this._table = root; } - setInteractionType = (type: UserOpts['interactionType']) => { + /** + * Set the interaction type used by the table tester. + */ + setInteractionType(type: UserOpts['interactionType']) { this._interactionType = type; - }; + } - toggleRowSelection = async (opts: {index?: number, text?: string, needsLongPress?: boolean, interactionType?: UserOpts['interactionType']} = {}) => { + /** + * Toggles the selection for the specified table row. Defaults to using the interaction type set on the gridlist tester. + */ + async toggleRowSelection(opts: {index?: number, text?: string, needsLongPress?: boolean, interactionType?: UserOpts['interactionType']} = {}) { let { index, text, @@ -77,9 +83,12 @@ export class TableTester { await this._advanceTimer(200); }); - }; + } - toggleSort = async (opts: {index?: number, text?: string, interactionType?: UserOpts['interactionType']} = {}) => { + /** + * Toggles the sort order for the specified table column. Defaults to using the interaction type set on the gridlist tester. + */ + async toggleSort(opts: {index?: number, text?: string, interactionType?: UserOpts['interactionType']} = {}) { let { index, text, @@ -163,11 +172,14 @@ export class TableTester { } else { await pressElement(this.user, columnheader, interactionType); } - }; + } // TODO: should there be a util for triggering a row action? Perhaps there should be but it would rely on the user teling us the config of the // table. Maybe we could rely on the user knowing to trigger a press/double click? We could have the user pass in "needsDoubleClick" // It is also iffy if there is any row selected because then the table is in selectionMode and the below actions will simply toggle row selection - triggerRowAction = async (opts: {index?: number, text?: string, needsDoubleClick?: boolean, interactionType?: UserOpts['interactionType']} = {}) => { + /** + * Triggers the action for the specified table row. Defaults to using the interaction type set on the table tester. + */ + async triggerRowAction(opts: {index?: number, text?: string, needsDoubleClick?: boolean, interactionType?: UserOpts['interactionType']} = {}) { let { index, text, @@ -186,15 +198,17 @@ export class TableTester { await pressElement(this.user, row, interactionType); } } - }; + } // TODO: should there be utils for drag and drop and column resizing? For column resizing, I'm not entirely convinced that users will be doing that in their tests. // For DnD, it might be tricky to do for keyboard DnD since we wouldn't know what valid drop zones there are... Similarly, for simulating mouse drag and drop the coordinates depend // on the mocks the user sets up for their row height/etc. // Additionally, should we also support keyboard navigation/typeahead? Those felt like they could be very easily replicated by the user via user.keyboard already and don't really // add much value if we provide that to them - - toggleSelectAll = async (opts: {interactionType?: UserOpts['interactionType']} = {}) => { + /** + * Toggle selection for all rows in the table. Defaults to using the interaction type set on the table tester. + */ + async toggleSelectAll(opts: {interactionType?: UserOpts['interactionType']} = {}) { let { interactionType = this._interactionType } = opts; @@ -205,9 +219,12 @@ export class TableTester { } else { await pressElement(this.user, checkbox, interactionType); } - }; + } - findRow = (opts: {index?: number, text?: string} = {}) => { + /** + * Returns a row matching the specified index or text content. + */ + findRow(opts: {index?: number, text?: string} = {}) { let { index, text @@ -226,9 +243,12 @@ export class TableTester { } return row; - }; + } - findCell = (opts: {text: string}) => { + /** + * Returns a cell matching the specified text content. + */ + findCell(opts: {text: string}) { let { text } = opts; @@ -245,38 +265,58 @@ export class TableTester { } return cell; - }; + } - get table() { + /** + * Returns the table. + */ + get table(): HTMLElement { return this._table; } - get rowGroups() { + /** + * Returns the row groups within the table. + */ + get rowGroups(): HTMLElement[] { let table = this._table; return table ? within(table).queryAllByRole('rowgroup') : []; } - get columns() { + /** + * Returns the columns within the table. + */ + get columns(): HTMLElement[] { let headerRowGroup = this.rowGroups[0]; return headerRowGroup ? within(headerRowGroup).queryAllByRole('columnheader') : []; } - get rows() { + /** + * Returns the rows within the table if any. + */ + get rows(): HTMLElement[] { let bodyRowGroup = this.rowGroups[1]; return bodyRowGroup ? within(bodyRowGroup).queryAllByRole('row') : []; } - get selectedRows() { + /** + * Returns the currently selected rows within the table if any. + */ + get selectedRows(): HTMLElement[] { return this.rows.filter(row => row.getAttribute('aria-selected') === 'true'); } - get rowHeaders() { - let table = this.table; - return table ? within(table).queryAllByRole('rowheader') : []; + /** + * Returns the row headers within the table if any. + */ + get rowHeaders(): HTMLElement[] { + return within(this.table).queryAllByRole('rowheader'); } - get cells() { - let table = this.table; - return table ? within(table).queryAllByRole('gridcell') : []; + /** + * Returns the cells within the table if any. Can be filtered against a specific row if provided. + */ + cells(opts: {element?: HTMLElement} = {}): HTMLElement[] { + let {element = this.table} = opts; + return within(element).queryAllByRole('gridcell'); } } diff --git a/packages/@react-spectrum/menu/test/MenuTrigger.test.js b/packages/@react-spectrum/menu/test/MenuTrigger.test.js index 83d36783f07..24084233bb7 100644 --- a/packages/@react-spectrum/menu/test/MenuTrigger.test.js +++ b/packages/@react-spectrum/menu/test/MenuTrigger.test.js @@ -281,7 +281,7 @@ describe('MenuTrigger', function () { await menuTester.open(); let menu = menuTester.menu; expect(menu).toBeTruthy(); - let menuItems = menuTester.options; + let menuItems = menuTester.options(); let selectedItem = menuItems[1]; expect(selectedItem).toBe(document.activeElement); await menuTester.close(); @@ -292,7 +292,7 @@ describe('MenuTrigger', function () { menuTester.setInteractionType('keyboard'); fireEvent.keyDown(button, {key: 'ArrowDown', code: 40, charCode: 40}); fireEvent.keyUp(button, {key: 'ArrowDown', code: 40, charCode: 40}); - menuItems = menuTester.options; + menuItems = menuTester.options(); selectedItem = menuItems[1]; expect(selectedItem).toBe(document.activeElement); await menuTester.close(); @@ -300,7 +300,7 @@ describe('MenuTrigger', function () { // Opening menu via up arrow still autofocuses the selected item fireEvent.keyDown(button, {key: 'ArrowUp', code: 38, charCode: 38}); - menuItems = menuTester.options; + menuItems = menuTester.options(); selectedItem = menuItems[1]; expect(selectedItem).toBe(document.activeElement); }); @@ -355,17 +355,17 @@ describe('MenuTrigger', function () { let menuTester = testUtilUser.createTester('Menu', {root: tree.container}); fireEvent.keyDown(menuTester.trigger, {key: 'ArrowDown', code: 40, charCode: 40}); - let selectedItem = menuTester.options[0]; + let selectedItem = menuTester.options()[0]; expect(selectedItem).toBe(document.activeElement); fireEvent.keyDown(menuTester.menu, {key: 'ArrowDown', code: 40, charCode: 40}); - expect(menuTester.options[1]).toBe(document.activeElement); + expect(menuTester.options()[1]).toBe(document.activeElement); fireEvent.keyDown(menuTester.menu, {key: 'ArrowDown', code: 40, charCode: 40}); - expect(menuTester.options[2]).toBe(document.activeElement); + expect(menuTester.options()[2]).toBe(document.activeElement); fireEvent.keyDown(menuTester.menu, {key: 'ArrowUp', code: 38, charCode: 38}); - expect(menuTester.options[1]).toBe(document.activeElement); + expect(menuTester.options()[1]).toBe(document.activeElement); }); }); @@ -381,7 +381,7 @@ describe('MenuTrigger', function () { async function openAndTriggerMenuItem(tree, role, selectionMode, triggerEvent) { let menuTester = testUtilUser.createTester('Menu', {root: tree.container}); await menuTester.open(); - let menuItems = menuTester.options; + let menuItems = menuTester.options(); let itemToAction = menuItems[1]; await triggerEvent(itemToAction); act(() => {jest.runAllTimers();}); // FocusScope useLayoutEffect cleanup @@ -414,7 +414,7 @@ describe('MenuTrigger', function () { expect(onSelectionChange).not.toHaveBeenCalled(); let menu = menuTester.menu; - expect(menuTester.options[0]).toHaveAttribute('aria-checked', 'true'); + expect(menuTester.options()[0]).toHaveAttribute('aria-checked', 'true'); fireEvent.keyDown(menu, {key: 'Escape', code: 27, charCode: 27}); act(() => {jest.runAllTimers();}); // FocusScope useLayoutEffect cleanup act(() => {jest.runAllTimers();}); // FocusScope raf @@ -427,7 +427,7 @@ describe('MenuTrigger', function () { expect(onSelectionChange).not.toHaveBeenCalled(); menu = menuTester.menu; - expect(menuTester.options[0]).toHaveAttribute('aria-checked', 'true'); + expect(menuTester.options()[0]).toHaveAttribute('aria-checked', 'true'); expect(menu).toBeTruthy(); fireEvent.keyDown(menu, {key: 'Escape', code: 27, charCode: 27}); expect(onSelectionChange).not.toHaveBeenCalled(); diff --git a/packages/@react-spectrum/picker/test/Picker.test.js b/packages/@react-spectrum/picker/test/Picker.test.js index 9b04a3b5d63..6fe6bfd13e0 100644 --- a/packages/@react-spectrum/picker/test/Picker.test.js +++ b/packages/@react-spectrum/picker/test/Picker.test.js @@ -78,7 +78,7 @@ describe('Picker', function () { }); describe('opening', function () { - it('can be opened on mouse down', async function () { + it.only('can be opened on mouse down', async function () { let onOpenChange = jest.fn(); let {getByRole, queryByRole} = render( @@ -103,7 +103,7 @@ describe('Picker', function () { expect(picker).toHaveAttribute('aria-expanded', 'true'); expect(picker).toHaveAttribute('aria-controls', listbox.id); - let items = selectTester.options; + let items = selectTester.options(); expect(items.length).toBe(3); expect(items[0]).toHaveTextContent('One'); expect(items[1]).toHaveTextContent('Two'); @@ -214,7 +214,7 @@ describe('Picker', function () { expect(picker).toHaveAttribute('aria-expanded', 'true'); expect(picker).toHaveAttribute('aria-controls', listbox.id); - let items = selectTester.options; + let items = selectTester.options(); expect(items.length).toBe(3); expect(items[0]).toHaveTextContent('One'); expect(items[1]).toHaveTextContent('Two'); @@ -252,7 +252,7 @@ describe('Picker', function () { expect(picker).toHaveAttribute('aria-expanded', 'true'); expect(picker).toHaveAttribute('aria-controls', listbox.id); - let items = selectTester.options; + let items = selectTester.options(); expect(items.length).toBe(3); expect(items[0]).toHaveTextContent('One'); expect(items[1]).toHaveTextContent('Two'); @@ -982,7 +982,7 @@ describe('Picker', function () { await selectTester.open(); let listbox = selectTester.listbox; - let items = selectTester.options; + let items = selectTester.options(); expect(items.length).toBe(3); expect(items[0]).toHaveTextContent('One'); expect(items[1]).toHaveTextContent('Two'); @@ -1014,7 +1014,7 @@ describe('Picker', function () { await selectTester.open(); let listbox = selectTester.listbox; - let items = selectTester.options; + let items = selectTester.options(); expect(items.length).toBe(3); expect(items[0]).toHaveTextContent('Empty'); expect(items[1]).toHaveTextContent('Zero'); @@ -1105,7 +1105,7 @@ describe('Picker', function () { expect(picker).toHaveTextContent('Select…'); await selectTester.open(); - let items = selectTester.options; + let items = selectTester.options(); expect(items.length).toBe(3); expect(items[0]).toHaveTextContent('One'); expect(items[1]).toHaveTextContent('Two'); @@ -1187,7 +1187,7 @@ describe('Picker', function () { expect(listbox).toBeVisible(); expect(listbox).toHaveAttribute('aria-labelledby', label.id); - let items = selectTester.options; + let items = selectTester.options(); expect(items[0]).toHaveTextContent('One'); expect(items[1]).toHaveTextContent('Two'); expect(items[2]).toHaveTextContent('Three'); @@ -1385,7 +1385,7 @@ describe('Picker', function () { await selectTester.open(); let listbox = selectTester.listbox; - let items = selectTester.options; + let items = selectTester.options(); expect(items.length).toBe(6); let groups = selectTester.sections; @@ -1437,7 +1437,7 @@ describe('Picker', function () { await selectTester.open(); listbox = selectTester.listbox; - items = selectTester.options; + items = selectTester.options(); expect(items.length).toBe(6); expect(document.activeElement).toBe(items[1]); @@ -1546,7 +1546,7 @@ describe('Picker', function () { expect(picker).toHaveTextContent('Two'); await selectTester.open(); - let items = selectTester.options; + let items = selectTester.options(); expect(document.activeElement).toBe(items[1]); await selectTester.selectOption({optionText: 'Two'}); diff --git a/packages/dev/docs/pages/react-aria/testing.mdx b/packages/dev/docs/pages/react-aria/testing.mdx index bf9e3b9b499..04377d5582d 100644 --- a/packages/dev/docs/pages/react-aria/testing.mdx +++ b/packages/dev/docs/pages/react-aria/testing.mdx @@ -30,7 +30,21 @@ for each aria pattern and how they can be used to simulate common user interacti ### Introduction -TODO Introduce the test utils purpose and motivation +As both the adoption of our component libraries and the complexity of the components offered has grown, various testing pain points have surfaced, both from within the maintaining team and from consumers of the library itself. +The test writer may not be familiar with the internal structure of the component they are testing against and thus are unable to easily target/interact with the desired element within the component. Alternatively, the specifics +of what events to simulate for various interaction modes can be onerous to figure out and adds unnecessary friction for new adopters. + +To address this, we've created [@react-aria/test-utils](https://www.npmjs.com/package/@react-aria/test-utils) which features a set of testing utilities that aims to make writing unit tests easier for consumers of our component libraries +or for users who have built their own components following the respective ARIA pattern specification. By using the ARIA specification for any given component pattern as a source of truth, +we can make assumptions about the existence of specific aria attributes that allow us to navigate the component's DOM structure. Similarly, we can also expect that the component +permits specific interaction patterns described by the ARIA pattern specification and thus accurately simulate those interactions, using the aforementioned aria attributes to target the proper node +within the component or to verify that the component's state has changed appropriately post-interaction. By providing utilities to simulate these standard interaction and getters that +allow the user to easily look up the subcomponents of the component itself, we hope to simplify the overall test writing experience, leading towards easier adoption. + +These test utilities were inspired by various issues and observations that the maintainers of this library and consumers have experienced when writing tests against our components over the years. It is still very much +a work in progress so if you discover any issues or have any feedback please feel free to report them via [GitHub issues](https://github.com/adobe/react-spectrum/issues)! If you have implemented +any testing utilities yourself that you feel would be a good fit, we would be happy to field any pull request! Please read our [contributing guide](contribute.html) +for more information. ### Installation @@ -44,6 +58,8 @@ TODO need to update this so PatternNames actually comes through with the expecte #### Combobox +TODO need to update the constructor options so the extended types can actually be expanded + #### Gridlist diff --git a/packages/react-aria-components/test/Menu.test.js b/packages/react-aria-components/test/Menu.test.js index d31add9c079..77b4df6df4e 100644 --- a/packages/react-aria-components/test/Menu.test.js +++ b/packages/react-aria-components/test/Menu.test.js @@ -1031,7 +1031,7 @@ describe('Menu', () => { let submenu = submenuUtil.menu; expect(submenu).toBeInTheDocument(); - let submenuItems = submenuUtil.options; + let submenuItems = submenuUtil.options(); expect(submenuItems).toHaveLength(6); let groupsInSubmenu = submenuUtil.sections; diff --git a/packages/react-aria-components/test/Select.test.js b/packages/react-aria-components/test/Select.test.js index 1d620f85f40..aee26a60a51 100644 --- a/packages/react-aria-components/test/Select.test.js +++ b/packages/react-aria-components/test/Select.test.js @@ -73,7 +73,7 @@ describe('Select', () => { expect(listbox.closest('.react-aria-Popover')).toBeInTheDocument(); expect(listbox.closest('.react-aria-Popover')).toHaveAttribute('data-trigger', 'Select'); - let options = selectTester.options; + let options = selectTester.options(); expect(options).toHaveLength(3); await user.click(options[1]); diff --git a/packages/react-aria-components/test/Table.test.js b/packages/react-aria-components/test/Table.test.js index 8acab554cb8..76d752cfbda 100644 --- a/packages/react-aria-components/test/Table.test.js +++ b/packages/react-aria-components/test/Table.test.js @@ -214,7 +214,7 @@ describe('Table', () => { expect(cell).toHaveAttribute('class', 'react-aria-Cell'); } - for (let cell of tableTester.cells) { + for (let cell of tableTester.cells()) { expect(cell).toHaveAttribute('class', 'react-aria-Cell'); } }); From 4b5bad1cce6389d918bac8b93e16a76d871b231b Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Thu, 3 Oct 2024 15:38:30 -0700 Subject: [PATCH 04/19] add instalation, setup, and update method types --- packages/@react-aria/test-utils/package.json | 1 - .../@react-aria/test-utils/src/combobox.ts | 32 ++++++-- .../@react-aria/test-utils/src/gridlist.ts | 46 ++++++++++-- packages/@react-aria/test-utils/src/menu.ts | 56 ++++++++++++-- packages/@react-aria/test-utils/src/select.ts | 36 +++++++-- packages/@react-aria/test-utils/src/table.ts | 73 ++++++++++++++++--- packages/@react-aria/test-utils/src/user.ts | 28 +++---- .../dev/docs/pages/react-aria/testing.mdx | 47 +++++++++++- yarn.lock | 1 - 9 files changed, 268 insertions(+), 52 deletions(-) diff --git a/packages/@react-aria/test-utils/package.json b/packages/@react-aria/test-utils/package.json index e2a3c362b37..f0b5026a95b 100644 --- a/packages/@react-aria/test-utils/package.json +++ b/packages/@react-aria/test-utils/package.json @@ -27,7 +27,6 @@ "peerDependencies": { "@testing-library/react": "^15.0.7", "@testing-library/user-event": "^13.0.0 || ^14.0.0", - "jest": "^29.5.0", "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" }, "publishConfig": { diff --git a/packages/@react-aria/test-utils/src/combobox.ts b/packages/@react-aria/test-utils/src/combobox.ts index bfdde2d37f4..51922537ec4 100644 --- a/packages/@react-aria/test-utils/src/combobox.ts +++ b/packages/@react-aria/test-utils/src/combobox.ts @@ -13,10 +13,32 @@ import {act, waitFor, within} from '@testing-library/react'; import {BaseTesterOpts, UserOpts} from './user'; -export interface ComboBoxOptions extends UserOpts, BaseTesterOpts { +export interface ComboBoxTesterOpts extends UserOpts, BaseTesterOpts { user: any, trigger?: HTMLElement } +interface ComboBoxOpenOpts { + /** + * Whether the combobox opens on focus or needs to be manually opened via user action. + * @default 'manual' + */ + triggerBehavior?: 'focus' | 'manual', + /** + * What interaction type to use when opening the combobox. Defaults to the interaction type set on the tester. + */ + interactionType?: UserOpts['interactionType'] +} + +interface ComboBoxSelectOpts extends ComboBoxOpenOpts { + /** + * The option node to select. Option nodes can be sourced via `options()`. + */ + option?: HTMLElement, + /** + * The text of the node to look for when selecting a option. Alternative to `option`. + */ + optionText?: string +} export class ComboBoxTester { private user; @@ -24,7 +46,7 @@ export class ComboBoxTester { private _combobox: HTMLElement; private _trigger: HTMLElement; - constructor(opts: ComboBoxOptions) { + constructor(opts: ComboBoxTesterOpts) { let {root, trigger, user, interactionType} = opts; this.user = user; this._interactionType = interactionType || 'mouse'; @@ -62,7 +84,7 @@ export class ComboBoxTester { /** * Opens the combobox dropdown. Defaults to using the interaction type set on the combobox tester. */ - async open(opts: {triggerBehavior?: 'focus' | 'manual', interactionType?: UserOpts['interactionType']} = {}) { + async open(opts: ComboBoxOpenOpts = {}) { let {triggerBehavior = 'manual', interactionType = this._interactionType} = opts; let trigger = this.trigger; let combobox = this.combobox; @@ -108,7 +130,7 @@ export class ComboBoxTester { * Selects the desired combobox option. Defaults to using the interaction type set on the combobox tester. If necessary, will open the combobox dropdown beforehand. * The desired option can be targeted via the option's node or the option's text. */ - async selectOption(opts: {option?: HTMLElement, optionText?: string, triggerBehavior?: 'focus' | 'manual', interactionType?: UserOpts['interactionType']} = {}) { + async selectOption(opts: ComboBoxSelectOpts = {}) { let {optionText, option, triggerBehavior, interactionType = this._interactionType} = opts; if (!this.combobox.getAttribute('aria-controls')) { await this.open({triggerBehavior}); @@ -192,7 +214,7 @@ export class ComboBoxTester { } /** - * Returns the combobox's options if present. Can be filtered to a subsection of the listbox if provided. + * Returns the combobox's options if present. Can be filtered to a subsection of the listbox if provided via `element`. */ options(opts: {element?: HTMLElement} = {}): HTMLElement[] { let {element = this.listbox} = opts; diff --git a/packages/@react-aria/test-utils/src/gridlist.ts b/packages/@react-aria/test-utils/src/gridlist.ts index 8f6dfdd15e5..e609aeef2d7 100644 --- a/packages/@react-aria/test-utils/src/gridlist.ts +++ b/packages/@react-aria/test-utils/src/gridlist.ts @@ -14,16 +14,52 @@ import {act, within} from '@testing-library/react'; import {BaseTesterOpts, UserOpts} from './user'; import {pressElement} from './events'; -export interface GridListOptions extends UserOpts, BaseTesterOpts { +export interface GridListTesterOpts extends UserOpts, BaseTesterOpts { user: any } + +// TODO: this is a bit inconsistent from combobox, perhaps should also take node or combobox should also have find row +interface GridListToggleRowOpts { + /** + * The index of the row to toggle selection for. + */ + index?: number, + /** + * The text of the row to toggle selection for. Alternative to `index`. + */ + text?: string, + /** + * What interaction type to use when toggling the row selection. Defaults to the interaction type set on the tester. + */ + interactionType?: UserOpts['interactionType'] +} + +interface GridListRowActionOpts { + /** + * The index of the row to trigger its action for. + */ + index?: number, + /** + * The text of the row to trigger its action for. Alternative to `index`. + */ + text?: string, + /** + * What interaction type to use when triggering the row's action. Defaults to the interaction type set on the tester. + */ + interactionType?: UserOpts['interactionType'], + /** + * Whether or not the grid list needs a double click to trigger the row action. Depends on the grid list's implementation. + */ + needsDoubleClick?: boolean +} + export class GridListTester { private user; private _interactionType: UserOpts['interactionType']; private _gridlist: HTMLElement; - constructor(opts: GridListOptions) { + constructor(opts: GridListTesterOpts) { let {root, user, interactionType} = opts; this.user = user; this._interactionType = interactionType || 'mouse'; @@ -45,7 +81,7 @@ export class GridListTester { /** * Toggles the selection for the specified gridlist row. Defaults to using the interaction type set on the gridlist tester. */ - async toggleRowSelection(opts: {index?: number, text?: string, interactionType?: UserOpts['interactionType']} = {}) { + async toggleRowSelection(opts: GridListToggleRowOpts = {}) { let {index, text, interactionType = this._interactionType} = opts; let row = this.findRow({index, text}); @@ -87,7 +123,7 @@ export class GridListTester { /** * Triggers the action for the specified gridlist row. Defaults to using the interaction type set on the gridlist tester. */ - async triggerRowAction(opts: {index?: number, text?: string, needsDoubleClick?: boolean, interactionType?: UserOpts['interactionType']}) { + async triggerRowAction(opts: GridListRowActionOpts) { let { index, text, @@ -131,7 +167,7 @@ export class GridListTester { } /** - * Returns the gridlist's cells if any. Can be filtered against a specific row if provided. + * Returns the gridlist's cells if any. Can be filtered against a specific row if provided via `element`. */ cells(opts: {element?: HTMLElement} = {}): HTMLElement[] { let {element = this.gridlist} = opts; diff --git a/packages/@react-aria/test-utils/src/menu.ts b/packages/@react-aria/test-utils/src/menu.ts index 041b3f05778..7b983fe4a08 100644 --- a/packages/@react-aria/test-utils/src/menu.ts +++ b/packages/@react-aria/test-utils/src/menu.ts @@ -14,16 +14,60 @@ import {act, waitFor, within} from '@testing-library/react'; import {BaseTesterOpts, UserOpts} from './user'; import {triggerLongPress} from './events'; -export interface MenuOptions extends UserOpts, BaseTesterOpts { +export interface MenuTesterOpts extends UserOpts, BaseTesterOpts { user: any } + +interface MenuOpenOpts { + /** + * Whether the menu needs to be long pressed to open. + */ + needsLongPress?: boolean, + /** + * What interaction type to use when opening the menu. Defaults to the interaction type set on the tester. + */ + interactionType?: UserOpts['interactionType'] +} + +interface MenuSelectOpts extends MenuOpenOpts { + /** + * The option node to select. Option nodes can be sourced via `options()`. + */ + option?: HTMLElement, + /** + * The text of the node to look for when selecting a option. Alternative to `option`. + */ + optionText?: string, + /** + * The menu's selection mode. Will affect whether or not the menu is expected to be closed upon option selection. + * @default 'single' + */ + menuSelectionMode?: 'single' | 'multiple', + /** + * Whether or not the menu closes on select. Depends on menu implementation and configuration. + * @default true + */ + closesOnSelect?: boolean +} + +interface MenuOpenSubmenuOpts extends MenuOpenOpts { + /** + * The submenu trigger to open. Available submenu trigger nodes can be sourced via `submenuTriggers`. + */ + submenuTrigger?: HTMLElement, + /** + * The text of submenu trigger to open. Alternative to `submenuTrigger`. + */ + submenuTriggerText?: string +} + export class MenuTester { private user; private _interactionType: UserOpts['interactionType']; private _advanceTimer: UserOpts['advanceTimer']; private _trigger: HTMLElement; - constructor(opts: MenuOptions) { + constructor(opts: MenuTesterOpts) { let {root, user, interactionType, advanceTimer} = opts; this.user = user; this._interactionType = interactionType || 'mouse'; @@ -55,7 +99,7 @@ export class MenuTester { /** * Opens the menu. Defaults to using the interaction type set on the menu tester. */ - async open(opts: {needsLongPress?: boolean, interactionType?: UserOpts['interactionType']} = {}) { + async open(opts: MenuOpenOpts = {}) { let { needsLongPress, interactionType = this._interactionType @@ -105,7 +149,7 @@ export class MenuTester { * Selects the desired menu option. Defaults to using the interaction type set on the menu tester. If necessary, will open the menu dropdown beforehand. * The desired option can be targeted via the option's node or the option's text. */ - async selectOption(opts: {option?: HTMLElement, optionText?: string, menuSelectionMode?: 'single' | 'multiple', needsLongPress?: boolean, closesOnSelect?: boolean, interactionType?: UserOpts['interactionType']}) { + async selectOption(opts: MenuSelectOpts) { let { optionText, menuSelectionMode = 'single', @@ -162,7 +206,7 @@ export class MenuTester { /** * Opens the submenu. Defaults to using the interaction type set on the menu tester. The submenu trigger can be targeted via the trigger's node or the trigger's text. */ - async openSubmenu(opts: {submenuTrigger?: HTMLElement, submenuTriggerText?: string, needsLongPress?: boolean, interactionType?: UserOpts['interactionType']}): Promise { + async openSubmenu(opts: MenuOpenSubmenuOpts): Promise { let { submenuTrigger, submenuTriggerText, @@ -245,7 +289,7 @@ export class MenuTester { } /** - * Returns the menu's options if present. Can be filtered to a subsection of the menu if provided. + * Returns the menu's options if present. Can be filtered to a subsection of the menu if provided via `element`. */ options(opts: {element?: HTMLElement} = {}): HTMLElement[] { let {element = this.menu} = opts; diff --git a/packages/@react-aria/test-utils/src/select.ts b/packages/@react-aria/test-utils/src/select.ts index 689c022c804..04d0b3e428b 100644 --- a/packages/@react-aria/test-utils/src/select.ts +++ b/packages/@react-aria/test-utils/src/select.ts @@ -13,16 +13,35 @@ import {act, waitFor, within} from '@testing-library/react'; import {BaseTesterOpts, UserOpts} from './user'; -export interface SelectOptions extends UserOpts, BaseTesterOpts { +export interface SelectTesterOpts extends UserOpts, BaseTesterOpts { // TODO: I think the type grabbed from the testing library dist for UserEvent is breaking the build, will need to figure out a better place to grab from user: any } + +interface SelectOpenOpts { + /** + * What interaction type to use when opening the select. Defaults to the interaction type set on the tester. + */ + interactionType?: UserOpts['interactionType'] +} + +interface SelectTriggerOptionOpts extends SelectOpenOpts { + /** + * The option node to select. Option nodes can be sourced via `options()`. + */ + option?: HTMLElement, + /** + * The text of the node to look for when selecting a option. Alternative to `option`. + */ + optionText?: string +} + export class SelectTester { private user; private _interactionType: UserOpts['interactionType']; private _trigger: HTMLElement; - constructor(opts: SelectOptions) { + constructor(opts: SelectTesterOpts) { let {root, user, interactionType} = opts; this.user = user; this._interactionType = interactionType || 'mouse'; @@ -43,7 +62,7 @@ export class SelectTester { /** * Opens the select. Defaults to using the interaction type set on the select tester. */ - async open(opts: {interactionType?: UserOpts['interactionType']} = {}) { + async open(opts: SelectOpenOpts = {}) { let { interactionType = this._interactionType } = opts; @@ -104,9 +123,10 @@ export class SelectTester { * Selects the desired select option. Defaults to using the interaction type set on the select tester. If necessary, will open the select dropdown beforehand. * The desired option can be targeted via the option's text. */ - async selectOption(opts: {optionText: string, interactionType?: UserOpts['interactionType']}) { + async selectOption(opts: SelectTriggerOptionOpts) { let { optionText, + option, interactionType = this._interactionType } = opts || {}; let trigger = this.trigger; @@ -115,7 +135,11 @@ export class SelectTester { } let listbox = this.listbox; if (listbox) { - let option = within(listbox).getByText(optionText); + + if (!option && optionText) { + option = within(listbox).getByText(optionText); + } + if (interactionType === 'keyboard') { if (document.activeElement !== listbox || !listbox.contains(document.activeElement)) { act(() => listbox.focus()); @@ -151,7 +175,7 @@ export class SelectTester { } /** - * Returns the select's options if present. Can be filtered to a subsection of the listbox if provided. + * Returns the select's options if present. Can be filtered to a subsection of the listbox if provided via `element`. */ options(opts: {element?: HTMLElement} = {}): HTMLElement[] { let {element = this.listbox} = opts; diff --git a/packages/@react-aria/test-utils/src/table.ts b/packages/@react-aria/test-utils/src/table.ts index 2fa7aec5117..2653bd9488a 100644 --- a/packages/@react-aria/test-utils/src/table.ts +++ b/packages/@react-aria/test-utils/src/table.ts @@ -13,21 +13,72 @@ import {act, fireEvent, waitFor, within} from '@testing-library/react'; import {BaseTesterOpts, UserOpts} from './user'; import {pressElement, triggerLongPress} from './events'; -export interface TableOptions extends UserOpts, BaseTesterOpts { +export interface TableTesterOpts extends UserOpts, BaseTesterOpts { user: any, advanceTimer: UserOpts['advanceTimer'] } -// TODO: Previously used logic like https://github.com/testing-library/react-testing-library/blame/c63b873072d62c858959c2a19e68f8e2cc0b11be/src/pure.js#L16 -// but https://github.com/testing-library/dom-testing-library/issues/987#issuecomment-891901804 indicates that it may falsely indicate that fake timers are enabled -// when they aren't +// TODO: this is a bit inconsistent from combobox, perhaps should also take node or combobox should also have find row +interface TableToggleRowOpts { + /** + * The index of the row to toggle selection for. + */ + index?: number, + /** + * The text of the row to toggle selection for. Alternative to `index`. + */ + text?: string, + /** + * Whether the row needs to be long pressed to be selected. Depends on the table's implementation. + */ + needsLongPress?: boolean, + /** + * What interaction type to use when toggling the row selection. Defaults to the interaction type set on the tester. + */ + interactionType?: UserOpts['interactionType'] +} + +interface TableToggleSortOpts { + /** + * The index of the column to sort. + */ + index?: number, + /** + * The text of the column to sort. Alternative to `index`. + */ + text?: string, + /** + * What interaction type to use when sorting the column. Defaults to the interaction type set on the tester. + */ + interactionType?: UserOpts['interactionType'] +} + +interface TableRowActionOpts { + /** + * The index of the row to trigger its action for. + */ + index?: number, + /** + * The text of the row to trigger its action for. Alternative to `index`. + */ + text?: string, + /** + * What interaction type to use when triggering the row's action. Defaults to the interaction type set on the tester. + */ + interactionType?: UserOpts['interactionType'], + /** + * Whether or not the table needs a double click to trigger the row action. Depends on the table's implementation. + */ + needsDoubleClick?: boolean +} + export class TableTester { private user; private _interactionType: UserOpts['interactionType']; private _advanceTimer: UserOpts['advanceTimer']; private _table: HTMLElement; - constructor(opts: TableOptions) { + constructor(opts: TableTesterOpts) { let {root, user, interactionType, advanceTimer} = opts; this.user = user; this._interactionType = interactionType || 'mouse'; @@ -43,9 +94,9 @@ export class TableTester { } /** - * Toggles the selection for the specified table row. Defaults to using the interaction type set on the gridlist tester. + * Toggles the selection for the specified table row. Defaults to using the interaction type set on the table tester. */ - async toggleRowSelection(opts: {index?: number, text?: string, needsLongPress?: boolean, interactionType?: UserOpts['interactionType']} = {}) { + async toggleRowSelection(opts: TableToggleRowOpts = {}) { let { index, text, @@ -86,9 +137,9 @@ export class TableTester { } /** - * Toggles the sort order for the specified table column. Defaults to using the interaction type set on the gridlist tester. + * Toggles the sort order for the specified table column. Defaults to using the interaction type set on the table tester. */ - async toggleSort(opts: {index?: number, text?: string, interactionType?: UserOpts['interactionType']} = {}) { + async toggleSort(opts: TableToggleSortOpts = {}) { let { index, text, @@ -179,7 +230,7 @@ export class TableTester { /** * Triggers the action for the specified table row. Defaults to using the interaction type set on the table tester. */ - async triggerRowAction(opts: {index?: number, text?: string, needsDoubleClick?: boolean, interactionType?: UserOpts['interactionType']} = {}) { + async triggerRowAction(opts: TableRowActionOpts = {}) { let { index, text, @@ -313,7 +364,7 @@ export class TableTester { } /** - * Returns the cells within the table if any. Can be filtered against a specific row if provided. + * Returns the cells within the table if any. Can be filtered against a specific row if provided via `element`. */ cells(opts: {element?: HTMLElement} = {}): HTMLElement[] { let {element = this.table} = opts; diff --git a/packages/@react-aria/test-utils/src/user.ts b/packages/@react-aria/test-utils/src/user.ts index 0b0e0b98470..fcc56089214 100644 --- a/packages/@react-aria/test-utils/src/user.ts +++ b/packages/@react-aria/test-utils/src/user.ts @@ -10,12 +10,12 @@ * governing permissions and limitations under the License. */ -import {ComboBoxOptions, ComboBoxTester} from './combobox'; -import {GridListOptions, GridListTester} from './gridlist'; -import {MenuOptions, MenuTester} from './menu'; +import {ComboBoxTester, ComboBoxTesterOpts} from './combobox'; +import {GridListTester, GridListTesterOpts} from './gridlist'; +import {MenuTester, MenuTesterOpts} from './menu'; import {pointerMap} from './'; -import {SelectOptions, SelectTester} from './select'; -import {TableOptions, TableTester} from './table'; +import {SelectTester, SelectTesterOpts} from './select'; +import {TableTester, TableTesterOpts} from './table'; import userEvent from '@testing-library/user-event'; // https://github.com/testing-library/dom-testing-library/issues/939#issuecomment-830771708 is an interesting way of allowing users to configure the timers @@ -45,7 +45,7 @@ let keyToUtil = {'Select': SelectTester, 'Table': TableTester, 'Menu': MenuTeste export type PatternNames = keyof typeof keyToUtil; // Conditional type: https://www.typescriptlang.org/docs/handbook/2/conditional-types.html -type ObjectType = +type Tester = T extends 'Select' ? SelectTester : T extends 'Table' ? TableTester : T extends 'Menu' ? MenuTester : @@ -53,12 +53,12 @@ type ObjectType = T extends 'GridList' ? GridListTester : never; -type ObjectOptionsTypes = - T extends 'Select' ? SelectOptions : - T extends 'Table' ? TableOptions : - T extends 'Menu' ? MenuOptions : - T extends 'ComboBox' ? ComboBoxOptions : - T extends 'GridList' ? GridListOptions : +type TesterOpts = + T extends 'Select' ? SelectTesterOpts : + T extends 'Table' ? TableTesterOpts : + T extends 'Menu' ? MenuTesterOpts : + T extends 'ComboBox' ? ComboBoxTesterOpts : + T extends 'GridList' ? GridListTesterOpts : never; let defaultAdvanceTimer = async (waitTime: number | undefined) => await new Promise((resolve) => setTimeout(resolve, waitTime)); @@ -86,7 +86,7 @@ export class User { /** * Creates an aria pattern tester, inheriting the options provided to the original user. */ - createTester(patternName: T, opts: ObjectOptionsTypes): ObjectType { - return new (keyToUtil)[patternName]({...opts, user: this.user, interactionType: this.interactionType, advanceTimer: this.advanceTimer}) as ObjectType; + createTester(patternName: T, opts: TesterOpts): Tester { + return new (keyToUtil)[patternName]({...opts, user: this.user, interactionType: this.interactionType, advanceTimer: this.advanceTimer}) as Tester; } } diff --git a/packages/dev/docs/pages/react-aria/testing.mdx b/packages/dev/docs/pages/react-aria/testing.mdx index 04377d5582d..db992e65f1a 100644 --- a/packages/dev/docs/pages/react-aria/testing.mdx +++ b/packages/dev/docs/pages/react-aria/testing.mdx @@ -15,7 +15,7 @@ import gridlist from 'docs:@react-aria/test-utils/src/gridlist.ts'; import menu from 'docs:@react-aria/test-utils/src/menu.ts'; import select from 'docs:@react-aria/test-utils/src/select.ts'; import table from 'docs:@react-aria/test-utils/src/table.ts'; -import {ClassAPI, FunctionAPI, InterfaceType, TypeContext} from '@react-spectrum/docs'; +import {ClassAPI, FunctionAPI, InterfaceType, TypeContext, TypeLink} from '@react-spectrum/docs'; --- category: Concepts @@ -48,20 +48,61 @@ for more information. ### Installation +`@react-aria/test-utils` can be installed using a package manager like [npm](https://docs.npmjs.com/cli/npm) or [yarn](https://classic.yarnpkg.com/lang/en/). + +``` +yarn add --dev @react-aria/test-utils +``` + +Please note that this library uses [@testing-library/react@15](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event](https://www.npmjs.com/package/@testing-library/user-event/v/13.1.5). This means that you need +to be on React 18+ in order for these utilities to work. + ### Setup -### Patterns -TODO perhaps these should live in each RAC pages specifically and then we link from here to those pages? +Once installed, you can access the `User` that `@react-aria/test-utils` provides in your test file as shown below. This user only needs to be initialized once and accepts two options: `interactionType` and `advanceTimer`. `interactionType` will +initialize what mode of interaction (mouse, keyboard, or touch) will be used by default. This can be overridden at the pattern tester or interaction execution level if required. `advanceTimer` accepts a function that when called should advance timers (real or fake) +in the test by a given amount. This is required for certain interactions (e.g. long press) that some of the patterns support. + +Once the `User` is initialized, you can use its `createTester` method to initialize a specific ARIA pattern tester in your test cases. This gives you access to that pattern's specific utilities that you can then call +within your test to query for specific subcomponents or simulate common interactions. See [below](#patterns) for more details on what is supported for each individual ARIA pattern. + +```ts +// YourTest.test.ts +import {User} from '@react-aria/test-utils'; + +// Provide whatever method of advancing timers you use in your test, this example assumes Jest with fake timers +let testUtilUser = new User({interactionType: 'mouse', advanceTimer: jest.advanceTimersByTime}); +// ... + +it('my test case', async function () { + // Render your test component/app and initialize the table tester + render(); + let table = testUtilUser.createTester('Table', {root: screen.getByTestId('test_table')}); + // ... +}); +``` + +See below for the full definition of the `User` object. TODO need to update this so PatternNames actually comes through with the expected values + +### Patterns +TODO perhaps these should live in each RAC pages specifically and then we link from here to those pages? + +Below is a list of the ARIA patterns testers currently supported by `createTester`. + #### Combobox TODO need to update the constructor options so the extended types can actually be expanded +``` + +``` + #### Gridlist diff --git a/yarn.lock b/yarn.lock index e3234482525..1832f1e6516 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6365,7 +6365,6 @@ __metadata: peerDependencies: "@testing-library/react": ^15.0.7 "@testing-library/user-event": ^13.0.0 || ^14.0.0 - jest: ^29.5.0 react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 languageName: unknown linkType: soft From 4b28f245db605de6ce453c0e98a69af0feca079a Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 4 Oct 2024 14:40:23 -0700 Subject: [PATCH 05/19] update docs with examples and update types --- .../@react-aria/test-utils/src/combobox.ts | 6 +- .../@react-aria/test-utils/src/gridlist.ts | 6 +- packages/@react-aria/test-utils/src/index.ts | 2 +- packages/@react-aria/test-utils/src/menu.ts | 6 +- packages/@react-aria/test-utils/src/select.ts | 10 +-- packages/@react-aria/test-utils/src/table.ts | 6 +- packages/@react-aria/test-utils/src/types.ts | 84 +++++++++++++++++++ packages/@react-aria/test-utils/src/user.ts | 54 ++++-------- .../combobox/docs/ComboBox.mdx | 34 +++++++- .../@react-spectrum/list/docs/ListView.mdx | 41 ++++++++- .../@react-spectrum/menu/docs/MenuTrigger.mdx | 38 ++++++++- .../@react-spectrum/picker/docs/Picker.mdx | 32 ++++++- .../@react-spectrum/table/docs/TableView.mdx | 42 +++++++++- .../dev/docs/pages/react-aria/testing.mdx | 33 ++------ .../dev/docs/pages/react-spectrum/testing.mdx | 6 ++ .../react-aria-components/docs/ComboBox.mdx | 36 +++++++- .../react-aria-components/docs/GridList.mdx | 45 +++++++++- packages/react-aria-components/docs/Menu.mdx | 42 +++++++++- .../react-aria-components/docs/Select.mdx | 33 +++++++- packages/react-aria-components/docs/Table.mdx | 46 +++++++++- 20 files changed, 498 insertions(+), 104 deletions(-) create mode 100644 packages/@react-aria/test-utils/src/types.ts diff --git a/packages/@react-aria/test-utils/src/combobox.ts b/packages/@react-aria/test-utils/src/combobox.ts index 51922537ec4..f3d4e2cd6d6 100644 --- a/packages/@react-aria/test-utils/src/combobox.ts +++ b/packages/@react-aria/test-utils/src/combobox.ts @@ -11,12 +11,8 @@ */ import {act, waitFor, within} from '@testing-library/react'; -import {BaseTesterOpts, UserOpts} from './user'; +import {ComboBoxTesterOpts, UserOpts} from './types'; -export interface ComboBoxTesterOpts extends UserOpts, BaseTesterOpts { - user: any, - trigger?: HTMLElement -} interface ComboBoxOpenOpts { /** * Whether the combobox opens on focus or needs to be manually opened via user action. diff --git a/packages/@react-aria/test-utils/src/gridlist.ts b/packages/@react-aria/test-utils/src/gridlist.ts index e609aeef2d7..713da7b2b92 100644 --- a/packages/@react-aria/test-utils/src/gridlist.ts +++ b/packages/@react-aria/test-utils/src/gridlist.ts @@ -11,13 +11,9 @@ */ import {act, within} from '@testing-library/react'; -import {BaseTesterOpts, UserOpts} from './user'; +import {GridListTesterOpts, UserOpts} from './types'; import {pressElement} from './events'; -export interface GridListTesterOpts extends UserOpts, BaseTesterOpts { - user: any -} - // TODO: this is a bit inconsistent from combobox, perhaps should also take node or combobox should also have find row interface GridListToggleRowOpts { /** diff --git a/packages/@react-aria/test-utils/src/index.ts b/packages/@react-aria/test-utils/src/index.ts index 962abbf72c3..b5b7da34492 100644 --- a/packages/@react-aria/test-utils/src/index.ts +++ b/packages/@react-aria/test-utils/src/index.ts @@ -15,4 +15,4 @@ export {installMouseEvent, installPointerEvent} from './testSetup'; export {pointerMap} from './userEventMaps'; export {User} from './user'; -export type {UserOpts} from './user'; +export type {UserOpts} from './types'; diff --git a/packages/@react-aria/test-utils/src/menu.ts b/packages/@react-aria/test-utils/src/menu.ts index 7b983fe4a08..5f4de1b8be5 100644 --- a/packages/@react-aria/test-utils/src/menu.ts +++ b/packages/@react-aria/test-utils/src/menu.ts @@ -11,13 +11,9 @@ */ import {act, waitFor, within} from '@testing-library/react'; -import {BaseTesterOpts, UserOpts} from './user'; +import {MenuTesterOpts, UserOpts} from './types'; import {triggerLongPress} from './events'; -export interface MenuTesterOpts extends UserOpts, BaseTesterOpts { - user: any -} - interface MenuOpenOpts { /** * Whether the menu needs to be long pressed to open. diff --git a/packages/@react-aria/test-utils/src/select.ts b/packages/@react-aria/test-utils/src/select.ts index 04d0b3e428b..c8c5a4d29af 100644 --- a/packages/@react-aria/test-utils/src/select.ts +++ b/packages/@react-aria/test-utils/src/select.ts @@ -11,12 +11,7 @@ */ import {act, waitFor, within} from '@testing-library/react'; -import {BaseTesterOpts, UserOpts} from './user'; - -export interface SelectTesterOpts extends UserOpts, BaseTesterOpts { - // TODO: I think the type grabbed from the testing library dist for UserEvent is breaking the build, will need to figure out a better place to grab from - user: any -} +import {SelectTesterOpts, UserOpts} from './types'; interface SelectOpenOpts { /** @@ -135,7 +130,6 @@ export class SelectTester { } let listbox = this.listbox; if (listbox) { - if (!option && optionText) { option = within(listbox).getByText(optionText); } @@ -158,7 +152,7 @@ export class SelectTester { } } - if (option.getAttribute('href') == null) { + if (option?.getAttribute('href') == null) { await waitFor(() => { if (document.activeElement !== this._trigger) { throw new Error(`Expected the document.activeElement after selecting an option to be the select component trigger but got ${document.activeElement}`); diff --git a/packages/@react-aria/test-utils/src/table.ts b/packages/@react-aria/test-utils/src/table.ts index 2653bd9488a..9d52af082a4 100644 --- a/packages/@react-aria/test-utils/src/table.ts +++ b/packages/@react-aria/test-utils/src/table.ts @@ -11,12 +11,8 @@ */ import {act, fireEvent, waitFor, within} from '@testing-library/react'; -import {BaseTesterOpts, UserOpts} from './user'; import {pressElement, triggerLongPress} from './events'; -export interface TableTesterOpts extends UserOpts, BaseTesterOpts { - user: any, - advanceTimer: UserOpts['advanceTimer'] -} +import {TableTesterOpts, UserOpts} from './types'; // TODO: this is a bit inconsistent from combobox, perhaps should also take node or combobox should also have find row interface TableToggleRowOpts { diff --git a/packages/@react-aria/test-utils/src/types.ts b/packages/@react-aria/test-utils/src/types.ts new file mode 100644 index 00000000000..3f58dad1f51 --- /dev/null +++ b/packages/@react-aria/test-utils/src/types.ts @@ -0,0 +1,84 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +// https://github.com/testing-library/dom-testing-library/issues/939#issuecomment-830771708 is an interesting way of allowing users to configure the timers +// curent way is like https://testing-library.com/docs/user-event/options/#advancetimers, +export interface UserOpts { + /** + * The interaction type (mouse, touch, keyboard) that the test util user will use when interacting with a component. This can be overridden + * at the aria pattern tester level if needed. + * @default mouse + */ + interactionType?: 'mouse' | 'touch' | 'keyboard', + // If using fake timers user should provide something like (time) => jest.advanceTimersByTime(time))} + // A real timer user would pass async () => await new Promise((resolve) => setTimeout(resolve, waitTime)) + // Time is in ms. + /** + * A function used by the test utils to advance timers during interactions. Required for certain aria patterns (e.g. table). This can be overridden + * at the aria pattern tester level if needed. + */ + advanceTimer?: (time?: number) => void | Promise +} + +export interface BaseTesterOpts { + /** The base element for the given tester (e.g. the table, menu trigger button, etc). */ + root: HTMLElement +} + +export interface ComboBoxTesterOpts extends UserOpts, BaseTesterOpts { + /** @private */ + user: any, + /** + * The base element for the combobox. If provided the wrapping element around the target combobox (as is the the case with a ref provided to RSP ComboBox), + * will automatically search for the combobox element within. + */ + root: HTMLElement, + /** + * The node of the combobox trigger button if any. If not provided, we will try to automatically use any button + * within the `root` provided or that the `root` serves as the trigger. + */ + trigger?: HTMLElement +} + +export interface GridListTesterOpts extends UserOpts, BaseTesterOpts { + /** @private */ + user: any +} + +export interface MenuTesterOpts extends UserOpts, BaseTesterOpts { + /** @private */ + user: any, + /** + * The trigger element for the menu. + */ + root: HTMLElement +} + +export interface SelectTesterOpts extends UserOpts, BaseTesterOpts { + // TODO: I think the type grabbed from the testing library dist for UserEvent is breaking the build, will need to figure out a better place to grab from + /** @private */ + user: any, + /** + * The trigger element for the select. If provided the wrapping element around the target select (as is the case with a ref provided to RSP Select), + * will automatically search for the select's trigger element within. + */ + root: HTMLElement +} + +export interface TableTesterOpts extends UserOpts, BaseTesterOpts { + /** @private */ + user: any, + /** + * A function used by the test utils to advance timers during interactions. + */ + advanceTimer: UserOpts['advanceTimer'] +} diff --git a/packages/@react-aria/test-utils/src/user.ts b/packages/@react-aria/test-utils/src/user.ts index fcc56089214..b86702149f8 100644 --- a/packages/@react-aria/test-utils/src/user.ts +++ b/packages/@react-aria/test-utils/src/user.ts @@ -10,55 +10,33 @@ * governing permissions and limitations under the License. */ -import {ComboBoxTester, ComboBoxTesterOpts} from './combobox'; -import {GridListTester, GridListTesterOpts} from './gridlist'; -import {MenuTester, MenuTesterOpts} from './menu'; +import {ComboBoxTester} from './combobox'; +import {ComboBoxTesterOpts, GridListTesterOpts, MenuTesterOpts, SelectTesterOpts, TableTesterOpts, UserOpts} from './types'; +import {GridListTester} from './gridlist'; +import {MenuTester} from './menu'; import {pointerMap} from './'; -import {SelectTester, SelectTesterOpts} from './select'; -import {TableTester, TableTesterOpts} from './table'; +import {SelectTester} from './select'; +import {TableTester} from './table'; import userEvent from '@testing-library/user-event'; -// https://github.com/testing-library/dom-testing-library/issues/939#issuecomment-830771708 is an interesting way of allowing users to configure the timers -// curent way is like https://testing-library.com/docs/user-event/options/#advancetimers, -export interface UserOpts { - /** - * The interaction type (mouse, touch, keyboard) that the test util user will use when interacting with a component. This can be overridden - * at the aria pattern util level if needed. - * @default mouse - */ - interactionType?: 'mouse' | 'touch' | 'keyboard', - // If using fake timers user should provide something like (time) => jest.advanceTimersByTime(time))} - // A real timer user would pass async () => await new Promise((resolve) => setTimeout(resolve, waitTime)) - // Time is in ms. - /** - * A function used by the test utils to advance timers during interactions. Required for certain aria patterns (e.g. table). - */ - advanceTimer?: (time?: number) => void | Promise -} - -export interface BaseTesterOpts { - // The base element for the given tester (e.g. the table, menu trigger, etc) - root: HTMLElement -} - let keyToUtil = {'Select': SelectTester, 'Table': TableTester, 'Menu': MenuTester, 'ComboBox': ComboBoxTester, 'GridList': GridListTester} as const; export type PatternNames = keyof typeof keyToUtil; // Conditional type: https://www.typescriptlang.org/docs/handbook/2/conditional-types.html type Tester = - T extends 'Select' ? SelectTester : - T extends 'Table' ? TableTester : - T extends 'Menu' ? MenuTester : - T extends 'ComboBox' ? ComboBoxTester : - T extends 'GridList' ? GridListTester : - never; + T extends 'ComboBox' ? ComboBoxTester : + T extends 'GridList' ? GridListTester : + T extends 'Menu' ? MenuTester : + T extends 'Select' ? SelectTester : + T extends 'Table' ? TableTester : + never; type TesterOpts = - T extends 'Select' ? SelectTesterOpts : - T extends 'Table' ? TableTesterOpts : - T extends 'Menu' ? MenuTesterOpts : T extends 'ComboBox' ? ComboBoxTesterOpts : T extends 'GridList' ? GridListTesterOpts : + T extends 'Menu' ? MenuTesterOpts : + T extends 'Select' ? SelectTesterOpts : + T extends 'Table' ? TableTesterOpts : never; let defaultAdvanceTimer = async (waitTime: number | undefined) => await new Promise((resolve) => setTimeout(resolve, waitTime)); @@ -87,6 +65,6 @@ export class User { * Creates an aria pattern tester, inheriting the options provided to the original user. */ createTester(patternName: T, opts: TesterOpts): Tester { - return new (keyToUtil)[patternName]({...opts, user: this.user, interactionType: this.interactionType, advanceTimer: this.advanceTimer}) as Tester; + return new (keyToUtil)[patternName]({user: this.user, interactionType: this.interactionType, advanceTimer: this.advanceTimer, ...opts}) as Tester; } } diff --git a/packages/@react-spectrum/combobox/docs/ComboBox.mdx b/packages/@react-spectrum/combobox/docs/ComboBox.mdx index 9b2f1f9e5e0..b8d96bbaf46 100644 --- a/packages/@react-spectrum/combobox/docs/ComboBox.mdx +++ b/packages/@react-spectrum/combobox/docs/ComboBox.mdx @@ -11,8 +11,9 @@ import {Layout} from '@react-spectrum/docs'; export default Layout; import docs from 'docs:@react-spectrum/combobox'; +import comboboxUtils from 'docs:@react-aria/test-utils/src/combobox.ts'; import packageData from '@react-spectrum/combobox/package.json'; -import {HeaderInfo, PropTable, PageDescription} from '@react-spectrum/docs'; +import {HeaderInfo, PropTable, PageDescription, ClassAPI} from '@react-spectrum/docs'; ```jsx import import Add from '@spectrum-icons/workflow/Add'; @@ -992,3 +993,34 @@ behaviors in your test suite. Please also refer to [React Spectrum's test suite](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/combobox/test/ComboBox.test.js) if you find that the above isn't sufficient when resolving issues in your own test cases. + +`@react-aria/test-utils` also offers common combobox interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +in your tests. Below is the full definition of the combobox tester and a sample of how you could use it in your test suite. + + + +```ts +// Combobox.test.ts +import {User} from '@react-aria/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse'}); +// ... + +it('ComboBox can select an option via keyboard', async function () { + // Render your test component/app and initialize the combobox tester + let {getByTestId} = render( + + ... + + ); + let comboboxTester = testUtilUser.createTester('ComboBox', {root: getByTestId('test-combobox'), interactionType: 'keyboard'}); + + await comboboxTester.open(); + expect(comboboxTester.listbox).toBeTruthy(); + + let options = comboboxTester.options(); + await comboboxTester.selectOption({option: options[0]}); + expect(comboboxTester.combobox.value).toBe('One'); + expect(comboboxTester.listbox).toBeFalsy(); +}); +``` diff --git a/packages/@react-spectrum/list/docs/ListView.mdx b/packages/@react-spectrum/list/docs/ListView.mdx index 362fa71631f..2e6723f787c 100644 --- a/packages/@react-spectrum/list/docs/ListView.mdx +++ b/packages/@react-spectrum/list/docs/ListView.mdx @@ -14,7 +14,8 @@ import Anatomy from './anatomy.svg'; import ChevronRight from '@spectrum-icons/workflow/ChevronRight'; import docs from 'docs:@react-spectrum/list'; import dndDocs from 'docs:@react-spectrum/dnd'; -import {HeaderInfo, PropTable, PageDescription, TypeLink} from '@react-spectrum/docs'; +import gridlistUtil from 'docs:@react-aria/test-utils/src/gridlist.ts'; +import {HeaderInfo, PropTable, PageDescription, TypeLink, ClassAPI} from '@react-spectrum/docs'; import {Keyboard} from '@react-spectrum/text'; import packageData from '@react-spectrum/list/package.json'; @@ -1191,3 +1192,41 @@ behaviors in your test suite. Please also refer to [React Spectrum's test suite](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/list/test/ListView.test.js) if you find that the above isn't sufficient when resolving issues in your own test cases. + +`@react-aria/test-utils` also offers common gridlist interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +in your tests. Below is the full definition of the gridlist tester and a sample of how you could use it in your test suite. + + + +```ts +// ListView.test.ts +import {User} from '@react-aria/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse'}); +// ... + +it('ListView can select a row via keyboard', async function () { + // Render your test component/app and initialize the gridlist tester + let {getByTestId} = render( + + ... + + ); + let gridlistTester = testUtilUser.createTester('GridList', {root: getByTestId('test-gridlist'), interactionType: 'keyboard'}); + + let row = gridListTester.rows[0]; + expect(row).not.toHaveClass('selected'); + expect(within(row).getByRole('checkbox')).not.toBeChecked(); + expect(gridListTester.selectedRows).toHaveLength(0); + + await gridListTester.toggleRowSelection({index: 0}); + expect(row).toHaveClass('selected'); + expect(within(row).getByRole('checkbox')).toBeChecked(); + expect(gridListTester.selectedRows).toHaveLength(1); + + await gridListTester.toggleRowSelection({index: 0}); + expect(row).not.toHaveClass('selected'); + expect(within(row).getByRole('checkbox')).not.toBeChecked(); + expect(gridListTester.selectedRows).toHaveLength(0); +}); +``` diff --git a/packages/@react-spectrum/menu/docs/MenuTrigger.mdx b/packages/@react-spectrum/menu/docs/MenuTrigger.mdx index ffcaade3523..3efbe16f862 100644 --- a/packages/@react-spectrum/menu/docs/MenuTrigger.mdx +++ b/packages/@react-spectrum/menu/docs/MenuTrigger.mdx @@ -11,7 +11,8 @@ import {Layout} from '@react-spectrum/docs'; export default Layout; import docs from 'docs:@react-spectrum/menu'; -import {HeaderInfo, PropTable, PageDescription} from '@react-spectrum/docs'; +import menuUtil from 'docs:@react-aria/test-utils/src/menu.ts'; +import {HeaderInfo, PropTable, PageDescription, ClassAPI} from '@react-spectrum/docs'; import packageData from '@react-spectrum/menu/package.json'; import {Keyboard} from '@react-spectrum/text'; @@ -256,3 +257,38 @@ behaviors in your test suite. Please also refer to [React Spectrum's test suite](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/menu/test/MenuTrigger.test.js) if you find that the above isn't sufficient when resolving issues in your own test cases. + +`@react-aria/test-utils` also offers common menu interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +in your tests. Below is the full definition of the menu tester and a sample of how you could use it in your test suite. + + + +```ts +// Menu.test.ts +import {User} from '@react-aria/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse'}); +// ... + +it('Menu can open its submenu via keyboard', async function () { + // Render your test component/app and initialize the menu tester + let {getByTestId} = render( + + ... + + ); + let menuTester = testUtilUser.createTester('Menu', {root: getByTestId('test-menutrigger'), interactionType: 'keyboard'}); + + await menuTester.open(); + expect(menuTester.menu).toBeInTheDocument(); + let submenuTriggers = menuTester.submenuTriggers; + expect(submenuTriggers).toHaveLength(1); + + let submenuTester = await menuTester.openSubmenu({submenuTriggerText: 'Share…'}); + expect(submenuTester.menu).toBeInTheDocument(); + + await submenuTester.selectOption({option: submenuUtil.options()[0]}); + expect(submenuTester.menu).toBeInTheDocument(); + expect(menuTester.menu).toBeInTheDocument(); +}); +``` diff --git a/packages/@react-spectrum/picker/docs/Picker.mdx b/packages/@react-spectrum/picker/docs/Picker.mdx index ba1ef556db1..f0faf73afe9 100644 --- a/packages/@react-spectrum/picker/docs/Picker.mdx +++ b/packages/@react-spectrum/picker/docs/Picker.mdx @@ -11,7 +11,8 @@ import {Layout} from '@react-spectrum/docs'; export default Layout; import docs from 'docs:@react-spectrum/picker'; -import {HeaderInfo, PropTable, PageDescription} from '@react-spectrum/docs'; +import selectUtil from 'docs:@react-aria/test-utils/src/select.ts'; +import {HeaderInfo, PropTable, PageDescription, ClassAPI} from '@react-spectrum/docs'; import packageData from '@react-spectrum/picker/package.json'; ```jsx import @@ -588,3 +589,32 @@ for more information on how to handle these behaviors in your test suite. Please also refer to [React Spectrum's test suite](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/picker/test/Picker.test.js) if you find that the above isn't sufficient when resolving issues in your own test cases. + +`@react-aria/test-utils` also offers common select interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +in your tests. Below is the full definition of the select tester and a sample of how you could use it in your test suite. + + + +```ts +// Picker.test.ts +import {User} from '@react-aria/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse'}); +// ... + +it('Picker can select an option via keyboard', async function () { + // Render your test component/app and initialize the select tester + let {getByTestId} = render( + + ... + + ); + let selectTester = testUtilUser.createTester('Select', {root: getByTestId('test-select'), interactionType: 'keyboard'}); + let trigger = selectTester.trigger; + expect(trigger).toHaveTextContent('Select an item'); + expect(trigger).not.toHaveAttribute('data-pressed'); + + await selectTester.selectOption({optionText: 'Cat'}); + expect(trigger).toHaveTextContent('Cat'); +}); +``` diff --git a/packages/@react-spectrum/table/docs/TableView.mdx b/packages/@react-spectrum/table/docs/TableView.mdx index 1ba899b6d23..18d04c7bcc1 100644 --- a/packages/@react-spectrum/table/docs/TableView.mdx +++ b/packages/@react-spectrum/table/docs/TableView.mdx @@ -12,8 +12,9 @@ export default Layout; import docs from 'docs:@react-spectrum/table'; import dndDocs from 'docs:@react-spectrum/dnd'; +import tableUtil from 'docs:@react-aria/test-utils/src/table.ts'; import tableTypes from 'docs:@react-types/table/src/index.d.ts'; -import {HeaderInfo, PropTable, PageDescription, TypeLink, VersionBadge} from '@react-spectrum/docs'; +import {HeaderInfo, PropTable, PageDescription, TypeLink, VersionBadge, ClassAPI} from '@react-spectrum/docs'; import {Keyboard} from '@react-spectrum/text'; import packageData from '@react-spectrum/table/package.json'; import ChevronRight from '@spectrum-icons/workflow/ChevronRight'; @@ -1957,3 +1958,42 @@ behaviors in your test suite. Please also refer to [React Spectrum's test suite](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/table/test/Table.test.js) if you find that the above isn't sufficient when resolving issues in your own test cases. + +`@react-aria/test-utils` also offers common table interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +in your tests. Below is the full definition of the table tester and a sample of how you could use it in your test suite. + + + +```ts +// TableView.test.ts +import {User} from '@react-aria/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse', advanceTimer: jest.advanceTimersByTime}); +// ... + +it('TableView can toggle row selection', async function () { + // Render your test component/app and initialize the table tester + let {getByTestId} = render( + + ... + + ); + let tableTester = testUtilUser.createTester('Table', {root: getByTestId('test-table')}); + expect(tableTester.selectedRows).toHaveLength(0); + + await tableTester.toggleSelectAll(); + expect(tableTester.selectedRows).toHaveLength(10); + + await tableTester.toggleRowSelection({index: 2}); + expect(tableTester.selectedRows).toHaveLength(9); + let checkbox = within(tableTester.rows[2]).getByRole('checkbox'); + expect(checkbox).not.toBeChecked(); + + await tableTester.toggleSelectAll(); + expect(tableTester.selectedRows).toHaveLength(10); + expect(checkbox).toBeChecked(); + + await tableTester.toggleSelectAll(); + expect(tableTester.selectedRows).toHaveLength(0); +}); +``` diff --git a/packages/dev/docs/pages/react-aria/testing.mdx b/packages/dev/docs/pages/react-aria/testing.mdx index db992e65f1a..9cea65f9cf3 100644 --- a/packages/dev/docs/pages/react-aria/testing.mdx +++ b/packages/dev/docs/pages/react-aria/testing.mdx @@ -64,7 +64,8 @@ initialize what mode of interaction (mouse, keyboard, or touch) will be used by in the test by a given amount. This is required for certain interactions (e.g. long press) that some of the patterns support. Once the `User` is initialized, you can use its `createTester` method to initialize a specific ARIA pattern tester in your test cases. This gives you access to that pattern's specific utilities that you can then call -within your test to query for specific subcomponents or simulate common interactions. See [below](#patterns) for more details on what is supported for each individual ARIA pattern. +within your test to query for specific subcomponents or simulate common interactions. `createTester` requires two arguments, the first being the name of the ARIA pattern tester you are creating and the second being a set of initialization options specific to that +pattern, typically including the `root` element (e.g. the menu trigger button, table, etc). See [below](#patterns) for more details on what is supported for each individual ARIA pattern tester. ```ts // YourTest.test.ts @@ -89,32 +90,16 @@ TODO need to update this so PatternNames actually comes through with the expecte ### Patterns -TODO perhaps these should live in each RAC pages specifically and then we link from here to those pages? -Below is a list of the ARIA patterns testers currently supported by `createTester`. +Below is a list of the ARIA patterns testers currently supported by `createTester`. See the accompanying component testing docs pages for a sample of how to use +the testers in your test suite. -#### Combobox +- [React Aria Components ComboBox](ComboBox.html#testing) and [React Spectrum ComboBox](../react-spectrum/ComboBox.html#testing) -TODO need to update the constructor options so the extended types can actually be expanded +- [React Aria Components GridList](GridList.html#testing) and [React Spectrum ListView](../react-spectrum/ListView.html#testing) - +- [React Aria Components Menu](Menu.html#testing) and [React Spectrum MenuTrigger](../react-spectrum/MenuTrigger.html#testing) -``` - -``` - -#### Gridlist - - - -#### Menu - - - -#### Select - - - -#### Table +- [React Aria Components Select](Select.html#testing) and [React Spectrum Picker](../react-spectrum/Picker.html#testing) - +- [React Aria Components Table](Table.html#testing) and [React Spectrum TableView](../react-spectrum/TableView.html#testing) diff --git a/packages/dev/docs/pages/react-spectrum/testing.mdx b/packages/dev/docs/pages/react-spectrum/testing.mdx index 2232db8bf25..04fc213564d 100644 --- a/packages/dev/docs/pages/react-spectrum/testing.mdx +++ b/packages/dev/docs/pages/react-spectrum/testing.mdx @@ -337,6 +337,12 @@ fireEvent.mouseMove(thumb, {pageX: 50}); fireEvent.mouseUp(thumb, {pageX: 50}); ``` +### Test Utilities + +In addition to some of the test utilities mentioned above, `@react-spectrum/test-utils` re-exports the same test utils available in `@react-aria/test-utils`, including +the ARIA pattern testers documented [here](../react-aria/testing.html#react-aria-test-utils). Those testers can be used with React Spectrum components as well and can be combined with the generalized +testing advice above. + ## Snapshot tests If you are using React 16 or 17, you may run into an issue where the ids generated by the React Spectrum components are changing on every snapshot. To remedy this, simply wrap your component in a [SSRProvider](../react-aria/SSRProvider.html). diff --git a/packages/react-aria-components/docs/ComboBox.mdx b/packages/react-aria-components/docs/ComboBox.mdx index 6aed3b95463..1a4ceb0ec90 100644 --- a/packages/react-aria-components/docs/ComboBox.mdx +++ b/packages/react-aria-components/docs/ComboBox.mdx @@ -12,7 +12,8 @@ export default Layout; import docs from 'docs:react-aria-components'; import statelyDocs from 'docs:@react-stately/combobox'; -import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable} from '@react-spectrum/docs'; +import comboboxUtils from 'docs:@react-aria/test-utils/src/combobox.ts'; +import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable, ClassAPI} from '@react-spectrum/docs'; import styles from '@react-spectrum/docs/src/docs.css'; import packageData from 'react-aria-components/package.json'; import Anatomy from './ComboBoxAnatomy.svg'; @@ -1472,3 +1473,36 @@ function ComboBoxClearButton() { ### Hooks If you need to customize things even further, such as accessing internal state, intercepting events, or customizing the DOM structure, you can drop down to the lower level Hook-based API. See [useComboBox](useComboBox.html) for more details. + +## Testing + +`@react-aria/test-utils` also offers common combobox interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities +in your tests. Below is the full definition of the combobox tester and a sample of how you could use it in your test suite. + + + +```ts +// Combobox.test.ts +import {User} from '@react-aria/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse'}); +// ... + +it('ComboBox can select an option via keyboard', async function () { + // Render your test component/app and initialize the combobox tester + let {getByTestId} = render( + + ... + + ); + let comboboxTester = testUtilUser.createTester('ComboBox', {root: getByTestId('test-combobox'), interactionType: 'keyboard'}); + + await comboboxTester.open(); + expect(comboboxTester.listbox).toBeTruthy(); + + let options = comboboxTester.options(); + await comboboxTester.selectOption({option: options[0]}); + expect(comboboxTester.combobox.value).toBe('One'); + expect(comboboxTester.listbox).toBeFalsy(); +}); +``` diff --git a/packages/react-aria-components/docs/GridList.mdx b/packages/react-aria-components/docs/GridList.mdx index 6022c6ea103..ca8fcc831d5 100644 --- a/packages/react-aria-components/docs/GridList.mdx +++ b/packages/react-aria-components/docs/GridList.mdx @@ -12,7 +12,8 @@ export default Layout; import docs from 'docs:react-aria-components'; import sharedDocs from 'docs:@react-types/shared'; -import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable} from '@react-spectrum/docs'; +import gridlistUtil from 'docs:@react-aria/test-utils/src/gridlist.ts'; +import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable, ClassAPI} from '@react-spectrum/docs'; import styles from '@react-spectrum/docs/src/docs.css'; import packageData from 'react-aria-components/package.json'; import Anatomy from './GridListAnatomy.svg'; @@ -578,7 +579,7 @@ Note that you are responsible for the styling of disabled rows, however, the sel When `disabledBehavior` is set to `selection`, interactions such as focus, dragging, or actions can still be performed on disabled rows. ```tsx example - + +```ts +// GridList.test.ts +import {User} from '@react-aria/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse'}); +// ... + +it('GridList can select a row via keyboard', async function () { + // Render your test component/app and initialize the gridlist tester + let {getByTestId} = render( + + ... + + ); + let gridlistTester = testUtilUser.createTester('GridList', {root: getByTestId('test-gridlist'), interactionType: 'keyboard'}); + + let row = gridListTester.rows[0]; + expect(row).not.toHaveClass('selected'); + expect(within(row).getByRole('checkbox')).not.toBeChecked(); + expect(gridListTester.selectedRows).toHaveLength(0); + + await gridListTester.toggleRowSelection({index: 0}); + expect(row).toHaveClass('selected'); + expect(within(row).getByRole('checkbox')).toBeChecked(); + expect(gridListTester.selectedRows).toHaveLength(1); + + await gridListTester.toggleRowSelection({index: 0}); + expect(row).not.toHaveClass('selected'); + expect(within(row).getByRole('checkbox')).not.toBeChecked(); + expect(gridListTester.selectedRows).toHaveLength(0); +}); +``` diff --git a/packages/react-aria-components/docs/Menu.mdx b/packages/react-aria-components/docs/Menu.mdx index 4cc80314895..117c90b8723 100644 --- a/packages/react-aria-components/docs/Menu.mdx +++ b/packages/react-aria-components/docs/Menu.mdx @@ -11,7 +11,8 @@ import {Layout} from '@react-spectrum/docs'; export default Layout; import docs from 'docs:react-aria-components'; -import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable} from '@react-spectrum/docs'; +import menuUtil from 'docs:@react-aria/test-utils/src/menu.ts'; +import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable, ClassAPI} from '@react-spectrum/docs'; import styles from '@react-spectrum/docs/src/docs.css'; import packageData from 'react-aria-components/package.json'; import Anatomy from './MenuAnatomy.svg'; @@ -681,7 +682,7 @@ function Example() { ]; return ( - ``` + +## Testing + +`@react-aria/test-utils` also offers common menu interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities +in your tests. Below is the full definition of the menu tester and a sample of how you could use it in your test suite. + + + +```ts +// Menu.test.ts +import {User} from '@react-aria/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse'}); +// ... + +it('Menu can open its submenu via keyboard', async function () { + // Render your test component/app and initialize the menu tester + let {getByTestId} = render( + + ... + + ); + let menuTester = testUtilUser.createTester('Menu', {root: getByTestId('test-menutrigger'), interactionType: 'keyboard'}); + + await menuTester.open(); + expect(menuTester.menu).toBeInTheDocument(); + let submenuTriggers = menuTester.submenuTriggers; + expect(submenuTriggers).toHaveLength(1); + + let submenuTester = await menuTester.openSubmenu({submenuTriggerText: 'Share…'}); + expect(submenuTester.menu).toBeInTheDocument(); + + await submenuTester.selectOption({option: submenuUtil.options()[0]}); + expect(submenuTester.menu).toBeInTheDocument(); + expect(menuTester.menu).toBeInTheDocument(); +}); +``` diff --git a/packages/react-aria-components/docs/Select.mdx b/packages/react-aria-components/docs/Select.mdx index 6b3a3c3c851..36d8f6438f0 100644 --- a/packages/react-aria-components/docs/Select.mdx +++ b/packages/react-aria-components/docs/Select.mdx @@ -12,7 +12,8 @@ export default Layout; import docs from 'docs:react-aria-components'; import statelyDocs from 'docs:@react-stately/select'; -import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable} from '@react-spectrum/docs'; +import selectUtil from 'docs:@react-aria/test-utils/src/select.ts'; +import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable, ClassAPI} from '@react-spectrum/docs'; import styles from '@react-spectrum/docs/src/docs.css'; import packageData from 'react-aria-components/package.json'; import Anatomy from './SelectAnatomy.svg'; @@ -1237,3 +1238,33 @@ knowing that we manage the focus in this way and thus throw this false positive. To facilitate the suppression of this false positive, the `data-a11y-ignore="aria-hidden-focus"` data attribute is automatically applied to the problematic element and references the relevant `AXE` rule. Please use this data attribute to target the problematic element and exclude it from your automated accessibility tests as shown [here](./accessibility.html#false-positives). + +## Testing + +`@react-aria/test-utils` also offers common select interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities +in your tests. Below is the full definition of the select tester and a sample of how you could use it in your test suite. + + + +```ts +// Select.test.ts +import {User} from '@react-aria/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse'}); +// ... + +it('Select can select an option via keyboard', async function () { + // Render your test component/app and initialize the select tester + let {getByTestId} = render( + + ); + let selectTester = testUtilUser.createTester('Select', {root: getByTestId('test-select'), interactionType: 'keyboard'}); + let trigger = selectTester.trigger; + expect(trigger).toHaveTextContent('Select an item'); + + await selectTester.selectOption({optionText: 'Cat'}); + expect(trigger).toHaveTextContent('Cat'); +}); +``` diff --git a/packages/react-aria-components/docs/Table.mdx b/packages/react-aria-components/docs/Table.mdx index b0f1556c485..dde95b257c5 100644 --- a/packages/react-aria-components/docs/Table.mdx +++ b/packages/react-aria-components/docs/Table.mdx @@ -12,7 +12,8 @@ export default Layout; import docs from 'docs:react-aria-components'; import sharedDocs from 'docs:@react-types/shared'; -import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable} from '@react-spectrum/docs'; +import tableUtil from 'docs:@react-aria/test-utils/src/table.ts'; +import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable, ClassAPI} from '@react-spectrum/docs'; import styles from '@react-spectrum/docs/src/docs.css'; import packageData from 'react-aria-components/package.json'; import Anatomy from './TableAnatomy.svg'; @@ -724,7 +725,7 @@ Note that you are responsible for the styling of disabled rows, however, the sel By default, only row selection is disabled. When `disabledBehavior` is set to `all`, all interactions such as focus, dragging, and actions are also disabled. ```tsx example - + +```ts +// Table.test.ts +import {User} from '@react-aria/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse', advanceTimer: jest.advanceTimersByTime}); +// ... + +it('Table can toggle row selection', async function () { + // Render your test component/app and initialize the table tester + let {getByTestId} = render( +
+ ... +
+ ); + let tableTester = testUtilUser.createTester('Table', {root: getByTestId('test-table')}); + expect(tableTester.selectedRows).toHaveLength(0); + + await tableTester.toggleSelectAll(); + expect(tableTester.selectedRows).toHaveLength(10); + + await tableTester.toggleRowSelection({index: 2}); + expect(tableTester.selectedRows).toHaveLength(9); + let checkbox = within(tableTester.rows[2]).getByRole('checkbox'); + expect(checkbox).not.toBeChecked(); + + await tableTester.toggleSelectAll(); + expect(tableTester.selectedRows).toHaveLength(10); + expect(checkbox).toBeChecked(); + + await tableTester.toggleSelectAll(); + expect(tableTester.selectedRows).toHaveLength(0); +}); +``` From 256e85940dd70c6f19d651e2de8b50e923cd4dc0 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 4 Oct 2024 14:43:33 -0700 Subject: [PATCH 06/19] remove some todos --- packages/@react-aria/test-utils/src/types.ts | 1 - packages/dev/docs/pages/react-aria/testing.mdx | 2 -- 2 files changed, 3 deletions(-) diff --git a/packages/@react-aria/test-utils/src/types.ts b/packages/@react-aria/test-utils/src/types.ts index 3f58dad1f51..619095c74f5 100644 --- a/packages/@react-aria/test-utils/src/types.ts +++ b/packages/@react-aria/test-utils/src/types.ts @@ -64,7 +64,6 @@ export interface MenuTesterOpts extends UserOpts, BaseTesterOpts { } export interface SelectTesterOpts extends UserOpts, BaseTesterOpts { - // TODO: I think the type grabbed from the testing library dist for UserEvent is breaking the build, will need to figure out a better place to grab from /** @private */ user: any, /** diff --git a/packages/dev/docs/pages/react-aria/testing.mdx b/packages/dev/docs/pages/react-aria/testing.mdx index 9cea65f9cf3..1d2e6f2cdce 100644 --- a/packages/dev/docs/pages/react-aria/testing.mdx +++ b/packages/dev/docs/pages/react-aria/testing.mdx @@ -85,10 +85,8 @@ it('my test case', async function () { See below for the full definition of the `User` object. -TODO need to update this so PatternNames actually comes through with the expected values - ### Patterns Below is a list of the ARIA patterns testers currently supported by `createTester`. See the accompanying component testing docs pages for a sample of how to use From 00a2fb0dbe71e175d64aee8385a16f76fc6835ae Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 4 Oct 2024 14:46:32 -0700 Subject: [PATCH 07/19] forgot to remove a only --- packages/@react-spectrum/picker/test/Picker.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-spectrum/picker/test/Picker.test.js b/packages/@react-spectrum/picker/test/Picker.test.js index 6fe6bfd13e0..ad348d0c1d7 100644 --- a/packages/@react-spectrum/picker/test/Picker.test.js +++ b/packages/@react-spectrum/picker/test/Picker.test.js @@ -78,7 +78,7 @@ describe('Picker', function () { }); describe('opening', function () { - it.only('can be opened on mouse down', async function () { + it('can be opened on mouse down', async function () { let onOpenChange = jest.fn(); let {getByRole, queryByRole} = render( From d460519627a5cf4e2a9fdc902bd6a22364474e03 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 4 Oct 2024 15:20:17 -0700 Subject: [PATCH 08/19] fix lint --- packages/@react-aria/test-utils/src/events.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-aria/test-utils/src/events.ts b/packages/@react-aria/test-utils/src/events.ts index ae1e16dfcf7..6683cbc60f4 100644 --- a/packages/@react-aria/test-utils/src/events.ts +++ b/packages/@react-aria/test-utils/src/events.ts @@ -11,7 +11,7 @@ */ import {act, fireEvent} from '@testing-library/react'; -import {UserOpts} from './user'; +import {UserOpts} from './types'; export const DEFAULT_LONG_PRESS_TIME = 500; From aa1f7f4151cfb77ee5e3cff2c6e2eac66bbf38fc Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 4 Oct 2024 15:49:14 -0700 Subject: [PATCH 09/19] fix lint again, for some reason local lint doesnt catch this one... --- packages/@react-aria/test-utils/src/user.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-aria/test-utils/src/user.ts b/packages/@react-aria/test-utils/src/user.ts index b86702149f8..6f515da7ece 100644 --- a/packages/@react-aria/test-utils/src/user.ts +++ b/packages/@react-aria/test-utils/src/user.ts @@ -65,6 +65,6 @@ export class User { * Creates an aria pattern tester, inheriting the options provided to the original user. */ createTester(patternName: T, opts: TesterOpts): Tester { - return new (keyToUtil)[patternName]({user: this.user, interactionType: this.interactionType, advanceTimer: this.advanceTimer, ...opts}) as Tester; + return new (keyToUtil)[patternName]({interactionType: this.interactionType, advanceTimer: this.advanceTimer, ...opts, user: this.user}) as Tester; } } From 452cb3d7f49d65685505061201c47b9fcf84c5be Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Mon, 9 Dec 2024 11:40:30 -0800 Subject: [PATCH 10/19] review comments --- .../@react-aria/test-utils/src/combobox.ts | 2 +- .../combobox/docs/ComboBox.mdx | 4 +- .../combobox/test/ComboBox.test.js | 4 +- .../menu/test/MenuTrigger.test.js | 44 +++++++++---------- .../dev/docs/pages/react-aria/testing.mdx | 6 +-- .../react-aria-components/docs/ComboBox.mdx | 4 +- 6 files changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/@react-aria/test-utils/src/combobox.ts b/packages/@react-aria/test-utils/src/combobox.ts index f3d4e2cd6d6..ed5fcccd6dd 100644 --- a/packages/@react-aria/test-utils/src/combobox.ts +++ b/packages/@react-aria/test-utils/src/combobox.ts @@ -187,7 +187,7 @@ export class ComboBoxTester { } /** - * Returns the combobox trigger button if present. + * Returns the combobox trigger button. */ get trigger(): HTMLElement { return this._trigger; diff --git a/packages/@react-spectrum/combobox/docs/ComboBox.mdx b/packages/@react-spectrum/combobox/docs/ComboBox.mdx index b8d96bbaf46..b94765dae25 100644 --- a/packages/@react-spectrum/combobox/docs/ComboBox.mdx +++ b/packages/@react-spectrum/combobox/docs/ComboBox.mdx @@ -1016,11 +1016,11 @@ it('ComboBox can select an option via keyboard', async function () { let comboboxTester = testUtilUser.createTester('ComboBox', {root: getByTestId('test-combobox'), interactionType: 'keyboard'}); await comboboxTester.open(); - expect(comboboxTester.listbox).toBeTruthy(); + expect(comboboxTester.listbox).toBeInTheDocument(); let options = comboboxTester.options(); await comboboxTester.selectOption({option: options[0]}); expect(comboboxTester.combobox.value).toBe('One'); - expect(comboboxTester.listbox).toBeFalsy(); + expect(comboboxTester.listbox).not.toBeInTheDocument(); }); ``` diff --git a/packages/@react-spectrum/combobox/test/ComboBox.test.js b/packages/@react-spectrum/combobox/test/ComboBox.test.js index efee099d566..d7f0a0e4c02 100644 --- a/packages/@react-spectrum/combobox/test/ComboBox.test.js +++ b/packages/@react-spectrum/combobox/test/ComboBox.test.js @@ -457,7 +457,7 @@ describe('ComboBox', function () { expect(comboboxTester.listbox).toBeFalsy(); await comboboxTester.open(); - expect(comboboxTester.listbox).toBeTruthy(); + expect(comboboxTester.listbox).toBeInTheDocument(); expect(document.activeElement).toBe(comboboxTester.combobox); await user.click(comboboxTester.trigger); @@ -494,7 +494,7 @@ describe('ComboBox', function () { comboboxTester.setInteractionType('touch'); await comboboxTester.open(); expect(document.activeElement).toBe(comboboxTester.combobox); - expect(comboboxTester.listbox).toBeTruthy(); + expect(comboboxTester.listbox).toBeInTheDocument(); let button = comboboxTester.trigger; fireEvent.touchStart(button, {targetTouches: [{identifier: 1}]}); diff --git a/packages/@react-spectrum/menu/test/MenuTrigger.test.js b/packages/@react-spectrum/menu/test/MenuTrigger.test.js index 24084233bb7..c89cecb1bfd 100644 --- a/packages/@react-spectrum/menu/test/MenuTrigger.test.js +++ b/packages/@react-spectrum/menu/test/MenuTrigger.test.js @@ -112,15 +112,15 @@ describe('MenuTrigger', function () { act(() => {jest.runAllTimers();}); let menu = menuTester.menu; - expect(menu).toBeTruthy(); + expect(menu).toBeInTheDocument(); expect(menu).toHaveAttribute('aria-labelledby', triggerButton.id); let menuItem1 = within(menu).getByText('Foo'); let menuItem2 = within(menu).getByText('Bar'); let menuItem3 = within(menu).getByText('Baz'); - expect(menuItem1).toBeTruthy(); - expect(menuItem2).toBeTruthy(); - expect(menuItem3).toBeTruthy(); + expect(menuItem1).toBeInTheDocument(); + expect(menuItem2).toBeInTheDocument(); + expect(menuItem3).toBeInTheDocument(); expect(triggerButton).toHaveAttribute('aria-expanded', 'true'); expect(triggerButton).toHaveAttribute('aria-controls', menu.id); @@ -153,13 +153,13 @@ describe('MenuTrigger', function () { let menuTester = testUtilUser.createTester('Menu', {root: tree.container}); let triggerButton = menuTester.trigger; - expect(triggerButton).toBeTruthy(); + expect(triggerButton).toBeInTheDocument(); expect(triggerButton).toHaveAttribute('aria-haspopup', 'true'); let buttonText = within(triggerButton).getByText(triggerText); - expect(buttonText).toBeTruthy(); + expect(buttonText).toBeInTheDocument(); - expect(menuTester.menu).toBeFalsy(); + expect(menuTester.menu).not.toBeInTheDocument(); expect(triggerButton).toHaveAttribute('aria-expanded', 'false'); expect(triggerButton).toHaveAttribute('type', 'button'); @@ -226,13 +226,13 @@ describe('MenuTrigger', function () { expect(onOpenChange).toBeCalledTimes(0); let menu = tree.getByRole('menu'); - expect(menu).toBeTruthy(); + expect(menu).toBeInTheDocument(); let triggerButton = tree.getByText('Menu Button'); await user.click(triggerButton); act(() => {jest.runAllTimers();}); - expect(menu).toBeTruthy(); + expect(menu).toBeInTheDocument(); expect(onOpenChange).toBeCalledTimes(1); }); @@ -246,7 +246,7 @@ describe('MenuTrigger', function () { expect(onOpenChange).toBeCalledTimes(0); let menu = tree.getByRole('menu'); - expect(menu).toBeTruthy(); + expect(menu).toBeInTheDocument(); let triggerButton = tree.getByText('Menu Button'); await user.click(triggerButton); @@ -265,7 +265,7 @@ describe('MenuTrigger', function () { await menuTester.open(); act(() => {jest.runAllTimers();}); let menu = menuTester.menu; - expect(menu).toBeFalsy(); + expect(menu).not.toBeInTheDocument(); expect(onOpenChange).toBeCalledTimes(0); }); @@ -280,7 +280,7 @@ describe('MenuTrigger', function () { let button = menuTester.trigger; await menuTester.open(); let menu = menuTester.menu; - expect(menu).toBeTruthy(); + expect(menu).toBeInTheDocument(); let menuItems = menuTester.options(); let selectedItem = menuItems[1]; expect(selectedItem).toBe(document.activeElement); @@ -428,7 +428,7 @@ describe('MenuTrigger', function () { menu = menuTester.menu; expect(menuTester.options()[0]).toHaveAttribute('aria-checked', 'true'); - expect(menu).toBeTruthy(); + expect(menu).toBeInTheDocument(); fireEvent.keyDown(menu, {key: 'Escape', code: 27, charCode: 27}); expect(onSelectionChange).not.toHaveBeenCalled(); }); @@ -443,7 +443,7 @@ describe('MenuTrigger', function () { act(() => {jest.runAllTimers();}); let menu = tree.getByRole('menu'); - expect(menu).toBeTruthy(); + expect(menu).toBeInTheDocument(); fireEvent.mouseDown(document.body); fireEvent.mouseUp(document.body); act(() => {jest.runAllTimers();}); // FocusScope useLayoutEffect cleanup @@ -517,7 +517,7 @@ describe('MenuTrigger', function () { await openAndTriggerMenuItem(tree, props.role, menuProps.selectionMode, async (item) => await user.click(item)); let menu = tree.getByRole('menu'); - expect(menu).toBeTruthy(); + expect(menu).toBeInTheDocument(); }); it.each` @@ -559,7 +559,7 @@ describe('MenuTrigger', function () { await openAndTriggerMenuItem(tree, props.role, menuProps.selectionMode, (item) => fireEvent.keyDown(item, {key: ' ', code: 32, charCode: 32})); let menu = tree.queryByRole('menu'); - expect(menu).toBeTruthy(); + expect(menu).toBeInTheDocument(); expect(onClose).toHaveBeenCalledTimes(0); }); @@ -587,7 +587,7 @@ describe('MenuTrigger', function () { act(() => {jest.runAllTimers();}); let menu = tree.getByRole('menu'); - expect(menu).toBeTruthy(); + expect(menu).toBeInTheDocument(); expect(onOpenChange).toBeCalledTimes(1); expect(button).toHaveAttribute('aria-expanded', 'true'); @@ -609,7 +609,7 @@ describe('MenuTrigger', function () { await openAndTriggerMenuItem(tree, props.role, menuProps.selectionMode, (item) => fireEvent.keyDown(item, {key: 'Enter', code: 13, charCode: 13, repeat: true})); let menu = tree.queryByRole('menu'); - expect(menu).toBeTruthy(); + expect(menu).toBeInTheDocument(); }); it('tabs to the next element after the trigger and closes the menu', async function () { @@ -637,7 +637,7 @@ describe('MenuTrigger', function () { act(() => {jest.runAllTimers();}); let menu = tree.getByRole('menu'); - expect(menu).toBeTruthy(); + expect(menu).toBeInTheDocument(); expect(onOpenChange).toBeCalledTimes(1); expect(button).toHaveAttribute('aria-expanded', 'true'); @@ -674,7 +674,7 @@ describe('MenuTrigger', function () { act(() => jest.runAllTimers()); let menu = getByRole('menu'); - expect(menu).toBeTruthy(); + expect(menu).toBeInTheDocument(); expect(onOpenChange).toBeCalledTimes(1); expect(button).toHaveAttribute('aria-expanded', 'true'); @@ -804,7 +804,7 @@ describe('MenuTrigger', function () { const getMenuOrThrow = (tree, button) => { try { let menu = tree.getByRole('menu'); - expect(menu).toBeTruthy(); + expect(menu).toBeInTheDocument(); expect(menu).toHaveAttribute('aria-labelledby', button.id); } catch (e) { throw ERROR_MENU_NOT_FOUND; @@ -894,7 +894,7 @@ describe('MenuTrigger', function () { menuItemRole = 'menuitemradio'; } let menu = tree.getByRole('menu'); - expect(menu).toBeTruthy(); + expect(menu).toBeInTheDocument(); let menuItems = within(menu).getAllByRole(menuItemRole); let selectedItem = menuItems[idx < 0 ? menuItems.length + idx : idx]; expect(selectedItem).toBe(document.activeElement); diff --git a/packages/dev/docs/pages/react-aria/testing.mdx b/packages/dev/docs/pages/react-aria/testing.mdx index 1d2e6f2cdce..af22bf4390c 100644 --- a/packages/dev/docs/pages/react-aria/testing.mdx +++ b/packages/dev/docs/pages/react-aria/testing.mdx @@ -38,12 +38,12 @@ To address this, we've created [@react-aria/test-utils](https://www.npmjs.com/pa or for users who have built their own components following the respective ARIA pattern specification. By using the ARIA specification for any given component pattern as a source of truth, we can make assumptions about the existence of specific aria attributes that allow us to navigate the component's DOM structure. Similarly, we can also expect that the component permits specific interaction patterns described by the ARIA pattern specification and thus accurately simulate those interactions, using the aforementioned aria attributes to target the proper node -within the component or to verify that the component's state has changed appropriately post-interaction. By providing utilities to simulate these standard interaction and getters that -allow the user to easily look up the subcomponents of the component itself, we hope to simplify the overall test writing experience, leading towards easier adoption. +within the component or to verify that the component's state has changed appropriately post-interaction. By providing utilities to simulate these standard interactions and getters that +allow the user to easily look up the subcomponents of the component itself, we hope to simplify the overall test writing experience, leading to easier adoption. These test utilities were inspired by various issues and observations that the maintainers of this library and consumers have experienced when writing tests against our components over the years. It is still very much a work in progress so if you discover any issues or have any feedback please feel free to report them via [GitHub issues](https://github.com/adobe/react-spectrum/issues)! If you have implemented -any testing utilities yourself that you feel would be a good fit, we would be happy to field any pull request! Please read our [contributing guide](contribute.html) +any testing utilities yourself that you feel would be a good fit, we would be happy to review any pull requests! Please read our [contributing guide](contribute.html) for more information. ### Installation diff --git a/packages/react-aria-components/docs/ComboBox.mdx b/packages/react-aria-components/docs/ComboBox.mdx index 1a4ceb0ec90..17427db2188 100644 --- a/packages/react-aria-components/docs/ComboBox.mdx +++ b/packages/react-aria-components/docs/ComboBox.mdx @@ -1498,11 +1498,11 @@ it('ComboBox can select an option via keyboard', async function () { let comboboxTester = testUtilUser.createTester('ComboBox', {root: getByTestId('test-combobox'), interactionType: 'keyboard'}); await comboboxTester.open(); - expect(comboboxTester.listbox).toBeTruthy(); + expect(comboboxTester.listbox).toBeInTheDocument(); let options = comboboxTester.options(); await comboboxTester.selectOption({option: options[0]}); expect(comboboxTester.combobox.value).toBe('One'); - expect(comboboxTester.listbox).toBeFalsy(); + expect(comboboxTester.listbox).not.toBeInTheDocument(); }); ``` From 3c3a70577dcac3ebc5103715ce5f1efa55bb862c Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Mon, 9 Dec 2024 14:17:44 -0800 Subject: [PATCH 11/19] update select option methods to accept node, string, and index using a single unified option for simplicity --- .../@react-aria/test-utils/src/combobox.ts | 42 ++++++++--- .../@react-aria/test-utils/src/gridlist.ts | 56 ++++++--------- packages/@react-aria/test-utils/src/menu.ts | 41 ++++++++--- packages/@react-aria/test-utils/src/select.ts | 41 ++++++++--- packages/@react-aria/test-utils/src/table.ts | 72 +++++++++---------- .../combobox/test/ComboBox.test.js | 2 +- .../@react-spectrum/list/docs/ListView.mdx | 4 +- .../list/test/ListView.test.js | 18 ++--- .../menu/test/MenuTrigger.test.js | 4 +- .../@react-spectrum/picker/docs/Picker.mdx | 2 +- .../picker/test/Picker.test.js | 29 ++++---- .../picker/test/TempUtilTest.test.js | 8 +-- .../@react-spectrum/table/docs/TableView.mdx | 2 +- .../@react-spectrum/table/test/Table.test.js | 22 +++--- .../table/test/TestTableUtils.test.js | 40 +++++------ .../react-aria-components/docs/GridList.mdx | 4 +- .../react-aria-components/docs/Select.mdx | 2 +- packages/react-aria-components/docs/Table.mdx | 2 +- .../test/GridList.test.js | 4 +- .../react-aria-components/test/Select.test.js | 2 +- .../react-aria-components/test/Table.test.js | 4 +- 21 files changed, 220 insertions(+), 181 deletions(-) diff --git a/packages/@react-aria/test-utils/src/combobox.ts b/packages/@react-aria/test-utils/src/combobox.ts index ed5fcccd6dd..fc001f45ecd 100644 --- a/packages/@react-aria/test-utils/src/combobox.ts +++ b/packages/@react-aria/test-utils/src/combobox.ts @@ -27,13 +27,9 @@ interface ComboBoxOpenOpts { interface ComboBoxSelectOpts extends ComboBoxOpenOpts { /** - * The option node to select. Option nodes can be sourced via `options()`. + * The index, text, or node of the option to select. Option nodes can be sourced via `options()`. */ - option?: HTMLElement, - /** - * The text of the node to look for when selecting a option. Alternative to `option`. - */ - optionText?: string + option: number | string | HTMLElement } export class ComboBoxTester { @@ -122,20 +118,44 @@ export class ComboBoxTester { }); } + /** + * Returns a option matching the specified index or text content. + */ + findOption(opts: {optionIndexOrText: number | string}): HTMLElement { + let { + optionIndexOrText + } = opts; + + let option; + let options = this.options; + let listbox = this.listbox; + + if (typeof optionIndexOrText === 'number') { + option = options[optionIndexOrText]; + } else if (typeof optionIndexOrText === 'string' && listbox != null) { + option = within(listbox).getByText(optionIndexOrText); + while (option && option.getAttribute('role') !== 'option') { + option = option.parentElement; + } + } + + return option; + } + /** * Selects the desired combobox option. Defaults to using the interaction type set on the combobox tester. If necessary, will open the combobox dropdown beforehand. - * The desired option can be targeted via the option's node or the option's text. + * The desired option can be targeted via the option's node, the option's text, or the option's index. */ - async selectOption(opts: ComboBoxSelectOpts = {}) { - let {optionText, option, triggerBehavior, interactionType = this._interactionType} = opts; + async selectOption(opts: ComboBoxSelectOpts) { + let {option, triggerBehavior, interactionType = this._interactionType} = opts; if (!this.combobox.getAttribute('aria-controls')) { await this.open({triggerBehavior}); } let listbox = this.listbox; if (listbox) { - if (!option && optionText) { - option = within(listbox).getByText(optionText); + if (typeof option === 'string' || typeof option === 'number') { + option = this.findOption({optionIndexOrText: option}); } // TODO: keyboard method of selecting the the option is a bit tricky unless I simply simulate the user pressing the down arrow diff --git a/packages/@react-aria/test-utils/src/gridlist.ts b/packages/@react-aria/test-utils/src/gridlist.ts index 713da7b2b92..1ddc844be9b 100644 --- a/packages/@react-aria/test-utils/src/gridlist.ts +++ b/packages/@react-aria/test-utils/src/gridlist.ts @@ -17,32 +17,16 @@ import {pressElement} from './events'; // TODO: this is a bit inconsistent from combobox, perhaps should also take node or combobox should also have find row interface GridListToggleRowOpts { /** - * The index of the row to toggle selection for. - */ - index?: number, - /** - * The text of the row to toggle selection for. Alternative to `index`. + * What interaction type to use when toggling the row selection. Defaults to the interaction type set on the tester. */ - text?: string, + interactionType?: UserOpts['interactionType'], /** - * What interaction type to use when toggling the row selection. Defaults to the interaction type set on the tester. + * The index, text, or node of the row to toggle selection for. */ - interactionType?: UserOpts['interactionType'] + row: number | string | HTMLElement } -interface GridListRowActionOpts { - /** - * The index of the row to trigger its action for. - */ - index?: number, - /** - * The text of the row to trigger its action for. Alternative to `index`. - */ - text?: string, - /** - * What interaction type to use when triggering the row's action. Defaults to the interaction type set on the tester. - */ - interactionType?: UserOpts['interactionType'], +interface GridListRowActionOpts extends GridListToggleRowOpts { /** * Whether or not the grid list needs a double click to trigger the row action. Depends on the grid list's implementation. */ @@ -77,10 +61,13 @@ export class GridListTester { /** * Toggles the selection for the specified gridlist row. Defaults to using the interaction type set on the gridlist tester. */ - async toggleRowSelection(opts: GridListToggleRowOpts = {}) { - let {index, text, interactionType = this._interactionType} = opts; + async toggleRowSelection(opts: GridListToggleRowOpts) { + let {row, interactionType = this._interactionType} = opts; + + if (typeof row === 'string' || typeof row === 'number') { + row = this.findRow({rowIndexOrText: row}); + } - let row = this.findRow({index, text}); let rowCheckbox = within(row).queryByRole('checkbox'); if (rowCheckbox) { await pressElement(this.user, rowCheckbox, interactionType); @@ -95,17 +82,16 @@ export class GridListTester { /** * Returns a row matching the specified index or text content. */ - findRow(opts: {index?: number, text?: string}) { + findRow(opts: {rowIndexOrText: number | string}): HTMLElement { let { - index, - text + rowIndexOrText } = opts; let row; - if (index != null) { - row = this.rows[index]; - } else if (text != null) { - row = within(this?.gridlist).getByText(text); + if (typeof rowIndexOrText === 'number') { + row = this.rows[rowIndexOrText]; + } else if (typeof rowIndexOrText === 'string') { + row = within(this?.gridlist).getByText(rowIndexOrText); while (row && row.getAttribute('role') !== 'row') { row = row.parentElement; } @@ -121,13 +107,15 @@ export class GridListTester { */ async triggerRowAction(opts: GridListRowActionOpts) { let { - index, - text, + row, needsDoubleClick, interactionType = this._interactionType } = opts; - let row = this.findRow({index, text}); + if (typeof row === 'string' || typeof row === 'number') { + row = this.findRow({rowIndexOrText: row}); + } + if (row) { if (needsDoubleClick) { await this.user.dblClick(row); diff --git a/packages/@react-aria/test-utils/src/menu.ts b/packages/@react-aria/test-utils/src/menu.ts index 5f4de1b8be5..44d63638caa 100644 --- a/packages/@react-aria/test-utils/src/menu.ts +++ b/packages/@react-aria/test-utils/src/menu.ts @@ -27,13 +27,9 @@ interface MenuOpenOpts { interface MenuSelectOpts extends MenuOpenOpts { /** - * The option node to select. Option nodes can be sourced via `options()`. + * The index, text, or node of the option to select. Option nodes can be sourced via `options()`. */ - option?: HTMLElement, - /** - * The text of the node to look for when selecting a option. Alternative to `option`. - */ - optionText?: string, + option: number | string | HTMLElement, /** * The menu's selection mode. Will affect whether or not the menu is expected to be closed upon option selection. * @default 'single' @@ -139,15 +135,38 @@ export class MenuTester { } } + /** + * Returns a option matching the specified index or text content. + */ + findOption(opts: {optionIndexOrText: number | string}): HTMLElement { + let { + optionIndexOrText + } = opts; + + let option; + let options = this.options; + let menu = this.menu; + + if (typeof optionIndexOrText === 'number') { + option = options[optionIndexOrText]; + } else if (typeof optionIndexOrText === 'string' && menu != null) { + option = within(menu).getByText(optionIndexOrText); + while (option && !option.getAttribute('role')?.includes('menuitem')) { + option = option.parentElement; + } + } + + return option; + } + // TODO: also very similar to select, barring potential long press support // Close on select is also kinda specific? /** * Selects the desired menu option. Defaults to using the interaction type set on the menu tester. If necessary, will open the menu dropdown beforehand. - * The desired option can be targeted via the option's node or the option's text. + * The desired option can be targeted via the option's node, the option's text, or the option's index. */ async selectOption(opts: MenuSelectOpts) { let { - optionText, menuSelectionMode = 'single', needsLongPress, closesOnSelect = true, @@ -161,8 +180,8 @@ export class MenuTester { let menu = this.menu; if (menu) { - if (!option && optionText) { - option = within(menu).getByText(optionText); + if (typeof option === 'string' || typeof option === 'number') { + option = this.findOption({optionIndexOrText: option}); } if (interactionType === 'keyboard') { @@ -170,7 +189,7 @@ export class MenuTester { act(() => menu.focus()); } - await this.user.keyboard(optionText); + // await this.user.keyboard(option); await this.user.keyboard('[Enter]'); } else { if (interactionType === 'mouse') { diff --git a/packages/@react-aria/test-utils/src/select.ts b/packages/@react-aria/test-utils/src/select.ts index c8c5a4d29af..69e6b1a7afe 100644 --- a/packages/@react-aria/test-utils/src/select.ts +++ b/packages/@react-aria/test-utils/src/select.ts @@ -22,13 +22,9 @@ interface SelectOpenOpts { interface SelectTriggerOptionOpts extends SelectOpenOpts { /** - * The option node to select. Option nodes can be sourced via `options()`. + * The index, text, or node of the option to select. Option nodes can be sourced via `options()`. */ - option?: HTMLElement, - /** - * The text of the node to look for when selecting a option. Alternative to `option`. - */ - optionText?: string + option: number | string | HTMLElement } export class SelectTester { @@ -113,14 +109,37 @@ export class SelectTester { } } + /** + * Returns a option matching the specified index or text content. + */ + findOption(opts: {optionIndexOrText: number | string}): HTMLElement { + let { + optionIndexOrText + } = opts; + + let option; + let options = this.options; + let listbox = this.listbox; + + if (typeof optionIndexOrText === 'number') { + option = options[optionIndexOrText]; + } else if (typeof optionIndexOrText === 'string' && listbox != null) { + option = within(listbox).getByText(optionIndexOrText); + while (option && option.getAttribute('role') !== 'option') { + option = option.parentElement; + } + } + + return option; + } + // TODO: update this so it also can take the option node instead of just text, might already have been added in Rob's PR /** * Selects the desired select option. Defaults to using the interaction type set on the select tester. If necessary, will open the select dropdown beforehand. - * The desired option can be targeted via the option's text. + * The desired option can be targeted via the option's node, the option's text, or the option's index. */ async selectOption(opts: SelectTriggerOptionOpts) { let { - optionText, option, interactionType = this._interactionType } = opts || {}; @@ -130,8 +149,8 @@ export class SelectTester { } let listbox = this.listbox; if (listbox) { - if (!option && optionText) { - option = within(listbox).getByText(optionText); + if (typeof option === 'string' || typeof option === 'number') { + option = this.findOption({optionIndexOrText: option}); } if (interactionType === 'keyboard') { @@ -141,7 +160,7 @@ export class SelectTester { // TODO: this simulates typeahead, do we want to add a helper util for that? Not sure if users would really need that for // their test - await this.user.keyboard(optionText); + // await this.user.keyboard(option); await this.user.keyboard('[Enter]'); } else { // TODO: what if the user needs to scroll the list to find the option? What if there are multiple matches for text (hopefully the picker options are pretty unique) diff --git a/packages/@react-aria/test-utils/src/table.ts b/packages/@react-aria/test-utils/src/table.ts index 9d52af082a4..df831f95668 100644 --- a/packages/@react-aria/test-utils/src/table.ts +++ b/packages/@react-aria/test-utils/src/table.ts @@ -17,13 +17,9 @@ import {TableTesterOpts, UserOpts} from './types'; // TODO: this is a bit inconsistent from combobox, perhaps should also take node or combobox should also have find row interface TableToggleRowOpts { /** - * The index of the row to toggle selection for. + * The index, text, or node of the row to toggle selection for. */ - index?: number, - /** - * The text of the row to toggle selection for. Alternative to `index`. - */ - text?: string, + row: number | string | HTMLElement, /** * Whether the row needs to be long pressed to be selected. Depends on the table's implementation. */ @@ -36,13 +32,9 @@ interface TableToggleRowOpts { interface TableToggleSortOpts { /** - * The index of the column to sort. - */ - index?: number, - /** - * The text of the column to sort. Alternative to `index`. + * The index, text, or node of the column to toggle selection for. */ - text?: string, + column: number | string | HTMLElement, /** * What interaction type to use when sorting the column. Defaults to the interaction type set on the tester. */ @@ -51,13 +43,9 @@ interface TableToggleSortOpts { interface TableRowActionOpts { /** - * The index of the row to trigger its action for. - */ - index?: number, - /** - * The text of the row to trigger its action for. Alternative to `index`. + * The index, text, or node of the row to toggle selection for. */ - text?: string, + row: number | string | HTMLElement, /** * What interaction type to use when triggering the row's action. Defaults to the interaction type set on the tester. */ @@ -92,15 +80,17 @@ export class TableTester { /** * Toggles the selection for the specified table row. Defaults to using the interaction type set on the table tester. */ - async toggleRowSelection(opts: TableToggleRowOpts = {}) { + async toggleRowSelection(opts: TableToggleRowOpts) { let { - index, - text, + row, needsLongPress, interactionType = this._interactionType } = opts; - let row = this.findRow({index, text}); + if (typeof row === 'string' || typeof row === 'number') { + row = this.findRow({rowIndexOrText: row}); + } + let rowCheckbox = within(row).queryByRole('checkbox'); if (rowCheckbox) { await pressElement(this.user, rowCheckbox, interactionType); @@ -135,21 +125,22 @@ export class TableTester { /** * Toggles the sort order for the specified table column. Defaults to using the interaction type set on the table tester. */ - async toggleSort(opts: TableToggleSortOpts = {}) { + async toggleSort(opts: TableToggleSortOpts) { let { - index, - text, + column, interactionType = this._interactionType } = opts; let columnheader; - if (index != null) { - columnheader = this.columns[index]; - } else if (text != null) { - columnheader = within(this.rowGroups[0]).getByText(text); + if (typeof column === 'number') { + columnheader = this.columns[column]; + } else if (typeof column === 'string') { + columnheader = within(this.rowGroups[0]).getByText(column); while (columnheader && !/columnheader/.test(columnheader.getAttribute('role'))) { columnheader = columnheader.parentElement; } + } else { + columnheader = column; } let menuButton = within(columnheader).queryByRole('button'); @@ -226,15 +217,17 @@ export class TableTester { /** * Triggers the action for the specified table row. Defaults to using the interaction type set on the table tester. */ - async triggerRowAction(opts: TableRowActionOpts = {}) { + async triggerRowAction(opts: TableRowActionOpts) { let { - index, - text, + row, needsDoubleClick, interactionType = this._interactionType } = opts; - let row = this.findRow({index, text}); + if (typeof row === 'string' || typeof row === 'number') { + row = this.findRow({rowIndexOrText: row}); + } + if (row) { if (needsDoubleClick) { await this.user.dblClick(row); @@ -271,19 +264,18 @@ export class TableTester { /** * Returns a row matching the specified index or text content. */ - findRow(opts: {index?: number, text?: string} = {}) { + findRow(opts: {rowIndexOrText: number | string}): HTMLElement { let { - index, - text + rowIndexOrText } = opts; let row; let rows = this.rows; let bodyRowGroup = this.rowGroups[1]; - if (index != null) { - row = rows[index]; - } else if (text != null) { - row = within(bodyRowGroup).getByText(text); + if (typeof rowIndexOrText === 'number') { + row = rows[rowIndexOrText]; + } else if (typeof rowIndexOrText === 'string') { + row = within(bodyRowGroup).getByText(rowIndexOrText); while (row && row.getAttribute('role') !== 'row') { row = row.parentElement; } diff --git a/packages/@react-spectrum/combobox/test/ComboBox.test.js b/packages/@react-spectrum/combobox/test/ComboBox.test.js index d7f0a0e4c02..2c0c936c96d 100644 --- a/packages/@react-spectrum/combobox/test/ComboBox.test.js +++ b/packages/@react-spectrum/combobox/test/ComboBox.test.js @@ -870,7 +870,7 @@ describe('ComboBox', function () { expect(combobox.value).toBe('Tw'); expect(comboboxTester.options().length).toBe(1); - await comboboxTester.selectOption({optionText: 'Two'}); + await comboboxTester.selectOption({option: 'Two'}); expect(comboboxTester.listbox).toBeFalsy(); expect(combobox.value).toBe('Two'); // selectionManager.select from useSingleSelectListState always calls onSelectionChange even if the key is the same diff --git a/packages/@react-spectrum/list/docs/ListView.mdx b/packages/@react-spectrum/list/docs/ListView.mdx index 2e6723f787c..a05abb793a3 100644 --- a/packages/@react-spectrum/list/docs/ListView.mdx +++ b/packages/@react-spectrum/list/docs/ListView.mdx @@ -1219,12 +1219,12 @@ it('ListView can select a row via keyboard', async function () { expect(within(row).getByRole('checkbox')).not.toBeChecked(); expect(gridListTester.selectedRows).toHaveLength(0); - await gridListTester.toggleRowSelection({index: 0}); + await gridListTester.toggleRowSelection({row: 0}); expect(row).toHaveClass('selected'); expect(within(row).getByRole('checkbox')).toBeChecked(); expect(gridListTester.selectedRows).toHaveLength(1); - await gridListTester.toggleRowSelection({index: 0}); + await gridListTester.toggleRowSelection({row: 0}); expect(row).not.toHaveClass('selected'); expect(within(row).getByRole('checkbox')).not.toBeChecked(); expect(gridListTester.selectedRows).toHaveLength(0); diff --git a/packages/@react-spectrum/list/test/ListView.test.js b/packages/@react-spectrum/list/test/ListView.test.js index 57fb545e7d3..1d37f29bde8 100644 --- a/packages/@react-spectrum/list/test/ListView.test.js +++ b/packages/@react-spectrum/list/test/ListView.test.js @@ -843,7 +843,7 @@ describe('ListView', function () { let gridListTester = testUtilUser.createTester('GridList', {root: grid}); let rows = gridListTester.rows; - await gridListTester.toggleRowSelection({index: 0}); + await gridListTester.toggleRowSelection({row: 0}); checkSelection(onSelectionChange, ['foo']); onSelectionChange.mockClear(); expect(announce).toHaveBeenLastCalledWith('Foo selected.'); @@ -874,17 +874,17 @@ describe('ListView', function () { let tree = renderSelectionList({onSelectionChange, selectionMode: 'multiple', onAction}); let gridListTester = testUtilUser.createTester('GridList', {root: tree.getByRole('grid')}); - await gridListTester.triggerRowAction({index: 1}); + await gridListTester.triggerRowAction({row: 1}); expect(onSelectionChange).not.toHaveBeenCalled(); expect(onAction).toHaveBeenCalledTimes(1); expect(onAction).toHaveBeenLastCalledWith('bar'); - await gridListTester.toggleRowSelection({index: 1}); + await gridListTester.toggleRowSelection({row: 1}); expect(onSelectionChange).toHaveBeenCalledTimes(1); checkSelection(onSelectionChange, ['bar']); onSelectionChange.mockReset(); - await gridListTester.toggleRowSelection({index: 2}); + await gridListTester.toggleRowSelection({row: 2}); expect(onSelectionChange).toHaveBeenCalledTimes(1); checkSelection(onSelectionChange, ['bar', 'baz']); }); @@ -895,18 +895,18 @@ describe('ListView', function () { let tree = renderSelectionList({onSelectionChange, selectionMode: 'multiple', onAction}); let gridListTester = testUtilUser.createTester('GridList', {root: tree.getByRole('grid')}); - await gridListTester.triggerRowAction({index: 1}); + await gridListTester.triggerRowAction({row: 1}); expect(onSelectionChange).not.toHaveBeenCalled(); expect(onAction).toHaveBeenCalledTimes(1); expect(onAction).toHaveBeenLastCalledWith('bar'); - await gridListTester.toggleRowSelection({index: 1}); + await gridListTester.toggleRowSelection({row: 1}); expect(onSelectionChange).toHaveBeenCalledTimes(1); checkSelection(onSelectionChange, ['bar']); onSelectionChange.mockReset(); gridListTester.setInteractionType('touch'); - await gridListTester.toggleRowSelection({index: 2}); + await gridListTester.toggleRowSelection({row: 2}); expect(onSelectionChange).toHaveBeenCalledTimes(1); checkSelection(onSelectionChange, ['bar', 'baz']); }); @@ -957,13 +957,13 @@ describe('ListView', function () { let gridListTester = testUtilUser.createTester('GridList', {root: tree.getByRole('grid')}); gridListTester.setInteractionType('keyboard'); - await gridListTester.triggerRowAction({index: 1}); + await gridListTester.triggerRowAction({row: 1}); expect(onSelectionChange).not.toHaveBeenCalled(); expect(onAction).toHaveBeenCalledTimes(1); expect(onAction).toHaveBeenLastCalledWith('bar'); onAction.mockReset(); - await gridListTester.toggleRowSelection({index: 2}); + await gridListTester.toggleRowSelection({row: 2}); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(onAction).not.toHaveBeenCalled(); checkSelection(onSelectionChange, ['baz']); diff --git a/packages/@react-spectrum/menu/test/MenuTrigger.test.js b/packages/@react-spectrum/menu/test/MenuTrigger.test.js index c89cecb1bfd..d2be39a84be 100644 --- a/packages/@react-spectrum/menu/test/MenuTrigger.test.js +++ b/packages/@react-spectrum/menu/test/MenuTrigger.test.js @@ -470,7 +470,7 @@ describe('MenuTrigger', function () { expect(onSelect).toBeCalledTimes(0); } - await menuTester.selectOption({optionText: 'Foo', menuSelectionMode: 'single', closesOnSelect: false}); + await menuTester.selectOption({option: 'Foo', menuSelectionMode: 'single', closesOnSelect: false}); if (Component === MenuTrigger) { expect(onSelectionChange).toBeCalledTimes(1); @@ -502,7 +502,7 @@ describe('MenuTrigger', function () { expect(onOpenChange).toBeCalledTimes(1); expect(onSelectionChange).toBeCalledTimes(0); menuTester.setInteractionType('keyboard'); - await menuTester.selectOption({optionText: 'Foo', menuSelectionMode: 'single', closesOnSelect: false}); + await menuTester.selectOption({option: 'Foo', menuSelectionMode: 'single', closesOnSelect: false}); expect(menuTester.menu).toBeInTheDocument(); expect(menuTester.trigger).toHaveAttribute('aria-expanded', 'true'); diff --git a/packages/@react-spectrum/picker/docs/Picker.mdx b/packages/@react-spectrum/picker/docs/Picker.mdx index f0faf73afe9..9646b62c200 100644 --- a/packages/@react-spectrum/picker/docs/Picker.mdx +++ b/packages/@react-spectrum/picker/docs/Picker.mdx @@ -614,7 +614,7 @@ it('Picker can select an option via keyboard', async function () { expect(trigger).toHaveTextContent('Select an item'); expect(trigger).not.toHaveAttribute('data-pressed'); - await selectTester.selectOption({optionText: 'Cat'}); + await selectTester.selectOption({option: 'Cat'}); expect(trigger).toHaveTextContent('Cat'); }); ``` diff --git a/packages/@react-spectrum/picker/test/Picker.test.js b/packages/@react-spectrum/picker/test/Picker.test.js index ad348d0c1d7..f5c6469f06f 100644 --- a/packages/@react-spectrum/picker/test/Picker.test.js +++ b/packages/@react-spectrum/picker/test/Picker.test.js @@ -990,7 +990,7 @@ describe('Picker', function () { expect(document.activeElement).toBe(listbox); - await selectTester.selectOption({optionText: 'Three'}); + await selectTester.selectOption({option: 'Three'}); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(onSelectionChange).toHaveBeenLastCalledWith('three'); @@ -1022,19 +1022,19 @@ describe('Picker', function () { expect(document.activeElement).toBe(listbox); - await selectTester.selectOption({optionText: 'Empty'}); + await selectTester.selectOption({option: 'Empty'}); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(onSelectionChange).toHaveBeenLastCalledWith(''); expect(document.activeElement).toBe(picker); expect(picker).toHaveTextContent('Empty'); - await selectTester.selectOption({optionText: 'Zero'}); + await selectTester.selectOption({option: 'Zero'}); expect(onSelectionChange).toHaveBeenCalledTimes(2); expect(onSelectionChange).toHaveBeenLastCalledWith('0'); expect(document.activeElement).toBe(picker); expect(picker).toHaveTextContent('Zero'); - await selectTester.selectOption({optionText: 'False'}); + await selectTester.selectOption({option: 'False'}); expect(onSelectionChange).toHaveBeenCalledTimes(3); expect(onSelectionChange).toHaveBeenLastCalledWith('false'); expect(document.activeElement).toBe(picker); @@ -1088,7 +1088,8 @@ describe('Picker', function () { expect(picker).toHaveTextContent('Two'); }); - it('can select items with the Enter key', async function () { + // TODO: re-enable after merge + it.skip('can select items with the Enter key', async function () { let {getByRole} = render( @@ -1111,7 +1112,7 @@ describe('Picker', function () { expect(items[1]).toHaveTextContent('Two'); expect(items[2]).toHaveTextContent('Three'); - await selectTester.selectOption({optionText: 'Two'}); + await selectTester.selectOption({option: 'Two'}); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(onSelectionChange).toHaveBeenLastCalledWith('two'); @@ -1192,7 +1193,7 @@ describe('Picker', function () { expect(items[1]).toHaveTextContent('Two'); expect(items[2]).toHaveTextContent('Three'); - await selectTester.selectOption({optionText: 'Three'}); + await selectTester.selectOption({option: 'Three'}); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(onOpenChangeSpy).toHaveBeenCalledTimes(2); @@ -1549,7 +1550,7 @@ describe('Picker', function () { let items = selectTester.options(); expect(document.activeElement).toBe(items[1]); - await selectTester.selectOption({optionText: 'Two'}); + await selectTester.selectOption({option: 'Two'}); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(onSelectionChange).toHaveBeenCalledWith('two'); @@ -2179,7 +2180,7 @@ describe('Picker', function () { expect(document.getElementById(picker.getAttribute('aria-describedby'))).toHaveTextContent('Constraints not satisfied'); expect(document.activeElement).toBe(picker); - await selectTester.selectOption({optionText: 'One'}); + await selectTester.selectOption({option: 'One'}); expect(picker).not.toHaveAttribute('aria-describedby'); }); @@ -2207,7 +2208,7 @@ describe('Picker', function () { expect(document.getElementById(picker.getAttribute('aria-describedby'))).toHaveTextContent('Invalid value'); expect(document.activeElement).toBe(picker); - await selectTester.selectOption({optionText: 'One'}); + await selectTester.selectOption({option: 'One'}); expect(picker).not.toHaveAttribute('aria-describedby'); }); @@ -2247,7 +2248,7 @@ describe('Picker', function () { expect(document.getElementById(picker.getAttribute('aria-describedby'))).toHaveTextContent('Invalid value.'); expect(input.validity.valid).toBe(false); - await selectTester.selectOption({optionText: 'One'}); + await selectTester.selectOption({option: 'One'}); expect(picker).not.toHaveAttribute('aria-describedby'); expect(input.validity.valid).toBe(true); }); @@ -2298,7 +2299,7 @@ describe('Picker', function () { expect(picker).toHaveAttribute('aria-describedby'); expect(document.getElementById(picker.getAttribute('aria-describedby'))).toHaveTextContent('Constraints not satisfied'); - await selectTester.selectOption({optionText: 'One'}); + await selectTester.selectOption({option: 'One'}); expect(picker).not.toHaveAttribute('aria-describedby'); await user.click(getByTestId('reset')); @@ -2326,7 +2327,7 @@ describe('Picker', function () { expect(document.getElementById(picker.getAttribute('aria-describedby'))).toHaveTextContent('Invalid value'); expect(input.validity.valid).toBe(true); - await selectTester.selectOption({optionText: 'One'}); + await selectTester.selectOption({option: 'One'}); expect(picker).not.toHaveAttribute('aria-describedby'); }); @@ -2347,7 +2348,7 @@ describe('Picker', function () { expect(picker).toHaveAttribute('aria-describedby'); expect(document.getElementById(picker.getAttribute('aria-describedby'))).toHaveTextContent('Invalid value'); - await selectTester.selectOption({optionText: 'One'}); + await selectTester.selectOption({option: 'One'}); expect(picker).not.toHaveAttribute('aria-describedby'); }); }); diff --git a/packages/@react-spectrum/picker/test/TempUtilTest.test.js b/packages/@react-spectrum/picker/test/TempUtilTest.test.js index dbbd1475274..54d1e5428d0 100644 --- a/packages/@react-spectrum/picker/test/TempUtilTest.test.js +++ b/packages/@react-spectrum/picker/test/TempUtilTest.test.js @@ -103,7 +103,7 @@ describe('Picker/Select ', function () { ); let selectTester = testUtilUser.createTester('Select', {root: screen.getByTestId('test')}); - await selectTester.selectOption({optionText: 'Three'}); + await selectTester.selectOption({option: 'Three'}); expect(selectTester.trigger).toHaveTextContent('Three'); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(onSelectionChange).toHaveBeenLastCalledWith('three'); @@ -129,7 +129,7 @@ describe('Picker/Select ', function () { ); let selectTester = testUtilUser.createTester('Select', {root: screen.getByTestId('test')}); - await selectTester.selectOption({optionText: 'Cat'}); + await selectTester.selectOption({option: 'Cat'}); expect(selectTester.trigger).toHaveTextContent('Cat'); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(onSelectionChange).toHaveBeenLastCalledWith('cat'); @@ -211,7 +211,7 @@ describe('Picker/Select ', function () { ); let selectTester = testUtilUser.createTester('Select', {root: screen.getByTestId('test')}); - await selectTester.selectOption({optionText: 'Three'}); + await selectTester.selectOption({option: 'Three'}); expect(selectTester.trigger).toHaveTextContent('Three'); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(onSelectionChange).toHaveBeenLastCalledWith('three'); @@ -237,7 +237,7 @@ describe('Picker/Select ', function () { ); let selectTester = testUtilUser.createTester('Select', {root: screen.getAllByTestId('test')[0]}); - await selectTester.selectOption({optionText: 'Cat'}); + await selectTester.selectOption({option: 'Cat'}); expect(selectTester.trigger).toHaveTextContent('Cat'); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(onSelectionChange).toHaveBeenLastCalledWith('cat'); diff --git a/packages/@react-spectrum/table/docs/TableView.mdx b/packages/@react-spectrum/table/docs/TableView.mdx index 18d04c7bcc1..e4b5613caae 100644 --- a/packages/@react-spectrum/table/docs/TableView.mdx +++ b/packages/@react-spectrum/table/docs/TableView.mdx @@ -1984,7 +1984,7 @@ it('TableView can toggle row selection', async function () { await tableTester.toggleSelectAll(); expect(tableTester.selectedRows).toHaveLength(10); - await tableTester.toggleRowSelection({index: 2}); + await tableTester.toggleRowSelection({row: 2}); expect(tableTester.selectedRows).toHaveLength(9); let checkbox = within(tableTester.rows[2]).getByRole('checkbox'); expect(checkbox).not.toBeChecked(); diff --git a/packages/@react-spectrum/table/test/Table.test.js b/packages/@react-spectrum/table/test/Table.test.js index 4955f80dcdb..757732b5fe7 100644 --- a/packages/@react-spectrum/table/test/Table.test.js +++ b/packages/@react-spectrum/table/test/Table.test.js @@ -2851,20 +2851,20 @@ export let tableTests = () => { act(() => jest.runAllTimers()); await user.pointer({target: document.body, keys: '[TouchA]'}); - await tableTester.toggleRowSelection({text: 'Foo 5', needsLongPress: true}); + await tableTester.toggleRowSelection({row: 'Foo 5', needsLongPress: true}); checkSelection(onSelectionChange, ['Foo 5']); expect(onAction).not.toHaveBeenCalled(); onSelectionChange.mockReset(); - await tableTester.toggleRowSelection({text: 'Foo 10', needsLongPress: false}); + await tableTester.toggleRowSelection({row: 'Foo 10', needsLongPress: false}); checkSelection(onSelectionChange, ['Foo 5', 'Foo 10']); // Deselect all to exit selection mode onSelectionChange.mockReset(); - await tableTester.toggleRowSelection({text: 'Foo 10', needsLongPress: false}); + await tableTester.toggleRowSelection({row: 'Foo 10', needsLongPress: false}); checkSelection(onSelectionChange, ['Foo 5']); onSelectionChange.mockReset(); - await tableTester.toggleRowSelection({text: 'Foo 5', needsLongPress: false}); + await tableTester.toggleRowSelection({row: 'Foo 5', needsLongPress: false}); act(() => jest.runAllTimers()); checkSelection(onSelectionChange, []); expect(onAction).not.toHaveBeenCalled(); @@ -2987,11 +2987,11 @@ export let tableTests = () => { tableTester.setInteractionType('touch'); expect(tree.queryByLabelText('Select All')).toBeNull(); - await tableTester.toggleRowSelection({text: 'Baz 5'}); + await tableTester.toggleRowSelection({row: 'Baz 5'}); expect(announce).toHaveBeenLastCalledWith('Foo 5 selected.'); expect(announce).toHaveBeenCalledTimes(1); onSelectionChange.mockReset(); - await tableTester.toggleRowSelection({text: 'Foo 10'}); + await tableTester.toggleRowSelection({row: 'Foo 10'}); expect(announce).toHaveBeenLastCalledWith('Foo 10 selected. 2 items selected.'); expect(announce).toHaveBeenCalledTimes(2); @@ -3017,7 +3017,7 @@ export let tableTests = () => { let tree = renderTable({onSelectionChange, selectionStyle: 'highlight', onAction}); let tableTester = testUtilUser.createTester('Table', {root: tree.getByRole('grid')}); - await tableTester.toggleRowSelection({text: 'Foo 5'}); + await tableTester.toggleRowSelection({row: 'Foo 5'}); expect(announce).toHaveBeenLastCalledWith('Foo 5 selected.'); expect(announce).toHaveBeenCalledTimes(1); checkSelection(onSelectionChange, ['Foo 5']); @@ -3025,7 +3025,7 @@ export let tableTests = () => { announce.mockReset(); onSelectionChange.mockReset(); - await tableTester.triggerRowAction({text: 'Foo 5', needsDoubleClick: true}); + await tableTester.triggerRowAction({row: 'Foo 5', needsDoubleClick: true}); expect(announce).not.toHaveBeenCalled(); expect(onSelectionChange).not.toHaveBeenCalled(); expect(onAction).toHaveBeenCalledTimes(1); @@ -3175,12 +3175,12 @@ export let tableTests = () => { checkSelection(onSelectionChange, ['Foo 5', 'Foo 10']); // Deselect all to exit selection mode - await tableTester.toggleRowSelection({text: 'Foo 10'}); + await tableTester.toggleRowSelection({row: 'Foo 10'}); expect(announce).toHaveBeenLastCalledWith('Foo 10 not selected. 1 item selected.'); expect(announce).toHaveBeenCalledTimes(3); onSelectionChange.mockReset(); - await tableTester.toggleRowSelection({text: 'Foo 5'}); + await tableTester.toggleRowSelection({row: 'Foo 5'}); expect(announce).toHaveBeenLastCalledWith('Foo 5 not selected.'); expect(announce).toHaveBeenCalledTimes(4); @@ -4381,7 +4381,7 @@ export let tableTests = () => { expect(document.getElementById(columnheaders[1].getAttribute('aria-describedby'))).toHaveTextContent('sortable column, ascending'); expect(columnheaders[2]).not.toHaveAttribute('aria-describedby'); - await tableTester.toggleSort({index: 1}); + await tableTester.toggleSort({column: 1}); expect(document.getElementById(columnheaders[1].getAttribute('aria-describedby'))).toHaveTextContent('sortable column, descending'); uaMock.mockRestore(); diff --git a/packages/@react-spectrum/table/test/TestTableUtils.test.js b/packages/@react-spectrum/table/test/TestTableUtils.test.js index 422889e426e..8fef6bc6fd4 100644 --- a/packages/@react-spectrum/table/test/TestTableUtils.test.js +++ b/packages/@react-spectrum/table/test/TestTableUtils.test.js @@ -76,11 +76,11 @@ describe('Table ', function () { render(); let tableTester = testUtilRealTimer.createTester('Table', {root: screen.getByTestId('test')}); tableTester.setInteractionType(interactionType); - await tableTester.toggleRowSelection({index: 2}); + await tableTester.toggleRowSelection({row: 2}); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(new Set(onSelectionChange.mock.calls[0][0])).toEqual(new Set(['Foo 3'])); - await tableTester.toggleRowSelection({text: 'Foo 4'}); + await tableTester.toggleRowSelection({row: 'Foo 4'}); expect(onSelectionChange).toHaveBeenCalledTimes(2); expect(new Set(onSelectionChange.mock.calls[1][0])).toEqual(new Set(['Foo 3', 'Foo 4'])); @@ -88,15 +88,15 @@ describe('Table ', function () { expect(onSelectionChange).toHaveBeenCalledTimes(3); expect((onSelectionChange.mock.calls[2][0])).toEqual('all'); - await tableTester.toggleSort({index: 2}); + await tableTester.toggleSort({column: 2}); expect(onSortChange).toHaveBeenCalledTimes(1); expect(onSortChange).toHaveBeenLastCalledWith({column: 'bar', direction: 'ascending'}); - await tableTester.toggleSort({text: 'Foo'}); + await tableTester.toggleSort({column: 'Foo'}); expect(onSortChange).toHaveBeenCalledTimes(2); expect(onSortChange).toHaveBeenLastCalledWith({column: 'foo', direction: 'ascending'}); - await tableTester.toggleSort({text: 'Foo'}); + await tableTester.toggleSort({column: 'Foo'}); expect(onSortChange).toHaveBeenCalledTimes(3); expect(onSortChange).toHaveBeenLastCalledWith({column: 'foo', direction: 'descending'}); }); @@ -106,23 +106,23 @@ describe('Table ', function () { render(); let tableTester = testUtilRealTimer.createTester('Table', {root: screen.getByTestId('test')}); tableTester.setInteractionType(interactionType); - await tableTester.toggleRowSelection({index: 2}); + await tableTester.toggleRowSelection({row: 2}); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(new Set(onSelectionChange.mock.calls[0][0])).toEqual(new Set(['Foo 3'])); - await tableTester.toggleRowSelection({text: 'Foo 4'}); + await tableTester.toggleRowSelection({row: 'Foo 4'}); expect(onSelectionChange).toHaveBeenCalledTimes(2); expect(new Set(onSelectionChange.mock.calls[1][0])).toEqual(new Set(['Foo 4'])); - await tableTester.toggleSort({index: 2}); + await tableTester.toggleSort({column: 2}); expect(onSortChange).toHaveBeenCalledTimes(1); expect(onSortChange).toHaveBeenLastCalledWith({column: 'baz', direction: 'ascending'}); - await tableTester.toggleSort({text: 'Foo'}); + await tableTester.toggleSort({column: 'Foo'}); expect(onSortChange).toHaveBeenCalledTimes(2); expect(onSortChange).toHaveBeenLastCalledWith({column: 'foo', direction: 'ascending'}); - await tableTester.toggleSort({text: 'Foo'}); + await tableTester.toggleSort({column: 'Foo'}); expect(onSortChange).toHaveBeenCalledTimes(3); expect(onSortChange).toHaveBeenLastCalledWith({column: 'foo', direction: 'descending'}); }); @@ -148,11 +148,11 @@ describe('Table ', function () { render(); let tableTester = testUtilFakeTimer.createTester('Table', {root: screen.getByTestId('test')}); tableTester.setInteractionType(interactionType); - await tableTester.toggleRowSelection({index: 2}); + await tableTester.toggleRowSelection({row: 2}); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(new Set(onSelectionChange.mock.calls[0][0])).toEqual(new Set(['Foo 3'])); - await tableTester.toggleRowSelection({text: 'Foo 4'}); + await tableTester.toggleRowSelection({row: 'Foo 4'}); expect(onSelectionChange).toHaveBeenCalledTimes(2); expect(new Set(onSelectionChange.mock.calls[1][0])).toEqual(new Set(['Foo 3', 'Foo 4'])); @@ -160,15 +160,15 @@ describe('Table ', function () { expect(onSelectionChange).toHaveBeenCalledTimes(3); expect((onSelectionChange.mock.calls[2][0])).toEqual('all'); - await tableTester.toggleSort({index: 2}); + await tableTester.toggleSort({column: 2}); expect(onSortChange).toHaveBeenCalledTimes(1); expect(onSortChange).toHaveBeenLastCalledWith({column: 'bar', direction: 'ascending'}); - await tableTester.toggleSort({text: 'Foo'}); + await tableTester.toggleSort({column: 'Foo'}); expect(onSortChange).toHaveBeenCalledTimes(2); expect(onSortChange).toHaveBeenLastCalledWith({column: 'foo', direction: 'ascending'}); - await tableTester.toggleSort({text: 'Foo'}); + await tableTester.toggleSort({column: 'Foo'}); expect(onSortChange).toHaveBeenCalledTimes(3); expect(onSortChange).toHaveBeenLastCalledWith({column: 'foo', direction: 'descending'}); }); @@ -179,23 +179,23 @@ describe('Table ', function () { let tableTester = testUtilFakeTimer.createTester('Table', {root: screen.getByTestId('test')}); tableTester.setInteractionType(interactionType); - await tableTester.toggleRowSelection({index: 2, focusToSelect: true}); + await tableTester.toggleRowSelection({row: 2, focusToSelect: true}); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(new Set(onSelectionChange.mock.calls[0][0])).toEqual(new Set(['Foo 3'])); - await tableTester.toggleRowSelection({text: 'Foo 4', focusToSelect: true}); + await tableTester.toggleRowSelection({row: 'Foo 4', focusToSelect: true}); expect(onSelectionChange).toHaveBeenCalledTimes(2); expect(new Set(onSelectionChange.mock.calls[1][0])).toEqual(new Set(['Foo 4'])); - await tableTester.toggleSort({index: 2}); + await tableTester.toggleSort({column: 2}); expect(onSortChange).toHaveBeenCalledTimes(1); expect(onSortChange).toHaveBeenLastCalledWith({column: 'baz', direction: 'ascending'}); - await tableTester.toggleSort({text: 'Foo'}); + await tableTester.toggleSort({column: 'Foo'}); expect(onSortChange).toHaveBeenCalledTimes(2); expect(onSortChange).toHaveBeenLastCalledWith({column: 'foo', direction: 'ascending'}); - await tableTester.toggleSort({text: 'Foo'}); + await tableTester.toggleSort({column: 'Foo'}); expect(onSortChange).toHaveBeenCalledTimes(3); expect(onSortChange).toHaveBeenLastCalledWith({column: 'foo', direction: 'descending'}); }); diff --git a/packages/react-aria-components/docs/GridList.mdx b/packages/react-aria-components/docs/GridList.mdx index ca8fcc831d5..cbe922a3b9e 100644 --- a/packages/react-aria-components/docs/GridList.mdx +++ b/packages/react-aria-components/docs/GridList.mdx @@ -1895,12 +1895,12 @@ it('GridList can select a row via keyboard', async function () { expect(within(row).getByRole('checkbox')).not.toBeChecked(); expect(gridListTester.selectedRows).toHaveLength(0); - await gridListTester.toggleRowSelection({index: 0}); + await gridListTester.toggleRowSelection({row: 0}); expect(row).toHaveClass('selected'); expect(within(row).getByRole('checkbox')).toBeChecked(); expect(gridListTester.selectedRows).toHaveLength(1); - await gridListTester.toggleRowSelection({index: 0}); + await gridListTester.toggleRowSelection({row: 0}); expect(row).not.toHaveClass('selected'); expect(within(row).getByRole('checkbox')).not.toBeChecked(); expect(gridListTester.selectedRows).toHaveLength(0); diff --git a/packages/react-aria-components/docs/Select.mdx b/packages/react-aria-components/docs/Select.mdx index 36d8f6438f0..442ff2512c1 100644 --- a/packages/react-aria-components/docs/Select.mdx +++ b/packages/react-aria-components/docs/Select.mdx @@ -1264,7 +1264,7 @@ it('Select can select an option via keyboard', async function () { let trigger = selectTester.trigger; expect(trigger).toHaveTextContent('Select an item'); - await selectTester.selectOption({optionText: 'Cat'}); + await selectTester.selectOption({option: 'Cat'}); expect(trigger).toHaveTextContent('Cat'); }); ``` diff --git a/packages/react-aria-components/docs/Table.mdx b/packages/react-aria-components/docs/Table.mdx index dde95b257c5..0e814c30f2c 100644 --- a/packages/react-aria-components/docs/Table.mdx +++ b/packages/react-aria-components/docs/Table.mdx @@ -2504,7 +2504,7 @@ it('Table can toggle row selection', async function () { await tableTester.toggleSelectAll(); expect(tableTester.selectedRows).toHaveLength(10); - await tableTester.toggleRowSelection({index: 2}); + await tableTester.toggleRowSelection({row: 2}); expect(tableTester.selectedRows).toHaveLength(9); let checkbox = within(tableTester.rows[2]).getByRole('checkbox'); expect(checkbox).not.toBeChecked(); diff --git a/packages/react-aria-components/test/GridList.test.js b/packages/react-aria-components/test/GridList.test.js index f5f65bb0ef9..80c3ec41255 100644 --- a/packages/react-aria-components/test/GridList.test.js +++ b/packages/react-aria-components/test/GridList.test.js @@ -226,12 +226,12 @@ describe('GridList', () => { expect(row).not.toHaveClass('selected'); expect(within(row).getByRole('checkbox')).not.toBeChecked(); - await gridListTester.toggleRowSelection({index: 0}); + await gridListTester.toggleRowSelection({row: 0}); expect(row).toHaveAttribute('aria-selected', 'true'); expect(row).toHaveClass('selected'); expect(within(row).getByRole('checkbox')).toBeChecked(); - await gridListTester.toggleRowSelection({index: 0}); + await gridListTester.toggleRowSelection({row: 0}); expect(row).not.toHaveAttribute('aria-selected', 'true'); expect(row).not.toHaveClass('selected'); expect(within(row).getByRole('checkbox')).not.toBeChecked(); diff --git a/packages/react-aria-components/test/Select.test.js b/packages/react-aria-components/test/Select.test.js index aee26a60a51..e5cc58c3ac7 100644 --- a/packages/react-aria-components/test/Select.test.js +++ b/packages/react-aria-components/test/Select.test.js @@ -245,7 +245,7 @@ describe('Select', () => { expect(select).toHaveAttribute('data-invalid'); expect(document.activeElement).toBe(trigger); - await selectTester.selectOption({optionText: 'Cat'}); + await selectTester.selectOption({option: 'Cat'}); expect(selectTester.trigger).not.toHaveAttribute('aria-describedby'); expect(select).not.toHaveAttribute('data-invalid'); }); diff --git a/packages/react-aria-components/test/Table.test.js b/packages/react-aria-components/test/Table.test.js index 76d752cfbda..a0be3599619 100644 --- a/packages/react-aria-components/test/Table.test.js +++ b/packages/react-aria-components/test/Table.test.js @@ -618,7 +618,7 @@ describe('Table', () => { ); let tableTester = testUtilUser.createTester('Table', {root: getByRole('grid')}); - await tableTester.triggerRowAction({index: 0}); + await tableTester.triggerRowAction({row: 0}); expect(onAction).toHaveBeenCalled(); }); @@ -639,7 +639,7 @@ describe('Table', () => { expect(columns[2]).toHaveAttribute('aria-sort', 'none'); expect(columns[2]).not.toHaveTextContent('▲'); - await tableTester.toggleSort({index: 0}); + await tableTester.toggleSort({column: 0}); expect(onSortChange).toHaveBeenCalledTimes(1); expect(onSortChange).toHaveBeenCalledWith({column: 'name', direction: 'descending'}); }); From 29afbafafb7df410c0f3435ef1e773a0690d6528 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Mon, 9 Dec 2024 15:50:29 -0800 Subject: [PATCH 12/19] fix tests and more consistency refactors --- .../@react-aria/test-utils/src/combobox.ts | 17 +++++--- .../@react-aria/test-utils/src/gridlist.ts | 25 ++++++----- packages/@react-aria/test-utils/src/menu.ts | 39 ++++++++---------- packages/@react-aria/test-utils/src/select.ts | 41 +++++++++++++++---- packages/@react-aria/test-utils/src/table.ts | 24 +++++++---- .../@react-spectrum/menu/docs/MenuTrigger.mdx | 2 +- packages/react-aria-components/docs/Menu.mdx | 2 +- .../test/AriaMenu.test-util.tsx | 40 +++++++++--------- .../react-aria-components/test/Menu.test.tsx | 2 +- .../react-aria-components/test/Select.test.js | 15 +++++++ 10 files changed, 125 insertions(+), 82 deletions(-) diff --git a/packages/@react-aria/test-utils/src/combobox.ts b/packages/@react-aria/test-utils/src/combobox.ts index fc001f45ecd..c496e8f2ea7 100644 --- a/packages/@react-aria/test-utils/src/combobox.ts +++ b/packages/@react-aria/test-utils/src/combobox.ts @@ -127,16 +127,13 @@ export class ComboBoxTester { } = opts; let option; - let options = this.options; + let options = this.options(); let listbox = this.listbox; if (typeof optionIndexOrText === 'number') { option = options[optionIndexOrText]; } else if (typeof optionIndexOrText === 'string' && listbox != null) { - option = within(listbox).getByText(optionIndexOrText); - while (option && option.getAttribute('role') !== 'option') { - option = option.parentElement; - } + option = (within(listbox!).getByText(optionIndexOrText).closest('[role=option]'))! as HTMLElement; } return option; @@ -153,11 +150,19 @@ export class ComboBoxTester { } let listbox = this.listbox; + if (!listbox) { + throw new Error('Combobox\'s listbox not found.'); + } + if (listbox) { if (typeof option === 'string' || typeof option === 'number') { option = this.findOption({optionIndexOrText: option}); } + if (!option) { + throw new Error('Target option not found in the listbox.'); + } + // TODO: keyboard method of selecting the the option is a bit tricky unless I simply simulate the user pressing the down arrow // the required amount of times to reach the option. For now just click the option even in keyboard mode if (interactionType === 'mouse' || interactionType === 'keyboard') { @@ -166,7 +171,7 @@ export class ComboBoxTester { await this.user.pointer({target: option, keys: '[TouchA]'}); } - if (option && option.getAttribute('href') == null) { + if (option.getAttribute('href') == null) { await waitFor(() => { if (document.contains(listbox)) { throw new Error('Expected listbox element to not be in the document after selecting an option'); diff --git a/packages/@react-aria/test-utils/src/gridlist.ts b/packages/@react-aria/test-utils/src/gridlist.ts index 1ddc844be9b..8e0f8c9c133 100644 --- a/packages/@react-aria/test-utils/src/gridlist.ts +++ b/packages/@react-aria/test-utils/src/gridlist.ts @@ -91,10 +91,7 @@ export class GridListTester { if (typeof rowIndexOrText === 'number') { row = this.rows[rowIndexOrText]; } else if (typeof rowIndexOrText === 'string') { - row = within(this?.gridlist).getByText(rowIndexOrText); - while (row && row.getAttribute('role') !== 'row') { - row = row.parentElement; - } + row = (within(this.gridlist!).getByText(rowIndexOrText).closest('[role=row]'))! as HTMLElement; } return row; @@ -116,15 +113,17 @@ export class GridListTester { row = this.findRow({rowIndexOrText: row}); } - if (row) { - if (needsDoubleClick) { - await this.user.dblClick(row); - } else if (interactionType === 'keyboard') { - act(() => row.focus()); - await this.user.keyboard('[Enter]'); - } else { - await pressElement(this.user, row, interactionType); - } + if (!row) { + throw new Error('Target row not found in the gridlist.'); + } + + if (needsDoubleClick) { + await this.user.dblClick(row); + } else if (interactionType === 'keyboard') { + act(() => row.focus()); + await this.user.keyboard('[Enter]'); + } else { + await pressElement(this.user, row, interactionType); } } diff --git a/packages/@react-aria/test-utils/src/menu.ts b/packages/@react-aria/test-utils/src/menu.ts index ae4c8dde5de..1b8abe6273b 100644 --- a/packages/@react-aria/test-utils/src/menu.ts +++ b/packages/@react-aria/test-utils/src/menu.ts @@ -53,13 +53,9 @@ interface MenuSelectOpts extends MenuOpenOpts { interface MenuOpenSubmenuOpts extends MenuOpenOpts { /** - * The submenu trigger to open. Available submenu trigger nodes can be sourced via `submenuTriggers`. + * The text or node of the submenu trigger to open. Available submenu trigger nodes can be sourced via `submenuTriggers`. */ - submenuTrigger?: HTMLElement, - /** - * The text of submenu trigger to open. Alternative to `submenuTrigger`. - */ - submenuTriggerText?: string + submenuTrigger: string | HTMLElement } export class MenuTester { @@ -164,14 +160,13 @@ export class MenuTester { } = opts; let option; - let options = this.options; + let options = this.options(); let menu = this.menu; if (typeof optionIndexOrText === 'number') { option = options[optionIndexOrText]; } else if (typeof optionIndexOrText === 'string' && menu != null) { - // TODO: do the same in other utils - option = (within(menu!).getByText(option).closest('[role=menuitem], [role=menuitemradio], [role=menuitemcheckbox]'))! as HTMLElement; + option = (within(menu!).getByText(optionIndexOrText).closest('[role=menuitem], [role=menuitemradio], [role=menuitemcheckbox]'))! as HTMLElement; } return option; @@ -199,14 +194,18 @@ export class MenuTester { } let menu = this.menu; + + if (!menu) { + throw new Error('Menu not found.'); + } + if (menu) { if (typeof option === 'string' || typeof option === 'number') { option = this.findOption({optionIndexOrText: option}); } - // TODO: add this to other utils if (!option) { - throw new Error('No option found in the menu.'); + throw new Error('Target option not found in the menu.'); } if (interactionType === 'keyboard') { @@ -225,7 +224,7 @@ export class MenuTester { } act(() => {jest.runAllTimers();}); - if (option && option.getAttribute('href') == null && option.getAttribute('aria-haspopup') == null && menuSelectionMode === 'single' && closesOnSelect && keyboardActivation !== 'Space' && !this._isSubmenu) { + if (option.getAttribute('href') == null && option.getAttribute('aria-haspopup') == null && menuSelectionMode === 'single' && closesOnSelect && keyboardActivation !== 'Space' && !this._isSubmenu) { await waitFor(() => { if (document.activeElement !== trigger) { throw new Error(`Expected the document.activeElement after selecting an option to be the menu trigger but got ${document.activeElement}`); @@ -250,7 +249,6 @@ export class MenuTester { async openSubmenu(opts: MenuOpenSubmenuOpts): Promise { let { submenuTrigger, - submenuTriggerText, needsLongPress, interactionType = this._interactionType } = opts; @@ -263,19 +261,16 @@ export class MenuTester { if (!isDisabled) { let menu = this.menu; if (menu) { - let submenu; - if (submenuTrigger) { - submenu = submenuTrigger; - } else if (submenuTriggerText) { - submenu = within(menu).getByText(submenuTriggerText); + if (typeof submenuTrigger === 'string') { + submenuTrigger = within(menu).getByText(submenuTrigger); } - let submenuTriggerTester = new MenuTester({user: this.user, interactionType: this._interactionType, root: submenu, isSubmenu: true}); + let submenuTriggerTester = new MenuTester({user: this.user, interactionType: this._interactionType, root: submenuTrigger, isSubmenu: true}); if (interactionType === 'mouse') { - await this.user.pointer({target: submenu}); + await this.user.pointer({target: submenuTrigger}); act(() => {jest.runAllTimers();}); } else if (interactionType === 'keyboard') { - await this.keyboardNavigateToOption({option: submenu}); + await this.keyboardNavigateToOption({option: submenuTrigger}); await this.user.keyboard('[ArrowRight]'); act(() => {jest.runAllTimers();}); } else { @@ -290,7 +285,7 @@ export class MenuTester { return null; } - keyboardNavigateToOption = async (opts: {option: HTMLElement}) => { + private async keyboardNavigateToOption(opts: {option: HTMLElement}) { let {option} = opts; let options = this.options(); let targetIndex = options.indexOf(option); diff --git a/packages/@react-aria/test-utils/src/select.ts b/packages/@react-aria/test-utils/src/select.ts index 69e6b1a7afe..bfe94678ef9 100644 --- a/packages/@react-aria/test-utils/src/select.ts +++ b/packages/@react-aria/test-utils/src/select.ts @@ -118,21 +118,39 @@ export class SelectTester { } = opts; let option; - let options = this.options; + let options = this.options(); let listbox = this.listbox; if (typeof optionIndexOrText === 'number') { option = options[optionIndexOrText]; } else if (typeof optionIndexOrText === 'string' && listbox != null) { - option = within(listbox).getByText(optionIndexOrText); - while (option && option.getAttribute('role') !== 'option') { - option = option.parentElement; - } + option = (within(listbox!).getByText(optionIndexOrText).closest('[role=option]'))! as HTMLElement; } return option; } + private async keyboardNavigateToOption(opts: {option: HTMLElement}) { + let {option} = opts; + let options = this.options(); + let targetIndex = options.indexOf(option); + if (targetIndex === -1) { + throw new Error('Option provided is not in the listbox'); + } + if (document.activeElement === this.listbox) { + await this.user.keyboard('[ArrowDown]'); + } + let currIndex = options.indexOf(document.activeElement as HTMLElement); + if (targetIndex === -1) { + throw new Error('ActiveElement is not in the listbox'); + } + let direction = targetIndex > currIndex ? 'down' : 'up'; + + for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) { + await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`); + } + }; + // TODO: update this so it also can take the option node instead of just text, might already have been added in Rob's PR /** * Selects the desired select option. Defaults to using the interaction type set on the select tester. If necessary, will open the select dropdown beforehand. @@ -148,19 +166,24 @@ export class SelectTester { await this.open(); } let listbox = this.listbox; + if (!listbox) { + throw new Error('Select\'s listbox not found.'); + } + if (listbox) { if (typeof option === 'string' || typeof option === 'number') { option = this.findOption({optionIndexOrText: option}); } + if (!option) { + throw new Error('Target option not found in the listbox.'); + } + if (interactionType === 'keyboard') { if (document.activeElement !== listbox || !listbox.contains(document.activeElement)) { act(() => listbox.focus()); } - - // TODO: this simulates typeahead, do we want to add a helper util for that? Not sure if users would really need that for - // their test - // await this.user.keyboard(option); + await this.keyboardNavigateToOption({option}); await this.user.keyboard('[Enter]'); } else { // TODO: what if the user needs to scroll the list to find the option? What if there are multiple matches for text (hopefully the picker options are pretty unique) diff --git a/packages/@react-aria/test-utils/src/table.ts b/packages/@react-aria/test-utils/src/table.ts index de92e3c2f20..2d704f3f12a 100644 --- a/packages/@react-aria/test-utils/src/table.ts +++ b/packages/@react-aria/test-utils/src/table.ts @@ -91,6 +91,10 @@ export class TableTester { row = this.findRow({rowIndexOrText: row}); } + if (!row) { + throw new Error('Target row not found in the table.'); + } + let rowCheckbox = within(row).queryByRole('checkbox'); if (rowCheckbox) { await pressElement(this.user, rowCheckbox, interactionType); @@ -219,15 +223,17 @@ export class TableTester { row = this.findRow({rowIndexOrText: row}); } - if (row) { - if (needsDoubleClick) { - await this.user.dblClick(row); - } else if (interactionType === 'keyboard') { - act(() => row.focus()); - await this.user.keyboard('[Enter]'); - } else { - await pressElement(this.user, row, interactionType); - } + if (!row) { + throw new Error('Target row not found in the table.'); + } + + if (needsDoubleClick) { + await this.user.dblClick(row); + } else if (interactionType === 'keyboard') { + act(() => row.focus()); + await this.user.keyboard('[Enter]'); + } else { + await pressElement(this.user, row, interactionType); } } diff --git a/packages/@react-spectrum/menu/docs/MenuTrigger.mdx b/packages/@react-spectrum/menu/docs/MenuTrigger.mdx index 3efbe16f862..347d289d7fd 100644 --- a/packages/@react-spectrum/menu/docs/MenuTrigger.mdx +++ b/packages/@react-spectrum/menu/docs/MenuTrigger.mdx @@ -284,7 +284,7 @@ it('Menu can open its submenu via keyboard', async function () { let submenuTriggers = menuTester.submenuTriggers; expect(submenuTriggers).toHaveLength(1); - let submenuTester = await menuTester.openSubmenu({submenuTriggerText: 'Share…'}); + let submenuTester = await menuTester.openSubmenu({submenuTrigger: 'Share…'}); expect(submenuTester.menu).toBeInTheDocument(); await submenuTester.selectOption({option: submenuUtil.options()[0]}); diff --git a/packages/react-aria-components/docs/Menu.mdx b/packages/react-aria-components/docs/Menu.mdx index c483003adad..d241e432bd5 100644 --- a/packages/react-aria-components/docs/Menu.mdx +++ b/packages/react-aria-components/docs/Menu.mdx @@ -1169,7 +1169,7 @@ it('Menu can open its submenu via keyboard', async function () { let submenuTriggers = menuTester.submenuTriggers; expect(submenuTriggers).toHaveLength(1); - let submenuTester = await menuTester.openSubmenu({submenuTriggerText: 'Share…'}); + let submenuTester = await menuTester.openSubmenu({submenuTrigger: 'Share…'}); expect(submenuTester.menu).toBeInTheDocument(); await submenuTester.selectOption({option: submenuUtil.options()[0]}); diff --git a/packages/react-aria-components/test/AriaMenu.test-util.tsx b/packages/react-aria-components/test/AriaMenu.test-util.tsx index 985a75cff4b..fb0fec16cd8 100644 --- a/packages/react-aria-components/test/AriaMenu.test-util.tsx +++ b/packages/react-aria-components/test/AriaMenu.test-util.tsx @@ -189,7 +189,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps) => let menu = menuTester.menu; expect(menu).toBeTruthy(); - let options = menuTester.options; + let options = menuTester.options(); expect(options[0]).toHaveFocus(); await menuTester.close(); @@ -210,7 +210,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps) => let menu = menuTester.menu; expect(menu).toBeTruthy(); - let options = menuTester.options; + let options = menuTester.options(); expect(options[0]).toHaveFocus(); await menuTester.close(); @@ -231,7 +231,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps) => let menu = menuTester.menu; expect(menu).toBeTruthy(); - let options = menuTester.options; + let options = menuTester.options(); expect(options[options.length - 1]).toHaveFocus(); await menuTester.close(); @@ -254,7 +254,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps) => expect(menu).toBeTruthy(); expect(menu).toHaveAttribute('aria-labelledby', triggerButton.id); - let options = menuTester.options; + let options = menuTester.options(); expect(options[0]).toHaveFocus(); await user.keyboard('[ArrowUp]'); @@ -341,7 +341,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps) => expect(menu).toBeTruthy(); expect(menu).toHaveAttribute('aria-labelledby', triggerButton.id); - let options = menuTester.options; + let options = menuTester.options(); await menuTester.selectOption({option: options[1], menuSelectionMode: 'single'}); @@ -351,7 +351,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps) => await menuTester.open(); act(() => {jest.runAllTimers();}); - options = menuTester.options; + options = menuTester.options(); expect(options[0]).toHaveAttribute('aria-checked', 'false'); expect(options[1]).toHaveAttribute('aria-checked', 'true'); }); @@ -368,7 +368,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps) => expect(menu).toBeTruthy(); expect(menu).toHaveAttribute('aria-labelledby', triggerButton.id); - let options = menuTester.options; + let options = menuTester.options(); expect(options[0]).toHaveFocus(); await menuTester.selectOption({option: options[1], menuSelectionMode: 'single'}); @@ -379,7 +379,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps) => await menuTester.open(); act(() => {jest.runAllTimers();}); - options = menuTester.options; + options = menuTester.options(); expect(options[0]).toHaveAttribute('aria-checked', 'false'); expect(options[1]).toHaveAttribute('aria-checked', 'true'); expect(options[1]).toHaveFocus(); @@ -389,7 +389,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps) => await menuTester.open(); act(() => {jest.runAllTimers();}); - options = menuTester.options; + options = menuTester.options(); expect(options[0]).toHaveAttribute('aria-checked', 'false'); expect(options[1]).toHaveAttribute('aria-checked', 'true'); expect(options[1]).toHaveFocus(); @@ -408,7 +408,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps) => expect(menu).toBeTruthy(); expect(menu).toHaveAttribute('aria-labelledby', triggerButton.id); - let options = menuTester.options; + let options = menuTester.options(); expect(options[0]).toHaveFocus(); await user.keyboard('[ArrowDown]'); @@ -433,7 +433,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps) => let menu = menuTester.menu; expect(menu).toBeInTheDocument(); await user.keyboard('{/Enter}'); - expect(menuTester.options.filter(option => option.getAttribute('aria-checked') === 'true').length).toBe(0); + expect(menuTester.options().filter(option => option.getAttribute('aria-checked') === 'true').length).toBe(0); }); }); } @@ -448,7 +448,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps) => act(() => {jest.runAllTimers();}); let menu = menuTester.menu; - let options = menuTester.options; + let options = menuTester.options(); await menuTester.selectOption({option: options[2], menuSelectionMode: 'multiple'}); await menuTester.selectOption({option: options[1], menuSelectionMode: 'multiple'}); @@ -467,7 +467,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps) => act(() => {jest.runAllTimers();}); menu = menuTester.menu; - options = menuTester.options; + options = menuTester.options(); expect(options[1]).toHaveAttribute('aria-checked', 'true'); expect(options[2]).toHaveAttribute('aria-checked', 'true'); expect(options[2]).toHaveFocus(); @@ -482,7 +482,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps) => act(() => {jest.runAllTimers();}); let menu = menuTester.menu; - let options = menuTester.options; + let options = menuTester.options(); expect(options[0]).toHaveFocus(); await user.keyboard('[ArrowDown]'); @@ -504,7 +504,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps) => act(() => {jest.runAllTimers();}); menu = menuTester.menu; - options = menuTester.options; + options = menuTester.options(); expect(options[1]).toHaveAttribute('aria-checked', 'true'); expect(options[2]).toHaveAttribute('aria-checked', 'true'); expect(options[1]).toHaveFocus(); @@ -519,7 +519,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps) => act(() => {jest.runAllTimers();}); let menu = menuTester.menu; - let options = menuTester.options; + let options = menuTester.options(); expect(options[0]).toHaveFocus(); await user.keyboard('[ArrowDown]'); @@ -532,7 +532,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps) => act(() => {jest.runAllTimers();}); menu = menuTester.menu; - options = menuTester.options; + options = menuTester.options(); expect(options[0]).toHaveAttribute('aria-checked', 'false'); expect(options[1]).toHaveAttribute('aria-checked', 'true'); expect(options[2]).toHaveAttribute('aria-checked', 'false'); @@ -551,7 +551,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps) => let menu = menuTester.menu; expect(menu).toBeInTheDocument(); await user.keyboard('{/Enter}'); - expect(menuTester.options.filter(option => option.getAttribute('aria-checked') === 'true').length).toBe(0); + expect(menuTester.options().filter(option => option.getAttribute('aria-checked') === 'true').length).toBe(0); }); }); } @@ -636,7 +636,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps) => let submenu = submenuUtil.menu; expect(submenu).toBeInTheDocument(); - await submenuUtil.selectOption({option: submenuUtil.options.filter(item => item.getAttribute('aria-haspopup') == null)[0]}); + await submenuUtil.selectOption({option: submenuUtil.options().filter(item => item.getAttribute('aria-haspopup') == null)[0]}); // TODO: not ideal, this runAllTimers is only needed for RSPv3, not RAC or S2 act(() => {jest.runAllTimers();}); expect(menu).not.toBeInTheDocument(); @@ -669,7 +669,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps) => let nestedSubmenu = nestedSubmenuUtil.menu; expect(nestedSubmenu).toBeInTheDocument(); - await nestedSubmenuUtil.selectOption({option: nestedSubmenuUtil.options.filter(item => item.getAttribute('aria-haspopup') == null)[0]}); + await nestedSubmenuUtil.selectOption({option: nestedSubmenuUtil.options().filter(item => item.getAttribute('aria-haspopup') == null)[0]}); act(() => {jest.runAllTimers();}); expect(menu).not.toBeInTheDocument(); expect(submenu).not.toBeInTheDocument(); diff --git a/packages/react-aria-components/test/Menu.test.tsx b/packages/react-aria-components/test/Menu.test.tsx index a85c1b5db10..2b60f0c9e46 100644 --- a/packages/react-aria-components/test/Menu.test.tsx +++ b/packages/react-aria-components/test/Menu.test.tsx @@ -1074,7 +1074,7 @@ describe('Menu', () => { expect(submenuTriggers).toHaveLength(1); // Open the submenu - let submenuUtil = (await menuTester.openSubmenu({submenuTriggerText: 'Share…'}))!; + let submenuUtil = (await menuTester.openSubmenu({submenuTrigger: 'Share…'}))!; let submenu = submenuUtil.menu; expect(submenu).toBeInTheDocument(); diff --git a/packages/react-aria-components/test/Select.test.js b/packages/react-aria-components/test/Select.test.js index a0f5673ddb2..302d4519e69 100644 --- a/packages/react-aria-components/test/Select.test.js +++ b/packages/react-aria-components/test/Select.test.js @@ -353,4 +353,19 @@ describe('Select', () => { expect(onChangeSpy).toHaveBeenCalledTimes(2); expect(onChangeSpy).toHaveBeenLastCalledWith(null); }); + + it('select can select an option via keyboard', async function () { + let {getByTestId} = render( + + ); + + let wrapper = getByTestId('select'); + let selectTester = testUtilUser.createTester('Select', {root: wrapper, interactionType: 'keyboard'}); + let trigger = selectTester.trigger; + expect(trigger).toHaveTextContent('Select an item'); + expect(trigger).not.toHaveAttribute('data-pressed'); + + await selectTester.selectOption({option: 'Kangaroo'}); + expect(trigger).toHaveTextContent('Kangaroo'); + }); }); From 1dff5c1454b660c5a8846da190b75b9f4895b7a4 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Mon, 9 Dec 2024 15:59:29 -0800 Subject: [PATCH 13/19] updating copy per review --- packages/dev/docs/pages/react-aria/testing.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dev/docs/pages/react-aria/testing.mdx b/packages/dev/docs/pages/react-aria/testing.mdx index af22bf4390c..961307f9ecb 100644 --- a/packages/dev/docs/pages/react-aria/testing.mdx +++ b/packages/dev/docs/pages/react-aria/testing.mdx @@ -60,8 +60,8 @@ to be on React 18+ in order for these utilities to work. ### Setup Once installed, you can access the `User` that `@react-aria/test-utils` provides in your test file as shown below. This user only needs to be initialized once and accepts two options: `interactionType` and `advanceTimer`. `interactionType` will -initialize what mode of interaction (mouse, keyboard, or touch) will be used by default. This can be overridden at the pattern tester or interaction execution level if required. `advanceTimer` accepts a function that when called should advance timers (real or fake) -in the test by a given amount. This is required for certain interactions (e.g. long press) that some of the patterns support. +initialize what mode of interaction (mouse, keyboard, or touch) will be used by default. This can be overridden at the pattern tester or interaction execution level if required. `advanceTimer` accepts a function that when called should advance timers (fake timers) +or wait for the required time (real timers). This is required for certain interactions (e.g. long press) that some of the patterns support. Once the `User` is initialized, you can use its `createTester` method to initialize a specific ARIA pattern tester in your test cases. This gives you access to that pattern's specific utilities that you can then call within your test to query for specific subcomponents or simulate common interactions. `createTester` requires two arguments, the first being the name of the ARIA pattern tester you are creating and the second being a set of initialization options specific to that From 2f9000625cace84bbe3ecde24a1fd438e7d47bab Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 13 Dec 2024 14:40:35 -0800 Subject: [PATCH 14/19] feat: Next batch of aria utils (Listbox, Tabs, Tree) (#7505) * adding listbox test utils and clean up of other utils * check that util works in tests * add docs * test listbox util get options section scoping * tabs test utils * add tests for tab test utils * add docs for tabs testing * update jsdoc and adding version badge * pulling in tree utils from s2-treeview branch modified some of the utils for consistency, but otherwise kept most of it the same. Changes to be discussed * update docs and use the utils in the spectrum tests * making things more consistent * fix tests temporarily * fix keyboard navigation if row is disabled --- .../@react-aria/test-utils/src/gridlist.ts | 144 +++++--- .../@react-aria/test-utils/src/listbox.ts | 226 ++++++++++++ packages/@react-aria/test-utils/src/menu.ts | 6 +- packages/@react-aria/test-utils/src/select.ts | 7 +- packages/@react-aria/test-utils/src/table.ts | 52 +-- packages/@react-aria/test-utils/src/tabs.ts | 198 +++++++++++ packages/@react-aria/test-utils/src/tree.ts | 248 +++++++++++++ packages/@react-aria/test-utils/src/types.ts | 80 ++++- packages/@react-aria/test-utils/src/user.ts | 32 +- .../combobox/docs/ComboBox.mdx | 4 +- .../@react-spectrum/list/docs/ListView.mdx | 4 +- .../list/stories/ListViewActions.stories.tsx | 32 +- .../list/stories/ListViewDnDUtil.stories.tsx | 32 +- .../stories/ListViewSelection.stories.tsx | 32 +- .../@react-spectrum/listbox/docs/ListBox.mdx | 32 +- .../listbox/test/ListBox.test.js | 114 +++--- .../@react-spectrum/menu/docs/MenuTrigger.mdx | 4 +- .../@react-spectrum/picker/docs/Picker.mdx | 4 +- packages/@react-spectrum/tabs/docs/Tabs.mdx | 41 ++- .../@react-spectrum/tabs/test/Tabs.test.js | 43 ++- .../@react-spectrum/tree/docs/TreeView.mdx | 42 ++- .../tree/test/TreeView.test.tsx | 80 +++-- .../dev/docs/pages/react-aria/testing.mdx | 16 +- .../react-aria-components/docs/ComboBox.mdx | 4 +- .../react-aria-components/docs/GridList.mdx | 4 +- .../react-aria-components/docs/ListBox.mdx | 36 +- packages/react-aria-components/docs/Menu.mdx | 4 +- .../react-aria-components/docs/Select.mdx | 4 +- packages/react-aria-components/docs/Table.mdx | 4 +- packages/react-aria-components/docs/Tabs.mdx | 37 +- packages/react-aria-components/docs/Tree.mdx | 42 ++- .../stories/GridList.stories.tsx | 2 +- .../stories/ListBox.stories.tsx | 12 +- .../test/AriaTree.test-util.tsx | 230 ++++++++++++ .../test/ListBox.test.js | 97 ++++- .../react-aria-components/test/Tabs.test.js | 115 +++--- .../react-aria-components/test/Tree.test.tsx | 330 ++++++++++-------- 37 files changed, 1882 insertions(+), 512 deletions(-) create mode 100644 packages/@react-aria/test-utils/src/listbox.ts create mode 100644 packages/@react-aria/test-utils/src/tabs.ts create mode 100644 packages/@react-aria/test-utils/src/tree.ts create mode 100644 packages/react-aria-components/test/AriaTree.test-util.tsx diff --git a/packages/@react-aria/test-utils/src/gridlist.ts b/packages/@react-aria/test-utils/src/gridlist.ts index 8e0f8c9c133..2052131f678 100644 --- a/packages/@react-aria/test-utils/src/gridlist.ts +++ b/packages/@react-aria/test-utils/src/gridlist.ts @@ -11,38 +11,23 @@ */ import {act, within} from '@testing-library/react'; -import {GridListTesterOpts, UserOpts} from './types'; -import {pressElement} from './events'; +import {GridListTesterOpts, GridRowActionOpts, ToggleGridRowOpts, UserOpts} from './types'; +import {pressElement, triggerLongPress} from './events'; -// TODO: this is a bit inconsistent from combobox, perhaps should also take node or combobox should also have find row -interface GridListToggleRowOpts { - /** - * What interaction type to use when toggling the row selection. Defaults to the interaction type set on the tester. - */ - interactionType?: UserOpts['interactionType'], - /** - * The index, text, or node of the row to toggle selection for. - */ - row: number | string | HTMLElement -} - -interface GridListRowActionOpts extends GridListToggleRowOpts { - /** - * Whether or not the grid list needs a double click to trigger the row action. Depends on the grid list's implementation. - */ - needsDoubleClick?: boolean -} +interface GridListToggleRowOpts extends ToggleGridRowOpts {} +interface GridListRowActionOpts extends GridRowActionOpts {} export class GridListTester { private user; private _interactionType: UserOpts['interactionType']; + private _advanceTimer: UserOpts['advanceTimer']; private _gridlist: HTMLElement; - constructor(opts: GridListTesterOpts) { - let {root, user, interactionType} = opts; + let {root, user, interactionType, advanceTimer} = opts; this.user = user; this._interactionType = interactionType || 'mouse'; + this._advanceTimer = advanceTimer; this._gridlist = root; } @@ -53,32 +38,6 @@ export class GridListTester { this._interactionType = type; } - // TODO: support long press? This is also pretty much the same as table's toggleRowSelection so maybe can share - // For now, don't include long press, see if people need it or if we should just expose long press as a separate util if it isn't very common - // If the current way of passing in the user specified advance timers is ok, then I'd be find including long press - // Maybe also support an option to force the click to happen on a specific part of the element (checkbox or row). That way - // the user can test a specific type of interaction? - /** - * Toggles the selection for the specified gridlist row. Defaults to using the interaction type set on the gridlist tester. - */ - async toggleRowSelection(opts: GridListToggleRowOpts) { - let {row, interactionType = this._interactionType} = opts; - - if (typeof row === 'string' || typeof row === 'number') { - row = this.findRow({rowIndexOrText: row}); - } - - let rowCheckbox = within(row).queryByRole('checkbox'); - if (rowCheckbox) { - await pressElement(this.user, rowCheckbox, interactionType); - } else { - let cell = within(row).getAllByRole('gridcell')[0]; - await pressElement(this.user, cell, interactionType); - } - } - - // TODO: pretty much the same as table except it uses this.gridlist. Make common between the two by accepting an option for - // an element? /** * Returns a row matching the specified index or text content. */ @@ -97,6 +56,84 @@ export class GridListTester { return row; } + // TODO: RTL + private async keyboardNavigateToRow(opts: {row: HTMLElement}) { + let {row} = opts; + let rows = this.rows; + let targetIndex = rows.indexOf(row); + if (targetIndex === -1) { + throw new Error('Option provided is not in the gridlist'); + } + if (document.activeElement === this._gridlist) { + await this.user.keyboard('[ArrowDown]'); + } else if (this._gridlist.contains(document.activeElement) && document.activeElement!.getAttribute('role') !== 'row') { + do { + await this.user.keyboard('[ArrowLeft]'); + } while (document.activeElement!.getAttribute('role') !== 'row'); + } + let currIndex = rows.indexOf(document.activeElement as HTMLElement); + if (currIndex === -1) { + throw new Error('ActiveElement is not in the gridlist'); + } + let direction = targetIndex > currIndex ? 'down' : 'up'; + + for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) { + await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`); + } + }; + + /** + * Toggles the selection for the specified gridlist row. Defaults to using the interaction type set on the gridlist tester. + */ + async toggleRowSelection(opts: GridListToggleRowOpts) { + let { + row, + needsLongPress, + checkboxSelection = true, + interactionType = this._interactionType + } = opts; + + if (typeof row === 'string' || typeof row === 'number') { + row = this.findRow({rowIndexOrText: row}); + } + + if (!row) { + throw new Error('Target row not found in the gridlist.'); + } + + let rowCheckbox = within(row).queryByRole('checkbox'); + + // TODO: we early return here because the checkbox/row can't be keyboard navigated to if the row is disabled usually + // but we may to check for disabledBehavior (aka if the disable row gets skipped when keyboard navigating or not) + if (interactionType === 'keyboard' && (rowCheckbox?.getAttribute('disabled') === '' || row?.getAttribute('aria-disabled') === 'true')) { + return; + } + + // this would be better than the check to do nothing in events.ts + // also, it'd be good to be able to trigger selection on the row instead of having to go to the checkbox directly + if (interactionType === 'keyboard' && !checkboxSelection) { + await this.keyboardNavigateToRow({row}); + await this.user.keyboard('{Space}'); + return; + } + if (rowCheckbox && checkboxSelection) { + await pressElement(this.user, rowCheckbox, interactionType); + } else { + let cell = within(row).getAllByRole('gridcell')[0]; + if (needsLongPress && interactionType === 'touch') { + if (this._advanceTimer == null) { + throw new Error('No advanceTimers provided for long press.'); + } + + // Note that long press interactions with rows is strictly touch only for grid rows + await triggerLongPress({element: cell, advanceTimer: this._advanceTimer, pointerOpts: {pointerType: 'touch'}}); + + } else { + await pressElement(this.user, cell, interactionType); + } + } + } + // TODO: There is a more difficult use case where the row has/behaves as link, don't think we have a good way to determine that unless the // user specificlly tells us /** @@ -120,14 +157,21 @@ export class GridListTester { if (needsDoubleClick) { await this.user.dblClick(row); } else if (interactionType === 'keyboard') { - act(() => row.focus()); + if (row?.getAttribute('aria-disabled') === 'true') { + return; + } + + if (document.activeElement !== this._gridlist || !this._gridlist.contains(document.activeElement)) { + act(() => this._gridlist.focus()); + } + + await this.keyboardNavigateToRow({row}); await this.user.keyboard('[Enter]'); } else { await pressElement(this.user, row, interactionType); } } - // TODO: do we really need this getter? Theoretically the user already has the reference to the gridlist /** * Returns the gridlist. */ diff --git a/packages/@react-aria/test-utils/src/listbox.ts b/packages/@react-aria/test-utils/src/listbox.ts new file mode 100644 index 00000000000..8fbec312b7b --- /dev/null +++ b/packages/@react-aria/test-utils/src/listbox.ts @@ -0,0 +1,226 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {act, within} from '@testing-library/react'; +import {ListBoxTesterOpts, UserOpts} from './types'; +import {pressElement, triggerLongPress} from './events'; + +interface ListBoxToggleOptionOpts { + /** + * What interaction type to use when toggling selection for an option. Defaults to the interaction type set on the tester. + */ + interactionType?: UserOpts['interactionType'], + /** + * The index, text, or node of the option to toggle selection for. + */ + option: number | string | HTMLElement, + /** + * Whether the option should be triggered by Space or Enter in keyboard modality. + * @default 'Enter' + */ + keyboardActivation?: 'Space' | 'Enter', + /** + * Whether the option needs to be long pressed to be selected. Depends on the listbox's implementation. + */ + needsLongPress?: boolean +} + +interface ListBoxOptionActionOpts extends Omit { + /** + * Whether or not the option needs a double click to trigger the option action. Depends on the listbox's implementation. + */ + needsDoubleClick?: boolean +} + +export class ListBoxTester { + private user; + private _interactionType: UserOpts['interactionType']; + private _advanceTimer: UserOpts['advanceTimer']; + private _listbox: HTMLElement; + + constructor(opts: ListBoxTesterOpts) { + let {root, user, interactionType, advanceTimer} = opts; + this.user = user; + this._interactionType = interactionType || 'mouse'; + this._listbox = root; + this._advanceTimer = advanceTimer; + } + + /** + * Set the interaction type used by the listbox tester. + */ + setInteractionType(type: UserOpts['interactionType']) { + this._interactionType = type; + } + + // TODO: now that we have listbox, perhaps select can make use of this tester internally + /** + * Returns a option matching the specified index or text content. + */ + findOption(opts: {optionIndexOrText: number | string}): HTMLElement { + let { + optionIndexOrText + } = opts; + + let option; + let options = this.options(); + + if (typeof optionIndexOrText === 'number') { + option = options[optionIndexOrText]; + } else if (typeof optionIndexOrText === 'string') { + option = (within(this.listbox!).getByText(optionIndexOrText).closest('[role=option]'))! as HTMLElement; + } + + return option; + } + + // TODO: this is basically the same as menu except for the error message, refactor later so that they share + // TODO: this also doesn't support grid layout yet + private async keyboardNavigateToOption(opts: {option: HTMLElement}) { + let {option} = opts; + let options = this.options(); + let targetIndex = options.indexOf(option); + if (targetIndex === -1) { + throw new Error('Option provided is not in the listbox'); + } + + if (document.activeElement === this._listbox) { + await this.user.keyboard('[ArrowDown]'); + } + + // TODO: not sure about doing same while loop that exists in other implementations of keyboardNavigateToOption, + // feels like it could break easily + if (document.activeElement?.getAttribute('role') !== 'option') { + await act(async () => { + option.focus(); + }); + } + + let currIndex = options.indexOf(document.activeElement as HTMLElement); + if (currIndex === -1) { + throw new Error('ActiveElement is not in the listbox'); + } + let direction = targetIndex > currIndex ? 'down' : 'up'; + + for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) { + await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`); + } + }; + + /** + * Toggles the selection for the specified listbox option. Defaults to using the interaction type set on the listbox tester. + */ + async toggleOptionSelection(opts: ListBoxToggleOptionOpts) { + let {option, needsLongPress, keyboardActivation = 'Enter', interactionType = this._interactionType} = opts; + + if (typeof option === 'string' || typeof option === 'number') { + option = this.findOption({optionIndexOrText: option}); + } + + if (!option) { + throw new Error('Target option not found in the listbox.'); + } + + if (interactionType === 'keyboard') { + if (option?.getAttribute('aria-disabled') === 'true') { + return; + } + + if (document.activeElement !== this._listbox || !this._listbox.contains(document.activeElement)) { + act(() => this._listbox.focus()); + } + + await this.keyboardNavigateToOption({option}); + await this.user.keyboard(`[${keyboardActivation}]`); + } else { + if (needsLongPress && interactionType === 'touch') { + if (this._advanceTimer == null) { + throw new Error('No advanceTimers provided for long press.'); + } + + await triggerLongPress({element: option, advanceTimer: this._advanceTimer, pointerOpts: {pointerType: 'touch'}}); + } else { + await pressElement(this.user, option, interactionType); + } + } + } + + /** + * Triggers the action for the specified listbox option. Defaults to using the interaction type set on the listbox tester. + */ + async triggerOptionAction(opts: ListBoxOptionActionOpts) { + let { + option, + needsDoubleClick, + interactionType = this._interactionType + } = opts; + + if (typeof option === 'string' || typeof option === 'number') { + option = this.findOption({optionIndexOrText: option}); + } + + if (!option) { + throw new Error('Target option not found in the listbox.'); + } + + if (needsDoubleClick) { + await this.user.dblClick(option); + } else if (interactionType === 'keyboard') { + if (option?.getAttribute('aria-disabled') === 'true') { + return; + } + + if (document.activeElement !== this._listbox || !this._listbox.contains(document.activeElement)) { + act(() => this._listbox.focus()); + } + + await this.keyboardNavigateToOption({option}); + await this.user.keyboard('[Enter]'); + } else { + await pressElement(this.user, option, interactionType); + } + } + + /** + * Returns the listbox. + */ + get listbox(): HTMLElement { + return this._listbox; + } + + /** + * Returns the listbox options. Can be filtered to a subsection of the listbox if provided via `element`. + */ + options(opts: {element?: HTMLElement} = {}): HTMLElement[] { + let {element = this._listbox} = opts; + let options = []; + if (element) { + options = within(element).queryAllByRole('option'); + } + + return options; + } + + /** + * Returns the listbox's selected options if any. + */ + get selectedOptions(): HTMLElement[] { + return this.options().filter(row => row.getAttribute('aria-selected') === 'true'); + } + + /** + * Returns the listbox's sections if any. + */ + get sections(): HTMLElement[] { + return within(this._listbox).queryAllByRole('group'); + } +} diff --git a/packages/@react-aria/test-utils/src/menu.ts b/packages/@react-aria/test-utils/src/menu.ts index 1b8abe6273b..d63b6aca6eb 100644 --- a/packages/@react-aria/test-utils/src/menu.ts +++ b/packages/@react-aria/test-utils/src/menu.ts @@ -209,6 +209,10 @@ export class MenuTester { } if (interactionType === 'keyboard') { + if (option?.getAttribute('aria-disabled') === 'true') { + return; + } + if (document.activeElement !== menu || !menu.contains(document.activeElement)) { act(() => menu.focus()); } @@ -296,7 +300,7 @@ export class MenuTester { await this.user.keyboard('[ArrowDown]'); } let currIndex = options.indexOf(document.activeElement as HTMLElement); - if (targetIndex === -1) { + if (currIndex === -1) { throw new Error('ActiveElement is not in the menu'); } let direction = targetIndex > currIndex ? 'down' : 'up'; diff --git a/packages/@react-aria/test-utils/src/select.ts b/packages/@react-aria/test-utils/src/select.ts index bfe94678ef9..3eddaaa6583 100644 --- a/packages/@react-aria/test-utils/src/select.ts +++ b/packages/@react-aria/test-utils/src/select.ts @@ -141,7 +141,7 @@ export class SelectTester { await this.user.keyboard('[ArrowDown]'); } let currIndex = options.indexOf(document.activeElement as HTMLElement); - if (targetIndex === -1) { + if (currIndex === -1) { throw new Error('ActiveElement is not in the listbox'); } let direction = targetIndex > currIndex ? 'down' : 'up'; @@ -151,7 +151,6 @@ export class SelectTester { } }; - // TODO: update this so it also can take the option node instead of just text, might already have been added in Rob's PR /** * Selects the desired select option. Defaults to using the interaction type set on the select tester. If necessary, will open the select dropdown beforehand. * The desired option can be targeted via the option's node, the option's text, or the option's index. @@ -180,6 +179,10 @@ export class SelectTester { } if (interactionType === 'keyboard') { + if (option?.getAttribute('aria-disabled') === 'true') { + return; + } + if (document.activeElement !== listbox || !listbox.contains(document.activeElement)) { act(() => listbox.focus()); } diff --git a/packages/@react-aria/test-utils/src/table.ts b/packages/@react-aria/test-utils/src/table.ts index 2d704f3f12a..614f4f9c0b2 100644 --- a/packages/@react-aria/test-utils/src/table.ts +++ b/packages/@react-aria/test-utils/src/table.ts @@ -11,25 +11,10 @@ */ import {act, fireEvent, waitFor, within} from '@testing-library/react'; +import {GridRowActionOpts, TableTesterOpts, ToggleGridRowOpts, UserOpts} from './types'; import {pressElement, triggerLongPress} from './events'; -import {TableTesterOpts, UserOpts} from './types'; - -// TODO: this is a bit inconsistent from combobox, perhaps should also take node or combobox should also have find row -interface TableToggleRowOpts { - /** - * The index, text, or node of the row to toggle selection for. - */ - row: number | string | HTMLElement, - /** - * Whether the row needs to be long pressed to be selected. Depends on the table's implementation. - */ - needsLongPress?: boolean, - /** - * What interaction type to use when toggling the row selection. Defaults to the interaction type set on the tester. - */ - interactionType?: UserOpts['interactionType'] -} +interface TableToggleRowOpts extends ToggleGridRowOpts {} interface TableToggleSortOpts { /** * The index, text, or node of the column to toggle selection for. @@ -40,21 +25,7 @@ interface TableToggleSortOpts { */ interactionType?: UserOpts['interactionType'] } - -interface TableRowActionOpts { - /** - * The index, text, or node of the row to toggle selection for. - */ - row: number | string | HTMLElement, - /** - * What interaction type to use when triggering the row's action. Defaults to the interaction type set on the tester. - */ - interactionType?: UserOpts['interactionType'], - /** - * Whether or not the table needs a double click to trigger the row action. Depends on the table's implementation. - */ - needsDoubleClick?: boolean -} +interface TableRowActionOpts extends GridRowActionOpts {} export class TableTester { private user; @@ -84,6 +55,7 @@ export class TableTester { let { row, needsLongPress, + checkboxSelection = true, interactionType = this._interactionType } = opts; @@ -96,7 +68,16 @@ export class TableTester { } let rowCheckbox = within(row).queryByRole('checkbox'); - if (rowCheckbox) { + + if (interactionType === 'keyboard' && !checkboxSelection) { + // TODO: for now focus the row directly until I add keyboard navigation + await act(async () => { + row.focus(); + }); + await this.user.keyboard('{Space}'); + return; + } + if (rowCheckbox && checkboxSelection) { await pressElement(this.user, rowCheckbox, interactionType); } else { let cell = within(row).getAllByRole('gridcell')[0]; @@ -206,9 +187,7 @@ export class TableTester { await pressElement(this.user, columnheader, interactionType); } } - // TODO: should there be a util for triggering a row action? Perhaps there should be but it would rely on the user teling us the config of the - // table. Maybe we could rely on the user knowing to trigger a press/double click? We could have the user pass in "needsDoubleClick" - // It is also iffy if there is any row selected because then the table is in selectionMode and the below actions will simply toggle row selection + /** * Triggers the action for the specified table row. Defaults to using the interaction type set on the table tester. */ @@ -230,6 +209,7 @@ export class TableTester { if (needsDoubleClick) { await this.user.dblClick(row); } else if (interactionType === 'keyboard') { + // TODO: add keyboard navigation instead of focusing the row directly. Will need to consider if the focus in in the columns act(() => row.focus()); await this.user.keyboard('[Enter]'); } else { diff --git a/packages/@react-aria/test-utils/src/tabs.ts b/packages/@react-aria/test-utils/src/tabs.ts new file mode 100644 index 00000000000..8a940d4f1d3 --- /dev/null +++ b/packages/@react-aria/test-utils/src/tabs.ts @@ -0,0 +1,198 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {act, within} from '@testing-library/react'; +import {Direction, Orientation, TabsTesterOpts, UserOpts} from './types'; +import {pressElement} from './events'; + +interface TriggerTabOptions { + /** + * What interaction type to use when triggering a tab. Defaults to the interaction type set on the tester. + */ + interactionType?: UserOpts['interactionType'], + /** + * The index, text, or node of the tab to toggle selection for. + */ + tab: number | string | HTMLElement, + /** + * Whether the tab needs to be activated manually rather than on focus. + */ + manualActivation?: boolean +} + +export class TabsTester { + private user; + private _interactionType: UserOpts['interactionType']; + private _tablist: HTMLElement; + private _direction: Direction; + + constructor(opts: TabsTesterOpts) { + let {root, user, interactionType, direction} = opts; + this.user = user; + this._interactionType = interactionType || 'mouse'; + this._direction = direction || 'ltr'; + + this._tablist = root; + let tablist = within(root).queryAllByRole('tablist'); + if (tablist.length > 0) { + this._tablist = tablist[0]; + } + } + + /** + * Set the interaction type used by the tabs tester. + */ + setInteractionType(type: UserOpts['interactionType']) { + this._interactionType = type; + } + + // TODO: This is pretty similar across most the utils, refactor to make it generic? + /** + * Returns a tab matching the specified index or text content. + */ + findTab(opts: {tabIndexOrText: number | string}): HTMLElement { + let { + tabIndexOrText + } = opts; + + let tab; + let tabs = this.tabs; + if (typeof tabIndexOrText === 'number') { + tab = tabs[tabIndexOrText]; + } else if (typeof tabIndexOrText === 'string') { + tab = (within(this._tablist).getByText(tabIndexOrText).closest('[role=tab]'))! as HTMLElement; + } + + return tab; + } + + // TODO: also quite similar across more utils albeit with orientation, refactor to make generic + private async keyboardNavigateToTab(opts: {tab: HTMLElement, orientation?: Orientation}) { + let {tab, orientation = 'vertical'} = opts; + let tabs = this.tabs; + let targetIndex = tabs.indexOf(tab); + if (targetIndex === -1) { + throw new Error('Tab provided is not in the tablist'); + } + + if (!this._tablist.contains(document.activeElement)) { + let selectedTab = this.selectedTab; + if (selectedTab != null) { + act(() => selectedTab.focus()); + } else { + act(() => tabs.find(tab => !(tab.hasAttribute('disabled') || tab.getAttribute('aria-disabled') === 'true'))?.focus()); + } + } + + let currIndex = this.tabs.indexOf(document.activeElement as HTMLElement); + if (currIndex === -1) { + throw new Error('ActiveElement is not in the tablist'); + } + + let arrowUp = 'ArrowUp'; + let arrowDown = 'ArrowDown'; + if (orientation === 'horizontal') { + if (this._direction === 'ltr') { + arrowUp = 'ArrowLeft'; + arrowDown = 'ArrowRight'; + } else { + arrowUp = 'ArrowRight'; + arrowDown = 'ArrowLeft'; + } + } + + let movementDirection = targetIndex > currIndex ? 'down' : 'up'; + for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) { + await this.user.keyboard(`[${movementDirection === 'down' ? arrowDown : arrowUp}]`); + } + }; + + /** + * Triggers the specified tab. Defaults to using the interaction type set on the tabs tester. + */ + async triggerTab(opts: TriggerTabOptions) { + let { + tab, + interactionType = this._interactionType, + manualActivation + } = opts; + + if (typeof tab === 'string' || typeof tab === 'number') { + tab = this.findTab({tabIndexOrText: tab}); + } + + if (!tab) { + throw new Error('Target tab not found in the tablist.'); + } else if (tab.hasAttribute('disabled')) { + throw new Error('Target tab is disabled.'); + } + + if (interactionType === 'keyboard') { + if (document.activeElement !== this._tablist || !this._tablist.contains(document.activeElement)) { + act(() => this._tablist.focus()); + } + + let tabsOrientation = this._tablist.getAttribute('aria-orientation') || 'horizontal'; + await this.keyboardNavigateToTab({tab, orientation: tabsOrientation as Orientation}); + if (manualActivation) { + await this.user.keyboard('[Enter]'); + } + } else { + await pressElement(this.user, tab, interactionType); + } + } + + /** + * Returns the tablist. + */ + get tablist(): HTMLElement { + return this._tablist; + } + + /** + * Returns the tabpanels. + */ + get tabpanels(): HTMLElement[] { + let tabpanels = [] as HTMLElement[]; + for (let tab of this.tabs) { + let controlId = tab.getAttribute('aria-controls'); + let panel = controlId != null ? document.getElementById(controlId) : null; + if (panel != null) { + tabpanels.push(panel); + } + } + + return tabpanels; + } + + /** + * Returns the tabs in the tablist. + */ + get tabs(): HTMLElement[] { + return within(this.tablist).queryAllByRole('tab'); + } + + /** + * Returns the currently selected tab in the tablist if any. + */ + get selectedTab(): HTMLElement | null { + return this.tabs.find(tab => tab.getAttribute('aria-selected') === 'true') || null; + } + + /** + * Returns the currently active tabpanel if any. + */ + get activeTabpanel(): HTMLElement | null { + let activeTabpanelId = this.selectedTab?.getAttribute('aria-controls'); + return activeTabpanelId ? document.getElementById(activeTabpanelId) : null; + } +} diff --git a/packages/@react-aria/test-utils/src/tree.ts b/packages/@react-aria/test-utils/src/tree.ts new file mode 100644 index 00000000000..93e4ec65ebf --- /dev/null +++ b/packages/@react-aria/test-utils/src/tree.ts @@ -0,0 +1,248 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {act, within} from '@testing-library/react'; +import {BaseGridRowInteractionOpts, GridRowActionOpts, ToggleGridRowOpts, TreeTesterOpts, UserOpts} from './types'; +import {pressElement, triggerLongPress} from './events'; + +interface TreeToggleExpansionOpts extends BaseGridRowInteractionOpts {} +interface TreeToggleRowOpts extends ToggleGridRowOpts {} +interface TreeRowActionOpts extends GridRowActionOpts {} + +// TODO: this ended up being pretty much the same as gridlist, refactor so it extends from gridlist +export class TreeTester { + private user; + private _interactionType: UserOpts['interactionType']; + private _advanceTimer: UserOpts['advanceTimer']; + private _tree: HTMLElement; + + constructor(opts: TreeTesterOpts) { + let {root, user, interactionType, advanceTimer} = opts; + this.user = user; + this._interactionType = interactionType || 'mouse'; + this._advanceTimer = advanceTimer; + this._tree = root; + // TODO: should all helpers do this? + let tree = within(root).queryByRole('treegrid'); + if (root.getAttribute('role') !== 'treegrid' && tree) { + this._tree = tree; + } + } + + /** + * Set the interaction type used by the tree tester. + */ + setInteractionType(type: UserOpts['interactionType']) { + this._interactionType = type; + }; + + /** + * Returns a row matching the specified index or text content. + */ + findRow(opts: {rowIndexOrText: number | string}): HTMLElement { + let { + rowIndexOrText + } = opts; + + let row; + if (typeof rowIndexOrText === 'number') { + row = this.rows[rowIndexOrText]; + } else if (typeof rowIndexOrText === 'string') { + row = (within(this.tree!).getByText(rowIndexOrText).closest('[role=row]'))! as HTMLElement; + } + + return row; + } + + // TODO: RTL + private async keyboardNavigateToRow(opts: {row: HTMLElement}) { + let {row} = opts; + let rows = this.rows; + let targetIndex = rows.indexOf(row); + if (targetIndex === -1) { + throw new Error('Option provided is not in the tree'); + } + if (document.activeElement === this.tree) { + await this.user.keyboard('[ArrowDown]'); + } else if (this._tree.contains(document.activeElement) && document.activeElement!.getAttribute('role') !== 'row') { + do { + await this.user.keyboard('[ArrowLeft]'); + } while (document.activeElement!.getAttribute('role') !== 'row'); + } + let currIndex = rows.indexOf(document.activeElement as HTMLElement); + if (currIndex === -1) { + throw new Error('ActiveElement is not in the tree'); + } + let direction = targetIndex > currIndex ? 'down' : 'up'; + + for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) { + await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`); + } + }; + + /** + * Toggles the selection for the specified tree row. Defaults to using the interaction type set on the tree tester. + */ + async toggleRowSelection(opts: TreeToggleRowOpts) { + let { + row, + needsLongPress, + checkboxSelection = true, + interactionType = this._interactionType + } = opts; + + if (typeof row === 'string' || typeof row === 'number') { + row = this.findRow({rowIndexOrText: row}); + } + + if (!row) { + throw new Error('Target row not found in the tree.'); + } + + let rowCheckbox = within(row).queryByRole('checkbox'); + + // TODO: we early return here because the checkbox can't be keyboard navigated to if the row is disabled usually + // but we may to check for disabledBehavior (aka if the disable row gets skipped when keyboard navigating or not) + if (interactionType === 'keyboard' && (rowCheckbox?.getAttribute('disabled') === '' || row?.getAttribute('aria-disabled') === 'true')) { + return; + } + + // this would be better than the check to do nothing in events.ts + // also, it'd be good to be able to trigger selection on the row instead of having to go to the checkbox directly + if (interactionType === 'keyboard' && !checkboxSelection) { + await this.keyboardNavigateToRow({row}); + await this.user.keyboard('{Space}'); + return; + } + if (rowCheckbox && checkboxSelection) { + await pressElement(this.user, rowCheckbox, interactionType); + } else { + let cell = within(row).getAllByRole('gridcell')[0]; + if (needsLongPress && interactionType === 'touch') { + if (this._advanceTimer == null) { + throw new Error('No advanceTimers provided for long press.'); + } + + // Note that long press interactions with rows is strictly touch only for grid rows + await triggerLongPress({element: cell, advanceTimer: this._advanceTimer, pointerOpts: {pointerType: 'touch'}}); + } else { + await pressElement(this.user, cell, interactionType); + } + } + }; + + /** + * Toggles the expansion for the specified tree row. Defaults to using the interaction type set on the tree tester. + */ + async toggleRowExpansion(opts: TreeToggleExpansionOpts) { + let { + row, + interactionType = this._interactionType + } = opts; + if (!this.tree.contains(document.activeElement)) { + await act(async () => { + this.tree.focus(); + }); + } + + if (typeof row === 'string' || typeof row === 'number') { + row = this.findRow({rowIndexOrText: row}); + } + + if (!row) { + throw new Error('Target row not found in the tree.'); + } else if (row.getAttribute('aria-expanded') == null) { + throw new Error('Target row is not expandable.'); + } + + if (interactionType === 'mouse' || interactionType === 'touch') { + let rowExpander = within(row).getAllByRole('button')[0]; // what happens if the button is not first? how can we differentiate? + await pressElement(this.user, rowExpander, interactionType); + } else if (interactionType === 'keyboard') { + if (row?.getAttribute('aria-disabled') === 'true') { + return; + } + + await this.keyboardNavigateToRow({row}); + if (row.getAttribute('aria-expanded') === 'true') { + await this.user.keyboard('[ArrowLeft]'); + } else { + await this.user.keyboard('[ArrowRight]'); + } + } + }; + + /** + * Triggers the action for the specified tree row. Defaults to using the interaction type set on the tree tester. + */ + async triggerRowAction(opts: TreeRowActionOpts) { + let { + row, + needsDoubleClick, + interactionType = this._interactionType + } = opts; + + if (typeof row === 'string' || typeof row === 'number') { + row = this.findRow({rowIndexOrText: row}); + } + + if (!row) { + throw new Error('Target row not found in the tree.'); + } + + if (needsDoubleClick) { + await this.user.dblClick(row); + } else if (interactionType === 'keyboard') { + if (row?.getAttribute('aria-disabled') === 'true') { + return; + } + + if (document.activeElement !== this._tree || !this._tree.contains(document.activeElement)) { + act(() => this._tree.focus()); + } + + await this.keyboardNavigateToRow({row}); + await this.user.keyboard('[Enter]'); + } else { + await pressElement(this.user, row, interactionType); + } + }; + + /** + * Returns the tree. + */ + get tree(): HTMLElement { + return this._tree; + } + + /** + * Returns the tree's rows if any. + */ + get rows(): HTMLElement[] { + return within(this?.tree).queryAllByRole('row'); + } + + /** + * Returns the tree's selected rows if any. + */ + get selectedRows(): HTMLElement[] { + return this.rows.filter(row => row.getAttribute('aria-selected') === 'true'); + } + + /** + * Returns the tree's cells if any. Can be filtered against a specific row if provided via `element`. + */ + cells(opts: {element?: HTMLElement} = {}): HTMLElement[] { + let {element = this.tree} = opts; + return within(element).queryAllByRole('gridcell'); + } +} diff --git a/packages/@react-aria/test-utils/src/types.ts b/packages/@react-aria/test-utils/src/types.ts index 048db0a33ee..d9c7f89bbda 100644 --- a/packages/@react-aria/test-utils/src/types.ts +++ b/packages/@react-aria/test-utils/src/types.ts @@ -10,6 +10,9 @@ * governing permissions and limitations under the License. */ +export type Orientation = 'horizontal' | 'vertical'; +export type Direction = 'ltr' | 'rtl'; + // https://github.com/testing-library/dom-testing-library/issues/939#issuecomment-830771708 is an interesting way of allowing users to configure the timers // curent way is like https://testing-library.com/docs/user-event/options/#advancetimers, export interface UserOpts { @@ -29,14 +32,14 @@ export interface UserOpts { advanceTimer?: (time?: number) => void | Promise } -export interface BaseTesterOpts { +export interface BaseTesterOpts extends UserOpts { + /** @private */ + user: any, /** The base element for the given tester (e.g. the table, menu trigger button, etc). */ root: HTMLElement } -export interface ComboBoxTesterOpts extends UserOpts, BaseTesterOpts { - /** @private */ - user: any, +export interface ComboBoxTesterOpts extends BaseTesterOpts { /** * The base element for the combobox. If provided the wrapping element around the target combobox (as is the the case with a ref provided to RSP ComboBox), * will automatically search for the combobox element within. @@ -49,14 +52,16 @@ export interface ComboBoxTesterOpts extends UserOpts, BaseTesterOpts { trigger?: HTMLElement } -export interface GridListTesterOpts extends UserOpts, BaseTesterOpts { - /** @private */ - user: any +export interface GridListTesterOpts extends BaseTesterOpts {} + +export interface ListBoxTesterOpts extends BaseTesterOpts { + /** + * A function used by the test utils to advance timers during interactions. + */ + advanceTimer?: UserOpts['advanceTimer'] } -export interface MenuTesterOpts extends UserOpts, BaseTesterOpts { - /** @private */ - user: any, +export interface MenuTesterOpts extends BaseTesterOpts { /** * The trigger element for the menu. */ @@ -67,9 +72,7 @@ export interface MenuTesterOpts extends UserOpts, BaseTesterOpts { isSubmenu?: boolean } -export interface SelectTesterOpts extends UserOpts, BaseTesterOpts { - /** @private */ - user: any, +export interface SelectTesterOpts extends BaseTesterOpts { /** * The trigger element for the select. If provided the wrapping element around the target select (as is the case with a ref provided to RSP Select), * will automatically search for the select's trigger element within. @@ -77,11 +80,54 @@ export interface SelectTesterOpts extends UserOpts, BaseTesterOpts { root: HTMLElement } -export interface TableTesterOpts extends UserOpts, BaseTesterOpts { - /** @private */ - user: any, +export interface TableTesterOpts extends BaseTesterOpts { /** * A function used by the test utils to advance timers during interactions. */ - advanceTimer: UserOpts['advanceTimer'] + advanceTimer?: UserOpts['advanceTimer'] +} + +export interface TabsTesterOpts extends BaseTesterOpts { + /** + * The horizontal layout direction, typically affected by locale. + * @default 'ltr' + */ + direction?: Direction +} + +export interface TreeTesterOpts extends BaseTesterOpts { + /** + * A function used by the test utils to advance timers during interactions. + */ + advanceTimer?: UserOpts['advanceTimer'] +} + +export interface BaseGridRowInteractionOpts { + /** + * The index, text, or node of the row to target. + */ + row: number | string | HTMLElement, + /** + * What interaction type to use when interacting with the row. Defaults to the interaction type set on the tester. + */ + interactionType?: UserOpts['interactionType'] +} + +export interface ToggleGridRowOpts extends BaseGridRowInteractionOpts { + /** + * Whether the row needs to be long pressed to be selected. Depends on the components implementation. + */ + needsLongPress?: boolean, + /** + * Whether the checkbox should be used to select the row. If false, will attempt to select the row via press. + * @default 'true' + */ + checkboxSelection?: boolean +} + +export interface GridRowActionOpts extends BaseGridRowInteractionOpts { + /** + * Whether or not the row needs a double click to trigger the row action. Depends on the components implementation. + */ + needsDoubleClick?: boolean } diff --git a/packages/@react-aria/test-utils/src/user.ts b/packages/@react-aria/test-utils/src/user.ts index 6f515da7ece..09b40fdde1a 100644 --- a/packages/@react-aria/test-utils/src/user.ts +++ b/packages/@react-aria/test-utils/src/user.ts @@ -11,32 +11,60 @@ */ import {ComboBoxTester} from './combobox'; -import {ComboBoxTesterOpts, GridListTesterOpts, MenuTesterOpts, SelectTesterOpts, TableTesterOpts, UserOpts} from './types'; +import { + ComboBoxTesterOpts, + GridListTesterOpts, + ListBoxTesterOpts, + MenuTesterOpts, + SelectTesterOpts, + TableTesterOpts, + TabsTesterOpts, + TreeTesterOpts, + UserOpts +} from './types'; import {GridListTester} from './gridlist'; +import {ListBoxTester} from './listbox'; import {MenuTester} from './menu'; import {pointerMap} from './'; import {SelectTester} from './select'; import {TableTester} from './table'; +import {TabsTester} from './tabs'; +import {TreeTester} from './tree'; import userEvent from '@testing-library/user-event'; -let keyToUtil = {'Select': SelectTester, 'Table': TableTester, 'Menu': MenuTester, 'ComboBox': ComboBoxTester, 'GridList': GridListTester} as const; +let keyToUtil = { + 'Select': SelectTester, + 'Table': TableTester, + 'Menu': MenuTester, + 'ComboBox': ComboBoxTester, + 'GridList': GridListTester, + 'ListBox': ListBoxTester, + 'Tabs': TabsTester, + 'Tree': TreeTester +} as const; export type PatternNames = keyof typeof keyToUtil; // Conditional type: https://www.typescriptlang.org/docs/handbook/2/conditional-types.html type Tester = T extends 'ComboBox' ? ComboBoxTester : T extends 'GridList' ? GridListTester : + T extends 'ListBox' ? ListBoxTester : T extends 'Menu' ? MenuTester : T extends 'Select' ? SelectTester : T extends 'Table' ? TableTester : + T extends 'Tabs' ? TabsTester : + T extends 'Tree' ? TreeTester : never; type TesterOpts = T extends 'ComboBox' ? ComboBoxTesterOpts : T extends 'GridList' ? GridListTesterOpts : + T extends 'ListBox' ? ListBoxTesterOpts : T extends 'Menu' ? MenuTesterOpts : T extends 'Select' ? SelectTesterOpts : T extends 'Table' ? TableTesterOpts : + T extends 'Tabs' ? TabsTesterOpts : + T extends 'Tree' ? TreeTesterOpts : never; let defaultAdvanceTimer = async (waitTime: number | undefined) => await new Promise((resolve) => setTimeout(resolve, waitTime)); diff --git a/packages/@react-spectrum/combobox/docs/ComboBox.mdx b/packages/@react-spectrum/combobox/docs/ComboBox.mdx index b94765dae25..063a1b4dc42 100644 --- a/packages/@react-spectrum/combobox/docs/ComboBox.mdx +++ b/packages/@react-spectrum/combobox/docs/ComboBox.mdx @@ -13,7 +13,7 @@ export default Layout; import docs from 'docs:@react-spectrum/combobox'; import comboboxUtils from 'docs:@react-aria/test-utils/src/combobox.ts'; import packageData from '@react-spectrum/combobox/package.json'; -import {HeaderInfo, PropTable, PageDescription, ClassAPI} from '@react-spectrum/docs'; +import {HeaderInfo, PropTable, PageDescription, ClassAPI, VersionBadge} from '@react-spectrum/docs'; ```jsx import import Add from '@spectrum-icons/workflow/Add'; @@ -994,6 +994,8 @@ behaviors in your test suite. Please also refer to [React Spectrum's test suite](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/combobox/test/ComboBox.test.js) if you find that the above isn't sufficient when resolving issues in your own test cases. +### Test utils + `@react-aria/test-utils` also offers common combobox interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the combobox tester and a sample of how you could use it in your test suite. diff --git a/packages/@react-spectrum/list/docs/ListView.mdx b/packages/@react-spectrum/list/docs/ListView.mdx index a05abb793a3..bac1dc9fc48 100644 --- a/packages/@react-spectrum/list/docs/ListView.mdx +++ b/packages/@react-spectrum/list/docs/ListView.mdx @@ -15,7 +15,7 @@ import ChevronRight from '@spectrum-icons/workflow/ChevronRight'; import docs from 'docs:@react-spectrum/list'; import dndDocs from 'docs:@react-spectrum/dnd'; import gridlistUtil from 'docs:@react-aria/test-utils/src/gridlist.ts'; -import {HeaderInfo, PropTable, PageDescription, TypeLink, ClassAPI} from '@react-spectrum/docs'; +import {HeaderInfo, PropTable, PageDescription, TypeLink, ClassAPI, VersionBadge} from '@react-spectrum/docs'; import {Keyboard} from '@react-spectrum/text'; import packageData from '@react-spectrum/list/package.json'; @@ -1193,6 +1193,8 @@ behaviors in your test suite. Please also refer to [React Spectrum's test suite](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/list/test/ListView.test.js) if you find that the above isn't sufficient when resolving issues in your own test cases. +### Test utils + `@react-aria/test-utils` also offers common gridlist interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the gridlist tester and a sample of how you could use it in your test suite. diff --git a/packages/@react-spectrum/list/stories/ListViewActions.stories.tsx b/packages/@react-spectrum/list/stories/ListViewActions.stories.tsx index 44c6fbfdc15..322d2538dd6 100644 --- a/packages/@react-spectrum/list/stories/ListViewActions.stories.tsx +++ b/packages/@react-spectrum/list/stories/ListViewActions.stories.tsx @@ -30,37 +30,27 @@ export default { }, argTypes: { selectionMode: { - control: { - type: 'radio', - options: ['none', 'single', 'multiple'] - } + control: 'radio', + options: ['none', 'single', 'multiple'] }, selectionStyle: { - control: { - type: 'radio', - options: ['checkbox', 'highlight'] - } + control: 'radio', + options: ['checkbox', 'highlight'] }, isQuiet: { - control: {type: 'boolean'} + control: 'boolean' }, density: { - control: { - type: 'select', - options: ['compact', 'regular', 'spacious'] - } + control: 'select', + options: ['compact', 'regular', 'spacious'] }, overflowMode: { - control: { - type: 'radio', - options: ['truncate', 'wrap'] - } + control: 'radio', + options: ['truncate', 'wrap'] }, disabledBehavior: { - control: { - type: 'radio', - options: ['selection', 'all'] - } + control: 'radio', + options: ['selection', 'all'] } } } as ComponentMeta; diff --git a/packages/@react-spectrum/list/stories/ListViewDnDUtil.stories.tsx b/packages/@react-spectrum/list/stories/ListViewDnDUtil.stories.tsx index 3be13f1b0cc..d2c56d02abe 100644 --- a/packages/@react-spectrum/list/stories/ListViewDnDUtil.stories.tsx +++ b/packages/@react-spectrum/list/stories/ListViewDnDUtil.stories.tsx @@ -19,37 +19,27 @@ export default { }, argTypes: { selectionMode: { - control: { - type: 'radio', - options: ['none', 'single', 'multiple'] - } + control: 'radio', + options: ['none', 'single', 'multiple'] }, selectionStyle: { - control: { - type: 'radio', - options: ['checkbox', 'highlight'] - } + control: 'radio', + options: ['checkbox', 'highlight'] }, isQuiet: { - control: {type: 'boolean'} + control: 'boolean' }, density: { - control: { - type: 'select', - options: ['compact', 'regular', 'spacious'] - } + control: 'select', + options: ['compact', 'regular', 'spacious'] }, overflowMode: { - control: { - type: 'radio', - options: ['truncate', 'wrap'] - } + control: 'radio', + options: ['truncate', 'wrap'] }, disabledBehavior: { - control: { - type: 'radio', - options: ['selection', 'all'] - } + control: 'radio', + options: ['selection', 'all'] } } } as ComponentMeta; diff --git a/packages/@react-spectrum/list/stories/ListViewSelection.stories.tsx b/packages/@react-spectrum/list/stories/ListViewSelection.stories.tsx index 3f2829374b0..bd8dd8cb558 100644 --- a/packages/@react-spectrum/list/stories/ListViewSelection.stories.tsx +++ b/packages/@react-spectrum/list/stories/ListViewSelection.stories.tsx @@ -26,37 +26,27 @@ export default { }, argTypes: { selectionMode: { - control: { - type: 'radio', - options: ['none', 'single', 'multiple'] - } + control: 'radio', + options: ['none', 'single', 'multiple'] }, selectionStyle: { - control: { - type: 'radio', - options: ['checkbox', 'highlight'] - } + control: 'radio', + options: ['checkbox', 'highlight'] }, isQuiet: { - control: {type: 'boolean'} + control: 'boolean' }, density: { - control: { - type: 'select', - options: ['compact', 'regular', 'spacious'] - } + control: 'select', + options: ['compact', 'regular', 'spacious'] }, overflowMode: { - control: { - type: 'radio', - options: ['truncate', 'wrap'] - } + control: 'radio', + options: ['truncate', 'wrap'] }, disabledBehavior: { - control: { - type: 'radio', - options: ['selection', 'all'] - } + control: 'radio', + options: ['selection', 'all'] } } } as ComponentMeta; diff --git a/packages/@react-spectrum/listbox/docs/ListBox.mdx b/packages/@react-spectrum/listbox/docs/ListBox.mdx index bcbe38cbf0e..c71320486e0 100644 --- a/packages/@react-spectrum/listbox/docs/ListBox.mdx +++ b/packages/@react-spectrum/listbox/docs/ListBox.mdx @@ -11,8 +11,9 @@ import {Layout} from '@react-spectrum/docs'; export default Layout; import docs from 'docs:@react-spectrum/listbox'; -import {HeaderInfo, PropTable, PageDescription} from '@react-spectrum/docs'; +import {HeaderInfo, PropTable, PageDescription, ClassAPI, VersionBadge} from '@react-spectrum/docs'; import packageData from '@react-spectrum/listbox/package.json'; +import listboxUtils from 'docs:@react-aria/test-utils/src/listbox.ts'; ```jsx import import {ListBox, Section, Item} from '@react-spectrum/listbox'; @@ -409,3 +410,32 @@ Please see the following sections in the testing docs for more information on ho Please also refer to [React Spectrum's test suite](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/listbox/test/ListBox.test.js) if you find that the above isn't sufficient when resolving issues in your own test cases. + +### Test utils + +`@react-aria/test-utils` also offers common listbox interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +in your tests. Below is the full definition of the listbox tester and a sample of how you could use it in your test suite. + + + +```ts +// ListBox.test.ts +import {User} from '@react-aria/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse'}); +// ... + +it('ListBox can select an option via keyboard', async function () { + // Render your test component/app and initialize the listbox tester + let {getByTestId} = render( + + ... + + ); + let listboxTester = testUtilUser.createTester('ListBox', {root: getByTestId('test-listbox'), interactionType: 'keyboard'}); + + await listboxTester.toggleOptionSelection({option: 4}); + expect(listboxTester.options()[4]).toHaveAttribute('aria-selected', 'true'); + expect(onSelectionChange).toBeCalledTimes(1); +}); +``` diff --git a/packages/@react-spectrum/listbox/test/ListBox.test.js b/packages/@react-spectrum/listbox/test/ListBox.test.js index 2a7c07ca6bb..56658103c55 100644 --- a/packages/@react-spectrum/listbox/test/ListBox.test.js +++ b/packages/@react-spectrum/listbox/test/ListBox.test.js @@ -19,6 +19,7 @@ import React from 'react'; import {Text} from '@react-spectrum/text'; import {theme} from '@react-spectrum/theme-default'; import {useAsyncList} from '@react-stately/data'; +import {User} from '@react-aria/test-utils'; import userEvent from '@testing-library/user-event'; let withSection = [ @@ -69,6 +70,7 @@ function renderComponent(props) { describe('ListBox', function () { let offsetWidth, offsetHeight, scrollHeight; let onSelectionChange = jest.fn(); + let testUtilUser = new User(); beforeAll(function () { offsetWidth = jest.spyOn(window.HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(() => 1000); @@ -89,11 +91,12 @@ describe('ListBox', function () { it('renders properly', function () { let tree = renderComponent(); - let listbox = tree.getByRole('listbox'); + let listboxTester = testUtilUser.createTester('ListBox', {root: tree.getByRole('listbox')}); + let listbox = listboxTester.listbox; expect(listbox).toBeTruthy(); expect(listbox).toHaveAttribute('aria-labelledby', 'label'); - let sections = within(listbox).getAllByRole('group'); + let sections = listboxTester.sections; expect(sections.length).toBe(withSection.length); for (let section of sections) { @@ -110,28 +113,22 @@ describe('ListBox', function () { } } - let items = within(listbox).getAllByRole('option'); - expect(items.length).toBe(withSection.reduce((acc, curr) => (acc + curr.children.length), 0)); + let options = listboxTester.options(); + expect(options.length).toBe(withSection.reduce((acc, curr) => (acc + curr.children.length), 0)); let i = 1; - for (let item of items) { - expect(item).toHaveAttribute('tabindex'); - expect(item).not.toHaveAttribute('aria-selected'); - expect(item).not.toHaveAttribute('aria-disabled'); - expect(item).toHaveAttribute('aria-posinset', '' + i++); - expect(item).toHaveAttribute('aria-setsize'); + for (let option of options) { + expect(option).toHaveAttribute('tabindex'); + expect(option).not.toHaveAttribute('aria-selected'); + expect(option).not.toHaveAttribute('aria-disabled'); + expect(option).toHaveAttribute('aria-posinset', '' + i++); + expect(option).toHaveAttribute('aria-setsize'); } - let item1 = within(listbox).getByText('Foo'); - let item2 = within(listbox).getByText('Bar'); - let item3 = within(listbox).getByText('Baz'); - let item4 = within(listbox).getByText('Blah'); - let item5 = within(listbox).getByText('Bleh'); - - expect(item1).toBeTruthy(); - expect(item2).toBeTruthy(); - expect(item3).toBeTruthy(); - expect(item4).toBeTruthy(); - expect(item5).toBeTruthy(); - expect(item3).toBeTruthy(); + + expect(listboxTester.findOption({optionIndexOrText: 'Foo'})).toBeTruthy(); + expect(listboxTester.findOption({optionIndexOrText: 'Bar'})).toBeTruthy(); + expect(listboxTester.findOption({optionIndexOrText: 'Baz'})).toBeTruthy(); + expect(listboxTester.findOption({optionIndexOrText: 'Blah'})).toBeTruthy(); + expect(listboxTester.findOption({optionIndexOrText: 'Bleh'})).toBeTruthy(); }); it('renders with falsy id', function () { @@ -190,30 +187,32 @@ describe('ListBox', function () { }); describe('supports single selection', function () { - it('supports defaultSelectedKeys (uncontrolled)', function () { - // Check that correct menu item is selected by default + it('supports defaultSelectedKeys (uncontrolled)', async function () { + // Check that correct listbox item is selected by default let tree = renderComponent({onSelectionChange, defaultSelectedKeys: ['Blah'], autoFocus: 'first', selectionMode: 'single'}); - let listbox = tree.getByRole('listbox'); - let options = within(listbox).getAllByRole('option'); - let selectedItem = options[3]; - expect(selectedItem).toBe(document.activeElement); - expect(selectedItem).toHaveAttribute('aria-selected', 'true'); - expect(selectedItem).toHaveAttribute('tabindex', '0'); - let itemText = within(selectedItem).getByText('Blah'); + let listboxTester = testUtilUser.createTester('ListBox', {root: tree.getByRole('listbox')}); + + let selectedOptions = listboxTester.selectedOptions; + expect(selectedOptions).toHaveLength(1); + expect(selectedOptions[0]).toBe(document.activeElement); + expect(selectedOptions[0]).toHaveAttribute('aria-selected', 'true'); + expect(selectedOptions[0]).toHaveAttribute('tabindex', '0'); + let itemText = within(selectedOptions[0]).getByText('Blah'); expect(itemText).toBeTruthy(); - let checkmark = within(selectedItem).getByRole('img', {hidden: true}); + let checkmark = within(selectedOptions[0]).getByRole('img', {hidden: true}); expect(checkmark).toBeTruthy(); - // Select a different menu item via enter - let nextSelectedItem = options[4]; - fireEvent.keyDown(nextSelectedItem, {key: 'Enter', code: 13, charCode: 13}); - expect(nextSelectedItem).toHaveAttribute('aria-selected', 'true'); - itemText = within(nextSelectedItem).getByText('Bleh'); + // Select a different listbox item via enter + await listboxTester.toggleOptionSelection({option: 4, interactionType: 'keyboard'}); + selectedOptions = listboxTester.selectedOptions; + expect(selectedOptions[0]).toHaveAttribute('aria-selected', 'true'); + itemText = within(selectedOptions[0]).getByText('Bleh'); expect(itemText).toBeTruthy(); - checkmark = within(nextSelectedItem).getByRole('img', {hidden: true}); + checkmark = within(selectedOptions[0]).getByRole('img', {hidden: true}); expect(checkmark).toBeTruthy(); + expect(selectedOptions).toHaveLength(1); - // Make sure there is only a single checkmark in the entire menu + // Make sure there is only a single checkmark in the entire listbox let checkmarks = tree.getAllByRole('img', {hidden: true}); expect(checkmarks.length).toBe(1); @@ -251,16 +250,15 @@ describe('ListBox', function () { expect(onSelectionChange.mock.calls[0][0].has('Bleh')).toBeTruthy(); }); - it('supports using space key to change item selection', function () { + it('supports using space key to change item selection', async function () { let tree = renderComponent({onSelectionChange, selectionMode: 'single'}); - let listbox = tree.getByRole('listbox'); - let options = within(listbox).getAllByRole('option'); + let listboxTester = testUtilUser.createTester('ListBox', {root: tree.getByRole('listbox')}); // Trigger a menu item via space - let item = options[4]; - fireEvent.keyDown(item, {key: ' ', code: 32, charCode: 32}); - expect(item).toHaveAttribute('aria-selected', 'true'); - let checkmark = within(item).getByRole('img', {hidden: true}); + let options = listboxTester.options(); + await listboxTester.toggleOptionSelection({option: 4, keyboardActivation: 'Space', interactionType: 'keyboard'}); + expect(options[4]).toHaveAttribute('aria-selected', 'true'); + let checkmark = within(options[4]).getByRole('img', {hidden: true}); expect(checkmark).toBeTruthy(); // Make sure there is only a single checkmark in the entire menu @@ -762,16 +760,16 @@ describe('ListBox', function () { it('should handle when an item changes sections', function () { let sections = [ { - id: 'foo', - title: 'Foo', + id: 'sect1', + title: 'Section 1', children: [ {id: 'foo-1', title: 'Foo 1'}, {id: 'foo-2', title: 'Foo 2'} ] }, { - id: 'bar', - title: 'Bar', + id: 'sect2', + title: 'Section 2', children: [ {id: 'bar-1', title: 'Bar 1'}, {id: 'bar-2', title: 'Bar 2'} @@ -793,9 +791,12 @@ describe('ListBox', function () { ); } - let {getByText, rerender} = render(); - let item = getByText('Foo 1'); - expect(document.getElementById(item.closest('[role=group]').getAttribute('aria-labelledby'))).toHaveTextContent('Foo'); + let {rerender, getByRole, getByLabelText} = render(); + let listboxTester = testUtilUser.createTester('ListBox', {root: getByRole('listbox')}); + let item = listboxTester.findOption({optionIndexOrText: 'Foo 1'}); + let listboxSections = listboxTester.sections; + expect(listboxTester.options({element: listboxSections[0]})).toContain(item); + expect(listboxSections[0]).toBe(getByLabelText('Section 1')); let sections2 = [ { @@ -809,8 +810,11 @@ describe('ListBox', function () { ]; rerender(); - item = getByText('Foo 1'); - expect(document.getElementById(item.closest('[role=group]').getAttribute('aria-labelledby'))).toHaveTextContent('Bar'); + listboxTester = testUtilUser.createTester('ListBox', {root: getByRole('listbox')}); + item = listboxTester.findOption({optionIndexOrText: 'Foo 1'}); + listboxSections = listboxTester.sections; + expect(listboxTester.options({element: listboxSections[1]})).toContain(item); + expect(listboxSections[1]).toBe(getByLabelText('Section 2')); }); describe('async loading', function () { diff --git a/packages/@react-spectrum/menu/docs/MenuTrigger.mdx b/packages/@react-spectrum/menu/docs/MenuTrigger.mdx index 347d289d7fd..ff48dfc9ffc 100644 --- a/packages/@react-spectrum/menu/docs/MenuTrigger.mdx +++ b/packages/@react-spectrum/menu/docs/MenuTrigger.mdx @@ -12,7 +12,7 @@ export default Layout; import docs from 'docs:@react-spectrum/menu'; import menuUtil from 'docs:@react-aria/test-utils/src/menu.ts'; -import {HeaderInfo, PropTable, PageDescription, ClassAPI} from '@react-spectrum/docs'; +import {HeaderInfo, PropTable, PageDescription, ClassAPI, VersionBadge} from '@react-spectrum/docs'; import packageData from '@react-spectrum/menu/package.json'; import {Keyboard} from '@react-spectrum/text'; @@ -258,6 +258,8 @@ behaviors in your test suite. Please also refer to [React Spectrum's test suite](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/menu/test/MenuTrigger.test.js) if you find that the above isn't sufficient when resolving issues in your own test cases. +### Test utils + `@react-aria/test-utils` also offers common menu interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the menu tester and a sample of how you could use it in your test suite. diff --git a/packages/@react-spectrum/picker/docs/Picker.mdx b/packages/@react-spectrum/picker/docs/Picker.mdx index 9646b62c200..94a633496de 100644 --- a/packages/@react-spectrum/picker/docs/Picker.mdx +++ b/packages/@react-spectrum/picker/docs/Picker.mdx @@ -12,7 +12,7 @@ export default Layout; import docs from 'docs:@react-spectrum/picker'; import selectUtil from 'docs:@react-aria/test-utils/src/select.ts'; -import {HeaderInfo, PropTable, PageDescription, ClassAPI} from '@react-spectrum/docs'; +import {HeaderInfo, PropTable, PageDescription, ClassAPI, VersionBadge} from '@react-spectrum/docs'; import packageData from '@react-spectrum/picker/package.json'; ```jsx import @@ -590,6 +590,8 @@ for more information on how to handle these behaviors in your test suite. Please also refer to [React Spectrum's test suite](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/picker/test/Picker.test.js) if you find that the above isn't sufficient when resolving issues in your own test cases. +### Test utils + `@react-aria/test-utils` also offers common select interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the select tester and a sample of how you could use it in your test suite. diff --git a/packages/@react-spectrum/tabs/docs/Tabs.mdx b/packages/@react-spectrum/tabs/docs/Tabs.mdx index 0028e535498..a89ae81377e 100644 --- a/packages/@react-spectrum/tabs/docs/Tabs.mdx +++ b/packages/@react-spectrum/tabs/docs/Tabs.mdx @@ -12,8 +12,9 @@ export default Layout; import docs from 'docs:@react-spectrum/tabs'; import utilsDocs from 'docs:@react-aria/utils'; -import {HeaderInfo, PropTable, PageDescription, TypeLink} from '@react-spectrum/docs'; +import {HeaderInfo, PropTable, PageDescription, TypeLink, ClassAPI, VersionBadge} from '@react-spectrum/docs'; import packageData from '@react-spectrum/tabs/package.json'; +import tabsUtils from 'docs:@react-aria/test-utils/src/tabs.ts'; ```jsx import import {ActionGroup} from '@react-spectrum/actiongroup'; @@ -629,3 +630,41 @@ function Example() { ``` + +## Testing + +### Test utils + +Tabs features automatic tab collapse behavior and may need specific mocks to test said behavior. Please also refer to +[React Spectrum's test suite](https://github.com/adobe/react-spectrum/blob/326f48154e301edab425c8198c5c3af72422462b/packages/%40react-spectrum/tabs/test/Tabs.test.js#L58-L62) if you +run into any issues with your tests. + +`@react-aria/test-utils` also offers common tabs interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +in your tests. Below is the full definition of the tabs tester and a sample of how you could use it in your test suite. + + + +```ts +// Tabs.test.ts +import {User} from '@react-aria/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse'}); +// ... + +it('Tabs can change selection via keyboard', async function () { + // Render your test component/app and initialize the listbox tester + let {getByTestId} = render( + + ... + + ); + let tabsTester = testUtilUser.createTester('Tabs', {root: getByTestId('test-tabs'), interactionType: 'keyboard'}); + + let tabs = tabsTester.tabs; + expect(tabsTester.selectedTab).toBe(tabs[0]); + + await tabsTester.triggerTab({tab: 1}); + expect(onSelectionChange).toBeCalledTimes(1); + expect(tabsTester.selectedTab).toBe(tabs[1]); +}); +``` diff --git a/packages/@react-spectrum/tabs/test/Tabs.test.js b/packages/@react-spectrum/tabs/test/Tabs.test.js index 7a411b22046..3178a266774 100644 --- a/packages/@react-spectrum/tabs/test/Tabs.test.js +++ b/packages/@react-spectrum/tabs/test/Tabs.test.js @@ -16,6 +16,7 @@ import {Links as LinksExample} from '../stories/Tabs.stories'; import {Provider} from '@react-spectrum/provider'; import React from 'react'; import {theme} from '@react-spectrum/theme-default'; +import {User} from '@react-aria/test-utils'; import userEvent from '@testing-library/user-event'; let defaultItems = [ @@ -25,9 +26,9 @@ let defaultItems = [ ]; function renderComponent(props = {}, itemProps) { - let {items = defaultItems} = props; + let {items = defaultItems, providerProps} = props; return render( - + {item => ( @@ -49,6 +50,7 @@ function renderComponent(props = {}, itemProps) { describe('Tabs', function () { let onSelectionChange = jest.fn(); let user; + let testUtilUser = new User(); beforeAll(function () { user = userEvent.setup({delay: null, pointerMap}); @@ -73,12 +75,13 @@ describe('Tabs', function () { it('renders properly', function () { let container = renderComponent(); - let tablist = container.getByRole('tablist'); - expect(tablist).toBeTruthy(); + let tabsTester = testUtilUser.createTester('Tabs', {root: container.getByRole('tablist')}); + let tablist = tabsTester.tablist; + expect(tablist).toBeTruthy(); expect(tablist).toHaveAttribute('aria-orientation', 'horizontal'); - let tabs = within(tablist).getAllByRole('tab'); + let tabs = tabsTester.tabs; expect(tabs.length).toBe(3); for (let tab of tabs) { @@ -86,12 +89,15 @@ describe('Tabs', function () { expect(tab).toHaveAttribute('aria-selected'); let isSelected = tab.getAttribute('aria-selected') === 'true'; if (isSelected) { + expect(tab).toBe(tabsTester.selectedTab); expect(tab).toHaveAttribute('aria-controls'); let tabpanel = document.getElementById(tab.getAttribute('aria-controls')); expect(tabpanel).toBeTruthy(); expect(tabpanel).toHaveAttribute('aria-labelledby', tab.id); expect(tabpanel).toHaveAttribute('role', 'tabpanel'); expect(tabpanel).toHaveTextContent(defaultItems[0].children); + expect(tabpanel).toBe(tabsTester.activeTabpanel); + expect(tabsTester.tabpanels).toHaveLength(1); } } }); @@ -134,6 +140,33 @@ describe('Tabs', function () { expect(arrowDown.defaultPrevented).toBe(false); }); + it('allows user to change tab item select via arrow keys with horizontal tabs (rtl)', async function () { + let onKeyDown = jest.fn(); + let container = renderComponent({orientation: 'horizontal', providerProps: {locale: 'ar-AE'}}); + let tabsTester = testUtilUser.createTester('Tabs', {root: container.getByRole('tablist'), interactionType: 'keyboard', direction: 'rtl'}); + let tabs = tabsTester.tabs; + window.addEventListener('keydown', onKeyDown); + + expect(tabs[0]).toHaveAttribute('aria-selected', 'true'); + + await tabsTester.triggerTab({tab: 1}); + expect(tabs[0]).not.toHaveAttribute('aria-selected', 'true'); + expect(tabs[1]).toHaveAttribute('aria-selected', 'true'); + // Just to double check that the util is actually pressing the expected arrow key + expect(onKeyDown.mock.calls[0][0].key).toBe('ArrowLeft'); + + await tabsTester.triggerTab({tab: 2}); + expect(tabs[1]).not.toHaveAttribute('aria-selected', 'true'); + expect(tabs[2]).toHaveAttribute('aria-selected', 'true'); + expect(onKeyDown.mock.calls[1][0].key).toBe('ArrowLeft'); + + await tabsTester.triggerTab({tab: 1}); + expect(tabs[2]).not.toHaveAttribute('aria-selected', 'true'); + expect(tabs[1]).toHaveAttribute('aria-selected', 'true'); + expect(onKeyDown.mock.calls[2][0].key).toBe('ArrowRight'); + window.removeEventListener('keydown', onKeyDown); + }); + it('allows user to change tab item select via arrow keys with vertical tabs', function () { let container = renderComponent({orientation: 'vertical'}); let tablist = container.getByRole('tablist'); diff --git a/packages/@react-spectrum/tree/docs/TreeView.mdx b/packages/@react-spectrum/tree/docs/TreeView.mdx index 06334ca4dc8..74024e4f7e7 100644 --- a/packages/@react-spectrum/tree/docs/TreeView.mdx +++ b/packages/@react-spectrum/tree/docs/TreeView.mdx @@ -11,7 +11,8 @@ import {Layout} from '@react-spectrum/docs'; export default Layout; import docs from 'docs:@react-spectrum/tree'; -import {HeaderInfo, PropTable, PageDescription, TypeLink, VersionBadge} from '@react-spectrum/docs'; +import treeUtils from 'docs:@react-aria/test-utils/src/tree.ts'; +import {HeaderInfo, PropTable, PageDescription, TypeLink, VersionBadge, ClassAPI} from '@react-spectrum/docs'; import {Keyboard} from '@react-spectrum/text'; import packageData from '@react-spectrum/tree/package.json'; import ChevronRight from '@spectrum-icons/workflow/ChevronRight'; @@ -219,7 +220,7 @@ function ControlledSelection() { aria-label="Example tree with controlled selection" defaultExpandedKeys={['projects', 'project-2']} /*- begin highlight -*/ - selectionMode="multiple" + selectionMode="multiple" selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} /*- end highlight -*/ @@ -456,3 +457,40 @@ behaviors in your test suite. Please also refer to [React Spectrum's test suite](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/tree/test/TreeView.test.js) if you find that the above isn't sufficient when resolving issues in your own test cases. + +### Test utils + +`@react-aria/test-utils` also offers common tree interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +in your tests. Below is the full definition of the tree tester and a sample of how you could use it in your test suite. + + + +```ts +// Tree.test.ts +import {User} from '@react-aria/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse'}); +// ... + +it('TreeView can select a row via keyboard', async function () { + // Render your test component/app and initialize the Tree tester + let {getByTestId} = render( + + ... + + ); + let treeTester = testUtilUser.createTester('Tree', {root: getByTestId('test-tree'), interactionType: 'keyboard'}); + + await treeTester.toggleRowSelection({row: 0}); + expect(treeTester.selectedRows).toHaveLength(1); + expect(within(treeTester.rows[0]).getByRole('checkbox')).toBeChecked(); + + await treeTester.toggleRowSelection({row: 1}); + expect(treeTester.selectedRows).toHaveLength(2); + expect(within(treeTester.rows[1]).getByRole('checkbox')).toBeChecked(); + + await treeTester.toggleRowSelection({row: 0}); + expect(treeTester.selectedRows).toHaveLength(1); + expect(within(treeTester.rows[0]).getByRole('checkbox')).not.toBeChecked(); +}); +``` diff --git a/packages/@react-spectrum/tree/test/TreeView.test.tsx b/packages/@react-spectrum/tree/test/TreeView.test.tsx index 7f518f060a5..0f1cef8f004 100644 --- a/packages/@react-spectrum/tree/test/TreeView.test.tsx +++ b/packages/@react-spectrum/tree/test/TreeView.test.tsx @@ -22,6 +22,7 @@ import {Provider} from '@react-spectrum/provider'; import React from 'react'; import {theme} from '@react-spectrum/theme-default'; import {TreeView, TreeViewItem} from '../'; +import {User} from '@react-aria/test-utils'; import userEvent from '@testing-library/user-event'; let onSelectionChange = jest.fn(); @@ -170,6 +171,7 @@ let DynamicTree = ({treeProps = {}, rowProps = {}}) => ( describe('Tree', () => { let user; + let testUtilUser = new User(); beforeAll(() => { user = userEvent.setup({delay: null, pointerMap}); @@ -320,8 +322,9 @@ describe('Tree', () => { }); it('should support dynamic trees', () => { - let {getAllByRole} = render(); - let rows = getAllByRole('row'); + let {getByRole} = render(); + let treeTester = testUtilUser.createTester('Tree', {user, root: getByRole('treegrid')}); + let rows = treeTester.rows; expect(rows).toHaveLength(20); // Check the rough structure to make sure dynamic rows are rendering as expected (just checks the expandable rows and their attributes) @@ -385,11 +388,12 @@ describe('Tree', () => { }); it('should not render checkboxes for selection with selectionStyle=highlight', async () => { - let {getByRole, getAllByRole} = render(); - let tree = getByRole('treegrid'); - expect(tree).toHaveAttribute('aria-multiselectable', 'true'); + let {getByRole} = render(); + let treeTester = testUtilUser.createTester('Tree', {user, root: getByRole('treegrid')}); + expect(treeTester.tree).toHaveAttribute('aria-multiselectable', 'true'); + let rows = treeTester.rows; - for (let row of getAllByRole('row')) { + for (let row of treeTester.rows) { let checkbox = within(row).queryByRole('checkbox'); expect(checkbox).toBeNull(); expect(row).toHaveAttribute('aria-selected', 'false'); @@ -397,21 +401,25 @@ describe('Tree', () => { expect(row).toHaveAttribute('data-selection-mode', 'multiple'); } - let row2 = getAllByRole('row')[2]; - await user.click(row2); + let row2 = rows[2]; + await treeTester.toggleRowSelection({row: 'Projects-1'}); expect(row2).toHaveAttribute('aria-selected', 'true'); expect(row2).toHaveAttribute('data-selected', 'true'); expect(onSelectionChange).toHaveBeenCalledTimes(1); expect(new Set(onSelectionChange.mock.calls[0][0])).toEqual(new Set(['Projects-1'])); + expect(treeTester.selectedRows).toHaveLength(1); + expect(treeTester.selectedRows[0]).toBe(row2); - let row1 = getAllByRole('row')[1]; - await user.click(row1); + let row1 = rows[1]; + await treeTester.toggleRowSelection({row: row1}); expect(row1).toHaveAttribute('aria-selected', 'true'); expect(row1).toHaveAttribute('data-selected', 'true'); expect(row2).toHaveAttribute('aria-selected', 'false'); expect(row2).not.toHaveAttribute('data-selected'); expect(onSelectionChange).toHaveBeenCalledTimes(2); expect(new Set(onSelectionChange.mock.calls[1][0])).toEqual(new Set(['Projects'])); + expect(treeTester.selectedRows).toHaveLength(1); + expect(treeTester.selectedRows[0]).toBe(row1); }); it('should render a chevron for an expandable row marked with hasChildRows', () => { @@ -585,28 +593,29 @@ describe('Tree', () => { }); it('should support actions on rows', async () => { - let {getAllByRole} = render(); + let {getByRole} = render(); + let treeTester = testUtilUser.createTester('Tree', {user, root: getByRole('treegrid')}); - let row = getAllByRole('row')[0]; - await user.click(row); + let rows = treeTester.rows; + await treeTester.triggerRowAction({row: rows[0]}); expect(onAction).toHaveBeenCalledTimes(1); expect(onAction).toHaveBeenLastCalledWith('Photos'); expect(onSelectionChange).toHaveBeenCalledTimes(0); // Due to disabledBehavior being set to 'all' this expandable row has its action disabled - let disabledRow = getAllByRole('row')[1]; + let disabledRow = rows[1]; expect(disabledRow).toHaveAttribute('data-disabled', 'true'); - await user.click(disabledRow); + await treeTester.triggerRowAction({row: disabledRow}); expect(onAction).toHaveBeenCalledTimes(1); expect(onSelectionChange).toHaveBeenCalledTimes(0); - let expandableRow = getAllByRole('row')[2]; - await user.click(expandableRow); + let expandableRow = rows[2]; + await treeTester.triggerRowAction({row: expandableRow}); expect(onAction).toHaveBeenCalledTimes(2); expect(onAction).toHaveBeenLastCalledWith('Projects-1'); expect(onSelectionChange).toHaveBeenCalledTimes(0); - await user.keyboard('{Enter}'); + await treeTester.triggerRowAction({row: expandableRow, interactionType: 'keyboard'}); expect(onAction).toHaveBeenCalledTimes(3); expect(onAction).toHaveBeenLastCalledWith('Projects-1'); expect(onSelectionChange).toHaveBeenCalledTimes(0); @@ -827,8 +836,9 @@ describe('Tree', () => { }; it('should expand/collapse a row when clicking/using Enter on the row itself and there arent any other primary actions', async () => { - let {getAllByRole} = render(); - let rows = getAllByRole('row'); + let {getByRole} = render(); + let treeTester = testUtilUser.createTester('Tree', {user, root: getByRole('treegrid')}); + let rows = treeTester.rows; expect(rows).toHaveLength(20); await user.tab(); @@ -842,7 +852,7 @@ describe('Tree', () => { expect(onExpandedChange).toHaveBeenCalledTimes(0); // Check we can open/close a top level row - await trigger(rows[0], 'Enter'); + await treeTester.toggleRowExpansion({row: rows[0], interactionType: type as 'mouse' | 'keyboard'}); expect(document.activeElement).toBe(rows[0]); expect(rows[0]).toHaveAttribute('aria-expanded', 'false'); expect(rows[0]).not.toHaveAttribute('data-expanded'); @@ -853,10 +863,10 @@ describe('Tree', () => { expect(onExpandedChange).toHaveBeenCalledTimes(1); // Note that the children of the parent row will still be in the "expanded" array expect(new Set(onExpandedChange.mock.calls[0][0])).toEqual(new Set(['Project-2', 'Project-5', 'Reports', 'Reports-1', 'Reports-1A', 'Reports-1AB'])); - rows = getAllByRole('row'); + rows = treeTester.rows; expect(rows).toHaveLength(9); - await trigger(rows[0], 'Enter'); + await treeTester.toggleRowExpansion({row: rows[0], interactionType: type as 'mouse' | 'keyboard'}); expect(document.activeElement).toBe(rows[0]); expect(rows[0]).toHaveAttribute('aria-expanded', 'true'); expect(rows[0]).toHaveAttribute('data-expanded', 'true'); @@ -866,7 +876,7 @@ describe('Tree', () => { expect(rows[0]).toHaveAttribute('data-has-child-rows', 'true'); expect(onExpandedChange).toHaveBeenCalledTimes(2); expect(new Set(onExpandedChange.mock.calls[1][0])).toEqual(new Set(['Projects', 'Project-2', 'Project-5', 'Reports', 'Reports-1', 'Reports-1A', 'Reports-1AB'])); - rows = getAllByRole('row'); + rows = treeTester.rows; expect(rows).toHaveLength(20); await user.keyboard('{ArrowDown}'); @@ -880,7 +890,7 @@ describe('Tree', () => { expect(rows[2]).toHaveAttribute('data-has-child-rows', 'true'); // Check we can close a nested row and it doesn't affect the parent - await trigger(rows[2], 'ArrowLeft'); + await treeTester.toggleRowExpansion({row: rows[2], interactionType: type as 'mouse' | 'keyboard'}); expect(document.activeElement).toBe(rows[2]); expect(rows[2]).toHaveAttribute('aria-expanded', 'false'); expect(rows[2]).not.toHaveAttribute('data-expanded'); @@ -896,25 +906,25 @@ describe('Tree', () => { expect(rows[0]).toHaveAttribute('data-has-child-rows', 'true'); expect(onExpandedChange).toHaveBeenCalledTimes(3); expect(new Set(onExpandedChange.mock.calls[2][0])).toEqual(new Set(['Projects', 'Project-5', 'Reports', 'Reports-1', 'Reports-1A', 'Reports-1AB'])); - rows = getAllByRole('row'); + rows = treeTester.rows; expect(rows).toHaveLength(17); // Check behavior of onExpandedChange when a nested row is already closed and the parent is collapsed await user.keyboard('{ArrowUp}'); await user.keyboard('{ArrowUp}'); - await trigger(rows[0], 'ArrowLeft'); + await treeTester.toggleRowExpansion({row: rows[0], interactionType: type as 'mouse' | 'keyboard'}); expect(document.activeElement).toBe(rows[0]); expect(onExpandedChange).toHaveBeenCalledTimes(4); expect(new Set(onExpandedChange.mock.calls[3][0])).toEqual(new Set(['Project-5', 'Reports', 'Reports-1', 'Reports-1A', 'Reports-1AB'])); - rows = getAllByRole('row'); + rows = treeTester.rows; expect(rows).toHaveLength(9); // Check that the nested collapsed row is still closed when the parent is reexpanded - await trigger(rows[0], 'ArrowRight'); + await treeTester.toggleRowExpansion({row: rows[0], interactionType: type as 'mouse' | 'keyboard'}); expect(document.activeElement).toBe(rows[0]); expect(onExpandedChange).toHaveBeenCalledTimes(5); expect(new Set(onExpandedChange.mock.calls[4][0])).toEqual(new Set(['Projects', 'Project-5', 'Reports', 'Reports-1', 'Reports-1A', 'Reports-1AB'])); - rows = getAllByRole('row'); + rows = treeTester.rows; expect(rows).toHaveLength(17); }); @@ -1125,6 +1135,7 @@ describe('Tree', () => { }); describe('empty state', () => { + // TODO it('should allow the user to tab to the empty tree', async () => { function renderEmptyState() { return ( @@ -1138,7 +1149,7 @@ describe('Tree', () => { ); } - let {getAllByRole, getByRole} = render( + let {getByRole} = render( { ); - let tree = getByRole('treegrid'); + let treeTester = testUtilUser.createTester('Tree', {user, root: getByRole('treegrid')}); + let tree = treeTester.tree; expect(tree).toHaveAttribute('data-empty', 'true'); expect(tree).not.toHaveAttribute('data-focused'); expect(tree).not.toHaveAttribute('data-focus-visible'); - let row = getAllByRole('row')[0]; + let row = treeTester.rows[0]; expect(row).toHaveAttribute('aria-level', '1'); expect(row).toHaveAttribute('aria-posinset', '1'); expect(row).toHaveAttribute('aria-setsize', '1'); - let gridCell = within(row).getByRole('gridcell'); + let gridCell = treeTester.cells({element: row})[0]; expect(gridCell).toHaveTextContent('No resultsNo results found.'); await user.tab(); diff --git a/packages/dev/docs/pages/react-aria/testing.mdx b/packages/dev/docs/pages/react-aria/testing.mdx index 961307f9ecb..94347934e37 100644 --- a/packages/dev/docs/pages/react-aria/testing.mdx +++ b/packages/dev/docs/pages/react-aria/testing.mdx @@ -92,12 +92,18 @@ See below for the full definition of the `User` object. Below is a list of the ARIA patterns testers currently supported by `createTester`. See the accompanying component testing docs pages for a sample of how to use the testers in your test suite. -- [React Aria Components ComboBox](ComboBox.html#testing) and [React Spectrum ComboBox](../react-spectrum/ComboBox.html#testing) +- [React Aria Components ComboBox](ComboBox.html#test-utils) and [React Spectrum ComboBox](../react-spectrum/ComboBox.html#test-utils) -- [React Aria Components GridList](GridList.html#testing) and [React Spectrum ListView](../react-spectrum/ListView.html#testing) +- [React Aria Components GridList](GridList.html#test-utils) and [React Spectrum ListView](../react-spectrum/ListView.html#test-utils) -- [React Aria Components Menu](Menu.html#testing) and [React Spectrum MenuTrigger](../react-spectrum/MenuTrigger.html#testing) +- [React Aria Components ListBox](ListBox.html#test-utils) and [React Spectrum ListBox](../react-spectrum/ListBox.html#test-utils) -- [React Aria Components Select](Select.html#testing) and [React Spectrum Picker](../react-spectrum/Picker.html#testing) +- [React Aria Components Menu](Menu.html#test-utils) and [React Spectrum MenuTrigger](../react-spectrum/MenuTrigger.html#test-utils) -- [React Aria Components Table](Table.html#testing) and [React Spectrum TableView](../react-spectrum/TableView.html#testing) +- [React Aria Components Select](Select.html#test-utils) and [React Spectrum Picker](../react-spectrum/Picker.html#test-utils) + +- [React Aria Components Table](Table.html#test-utils) and [React Spectrum TableView](../react-spectrum/TableView.html#test-utils) + +- [React Aria Components Tabs](Tabs.html#test-utils) and [React Spectrum Tabs](../react-spectrum/Tabs.html#test-utils) + +- [React Aria Components Tree](Tree.html#test-utils) and [React Spectrum TreeView](../react-spectrum/TreeView.html#test-utils) diff --git a/packages/react-aria-components/docs/ComboBox.mdx b/packages/react-aria-components/docs/ComboBox.mdx index e375b945c8a..eaa5500a8a4 100644 --- a/packages/react-aria-components/docs/ComboBox.mdx +++ b/packages/react-aria-components/docs/ComboBox.mdx @@ -13,7 +13,7 @@ export default Layout; import docs from 'docs:react-aria-components'; import statelyDocs from 'docs:@react-stately/combobox'; import comboboxUtils from 'docs:@react-aria/test-utils/src/combobox.ts'; -import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable, ClassAPI} from '@react-spectrum/docs'; +import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable, ClassAPI, VersionBadge} from '@react-spectrum/docs'; import styles from '@react-spectrum/docs/src/docs.css'; import packageData from 'react-aria-components/package.json'; import Anatomy from './ComboBoxAnatomy.svg'; @@ -1478,6 +1478,8 @@ If you need to customize things even further, such as accessing internal state, ## Testing +### Test utils + `@react-aria/test-utils` also offers common combobox interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the combobox tester and a sample of how you could use it in your test suite. diff --git a/packages/react-aria-components/docs/GridList.mdx b/packages/react-aria-components/docs/GridList.mdx index c83542ba3dc..efd8a5ecde5 100644 --- a/packages/react-aria-components/docs/GridList.mdx +++ b/packages/react-aria-components/docs/GridList.mdx @@ -13,7 +13,7 @@ export default Layout; import docs from 'docs:react-aria-components'; import sharedDocs from 'docs:@react-types/shared'; import gridlistUtil from 'docs:@react-aria/test-utils/src/gridlist.ts'; -import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable, ClassAPI} from '@react-spectrum/docs'; +import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable, ClassAPI, VersionBadge} from '@react-spectrum/docs'; import styles from '@react-spectrum/docs/src/docs.css'; import packageData from 'react-aria-components/package.json'; import Anatomy from './GridListAnatomy.svg'; @@ -1869,6 +1869,8 @@ If you need to customize things even further, such as accessing internal state, ## Testing +### Test utils + `@react-aria/test-utils` also offers common gridlist interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the gridlist tester and a sample of how you could use it in your test suite. diff --git a/packages/react-aria-components/docs/ListBox.mdx b/packages/react-aria-components/docs/ListBox.mdx index e01fb6994c0..9ed612c9bc0 100644 --- a/packages/react-aria-components/docs/ListBox.mdx +++ b/packages/react-aria-components/docs/ListBox.mdx @@ -12,7 +12,7 @@ export default Layout; import docs from 'docs:react-aria-components'; import sharedDocs from 'docs:@react-types/shared'; -import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable} from '@react-spectrum/docs'; +import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable, ClassAPI, VersionBadge} from '@react-spectrum/docs'; import styles from '@react-spectrum/docs/src/docs.css'; import packageData from 'react-aria-components/package.json'; import Anatomy from './ListBoxAnatomy.svg'; @@ -25,6 +25,7 @@ import Collections from '@react-spectrum/docs/pages/assets/component-illustratio import Selection from '@react-spectrum/docs/pages/assets/component-illustrations/Selection.svg'; import DragAndDrop from '@react-spectrum/docs/pages/assets/component-illustrations/DragAndDrop.svg'; import {StarterKits} from '@react-spectrum/docs/src/StarterKits'; +import listboxUtils from 'docs:@react-aria/test-utils/src/listbox.ts'; --- category: Collections @@ -864,7 +865,7 @@ function Example() { ]; return ( - + +`@react-aria/test-utils` also offers common listbox interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities +in your tests. Below is the full definition of the listbox tester and a sample of how you could use it in your test suite. + + + +```ts +// ListBox.test.ts +import {User} from '@react-aria/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse'}); +// ... + +it('ListBox can select an option via keyboard', async function () { + // Render your test component/app and initialize the listbox tester + let {getByTestId} = render( + + ... + + ); + let listboxTester = testUtilUser.createTester('ListBox', {root: getByTestId('test-listbox'), interactionType: 'keyboard'}); + + await listboxTester.toggleOptionSelection({option: 4}); + expect(listboxTester.options()[4]).toHaveAttribute('aria-selected', 'true'); + expect(onSelectionChange).toBeCalledTimes(1); +}); +``` diff --git a/packages/react-aria-components/docs/Menu.mdx b/packages/react-aria-components/docs/Menu.mdx index d241e432bd5..7af82c82e0a 100644 --- a/packages/react-aria-components/docs/Menu.mdx +++ b/packages/react-aria-components/docs/Menu.mdx @@ -12,7 +12,7 @@ export default Layout; import docs from 'docs:react-aria-components'; import menuUtil from 'docs:@react-aria/test-utils/src/menu.ts'; -import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable, ClassAPI} from '@react-spectrum/docs'; +import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable, ClassAPI, VersionBadge} from '@react-spectrum/docs'; import styles from '@react-spectrum/docs/src/docs.css'; import packageData from 'react-aria-components/package.json'; import Anatomy from './MenuAnatomy.svg'; @@ -1143,6 +1143,8 @@ By providing the above contexts, the existing `Button`, `Popover`, and `Menu` co ## Testing +### Test utils + `@react-aria/test-utils` also offers common menu interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the menu tester and a sample of how you could use it in your test suite. diff --git a/packages/react-aria-components/docs/Select.mdx b/packages/react-aria-components/docs/Select.mdx index daa445b19a2..f61aec40cfe 100644 --- a/packages/react-aria-components/docs/Select.mdx +++ b/packages/react-aria-components/docs/Select.mdx @@ -13,7 +13,7 @@ export default Layout; import docs from 'docs:react-aria-components'; import statelyDocs from 'docs:@react-stately/select'; import selectUtil from 'docs:@react-aria/test-utils/src/select.ts'; -import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable, ClassAPI} from '@react-spectrum/docs'; +import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable, ClassAPI, VersionBadge} from '@react-spectrum/docs'; import styles from '@react-spectrum/docs/src/docs.css'; import packageData from 'react-aria-components/package.json'; import Anatomy from './SelectAnatomy.svg'; @@ -1241,6 +1241,8 @@ from your automated accessibility tests as shown [here](./accessibility.html#fal ## Testing +### Test utils + `@react-aria/test-utils` also offers common select interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the select tester and a sample of how you could use it in your test suite. diff --git a/packages/react-aria-components/docs/Table.mdx b/packages/react-aria-components/docs/Table.mdx index 0e814c30f2c..2f235606d62 100644 --- a/packages/react-aria-components/docs/Table.mdx +++ b/packages/react-aria-components/docs/Table.mdx @@ -13,7 +13,7 @@ export default Layout; import docs from 'docs:react-aria-components'; import sharedDocs from 'docs:@react-types/shared'; import tableUtil from 'docs:@react-aria/test-utils/src/table.ts'; -import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable, ClassAPI} from '@react-spectrum/docs'; +import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable, ClassAPI, VersionBadge} from '@react-spectrum/docs'; import styles from '@react-spectrum/docs/src/docs.css'; import packageData from 'react-aria-components/package.json'; import Anatomy from './TableAnatomy.svg'; @@ -2479,6 +2479,8 @@ If you need to customize things even further, such as accessing internal state o ## Testing +### Test utils + `@react-aria/test-utils` also offers common table interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the table tester and a sample of how you could use it in your test suite. diff --git a/packages/react-aria-components/docs/Tabs.mdx b/packages/react-aria-components/docs/Tabs.mdx index 9e5809ce391..2125f9dd21b 100644 --- a/packages/react-aria-components/docs/Tabs.mdx +++ b/packages/react-aria-components/docs/Tabs.mdx @@ -12,7 +12,7 @@ export default Layout; import docs from 'docs:react-aria-components'; import statelyDocs from 'docs:@react-stately/tabs'; -import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable} from '@react-spectrum/docs'; +import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable, ClassAPI, VersionBadge} from '@react-spectrum/docs'; import styles from '@react-spectrum/docs/src/docs.css'; import packageData from 'react-aria-components/package.json'; import Anatomy from '@react-aria/tabs/docs/anatomy.svg'; @@ -24,6 +24,7 @@ import {ExampleList} from '@react-spectrum/docs/src/ExampleList'; import Collections from '@react-spectrum/docs/pages/assets/component-illustrations/Collections.svg'; import Selection from '@react-spectrum/docs/pages/assets/component-illustrations/Selection.svg'; import {StarterKits} from '@react-spectrum/docs/src/StarterKits'; +import tabsUtils from 'docs:@react-aria/test-utils/src/tabs.ts'; --- category: Navigation @@ -751,3 +752,37 @@ function TabNavigation() { ### Hooks If you need to customize things even further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See [useTabList](useTabList.html) for more details. + +## Testing + +### Test utils + +`@react-aria/test-utils` also offers common tabs interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +in your tests. Below is the full definition of the tabs tester and a sample of how you could use it in your test suite. + + + +```ts +// Tabs.test.ts +import {User} from '@react-aria/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse'}); +// ... + +it('Tabs can change selection via keyboard', async function () { + // Render your test component/app and initialize the listbox tester + let {getByTestId} = render( + + ... + + ); + let tabsTester = testUtilUser.createTester('Tabs', {root: getByTestId('test-tabs'), interactionType: 'keyboard'}); + + let tabs = tabsTester.tabs; + expect(tabsTester.selectedTab).toBe(tabs[0]); + + await tabsTester.triggerTab({tab: 1}); + expect(onSelectionChange).toBeCalledTimes(1); + expect(tabsTester.selectedTab).toBe(tabs[1]); +}); +``` diff --git a/packages/react-aria-components/docs/Tree.mdx b/packages/react-aria-components/docs/Tree.mdx index 708beebf310..f120c76a056 100644 --- a/packages/react-aria-components/docs/Tree.mdx +++ b/packages/react-aria-components/docs/Tree.mdx @@ -11,11 +11,12 @@ import {Layout} from '@react-spectrum/docs'; export default Layout; import docs from 'docs:react-aria-components'; -import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable} from '@react-spectrum/docs'; +import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable, ClassAPI, VersionBadge} from '@react-spectrum/docs'; import styles from '@react-spectrum/docs/src/docs.css'; import packageData from 'react-aria-components/package.json'; import ChevronRight from '@spectrum-icons/workflow/ChevronRight'; import {InlineAlert, Content, Heading} from '@adobe/react-spectrum'; +import treeUtils from 'docs:@react-aria/test-utils/src/tree.ts'; --- category: Collections @@ -320,3 +321,42 @@ TreeItem also exposes a `--tree-item-level` CSS custom property, which you can u padding-left: calc((var(--tree-item-level) - 1) * 20px); } ``` + +## Testing + +### Test utils + +`@react-aria/test-utils` also offers common tree interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +in your tests. Below is the full definition of the tree tester and a sample of how you could use it in your test suite. + + + +```ts +// Tree.test.ts +import {User} from '@react-aria/test-utils'; + +let testUtilUser = new User({interactionType: 'mouse'}); +// ... + +it('Tree can select a row via keyboard', async function () { + // Render your test component/app and initialize the Tree tester + let {getByTestId} = render( + + ... + + ); + let treeTester = testUtilUser.createTester('Tree', {root: getByTestId('test-tree'), interactionType: 'keyboard'}); + + await treeTester.toggleRowSelection({row: 0}); + expect(treeTester.selectedRows).toHaveLength(1); + expect(within(treeTester.rows[0]).getByRole('checkbox')).toBeChecked(); + + await treeTester.toggleRowSelection({row: 1}); + expect(treeTester.selectedRows).toHaveLength(2); + expect(within(treeTester.rows[1]).getByRole('checkbox')).toBeChecked(); + + await treeTester.toggleRowSelection({row: 0}); + expect(treeTester.selectedRows).toHaveLength(1); + expect(within(treeTester.rows[0]).getByRole('checkbox')).not.toBeChecked(); +}); +``` diff --git a/packages/react-aria-components/stories/GridList.stories.tsx b/packages/react-aria-components/stories/GridList.stories.tsx index 76b78c3af1a..d896c5d882b 100644 --- a/packages/react-aria-components/stories/GridList.stories.tsx +++ b/packages/react-aria-components/stories/GridList.stories.tsx @@ -45,7 +45,7 @@ export const GridListExample = (args) => ( ); -export const MyGridListItem = (props: GridListItemProps) => { +const MyGridListItem = (props: GridListItemProps) => { return ( describe.each` + interactionType + ${'mouse'} + ${'keyboard'} + ${'touch'} +`(`${name} - $interactionType`, tests)); + +// @ts-ignore +describeInteractions.only = ((name, tests) => describe.only.each` + interactionType + ${'mouse'} + ${'keyboard'} + ${'touch'} +`(`${name} - $interactionType`, tests)); + +// @ts-ignore +describeInteractions.skip = ((name, tests) => describe.skip.each` + interactionType + ${'mouse'} + ${'keyboard'} + ${'touch'} +`(`${name} - $interactionType`, tests)); + +interface AriaBaseTestProps { + setup?: () => void, + prefix?: string +} +interface AriaTreeTestProps extends AriaBaseTestProps { + renderers: { + // must have an aria-label + standard: (props?: {name: string}) => ReturnType, + // must have an aria-label + singleSelection?: (props?: {name: string}) => ReturnType, + // must have an aria-label + allInteractionsDisabled?: (props?: {name: string}) => ReturnType + } +} +export const AriaTreeTests = ({renderers, setup, prefix}: AriaTreeTestProps) => { + describe(prefix ? prefix + 'AriaTree' : 'AriaTree', function () { + let user; + let testUtilUser = new User(); + setup?.(); + + beforeAll(function () { + jest.useFakeTimers(); + }); + + beforeEach(function () { + user = userEvent.setup({delay: null, pointerMap}); + }); + + afterEach(() => { + act(() => jest.runAllTimers()); + }); + + it('should have the base set of aria and data attributes', () => { + let root = (renderers.standard!)(); + let treeTester = testUtilUser.createTester('Tree', {user, root: root.container}); + let tree = treeTester.tree; + expect(tree).toHaveAttribute('aria-label'); + + for (let row of treeTester.rows) { + expect(row).toHaveAttribute('aria-level'); + expect(row).toHaveAttribute('aria-posinset'); + expect(row).toHaveAttribute('aria-setsize'); + } + expect(treeTester.rows[0]).not.toHaveAttribute('aria-expanded'); + expect(treeTester.rows[1]).toHaveAttribute('aria-expanded', 'false'); + }); + + describeInteractions('interaction', function ({interactionType}) { + it('should have the expected attributes on the rows', async () => { + let tree = (renderers.standard!)(); + let treeTester = testUtilUser.createTester('Tree', {user, root: tree.container, interactionType}); + await treeTester.toggleRowExpansion({row: 1}); + await treeTester.toggleRowExpansion({row: 2}); + + let rows = treeTester.rows; + let rowNoChild = rows[0]; + expect(rowNoChild).toHaveAttribute('aria-label'); + expect(rowNoChild).not.toHaveAttribute('aria-expanded'); + expect(rowNoChild).toHaveAttribute('aria-level', '1'); + expect(rowNoChild).toHaveAttribute('aria-posinset', '1'); + expect(rowNoChild).toHaveAttribute('aria-setsize', '3'); + + let rowWithChildren = rows[1]; + // Row has action since it is expandable but not selectable. + expect(rowWithChildren).toHaveAttribute('aria-expanded', 'true'); + expect(rowWithChildren).toHaveAttribute('aria-level', '1'); + expect(rowWithChildren).toHaveAttribute('aria-posinset', '2'); + expect(rowWithChildren).toHaveAttribute('aria-setsize', '3'); + + let level2ChildRow = rows[2]; + expect(level2ChildRow).toHaveAttribute('aria-expanded', 'true'); + expect(level2ChildRow).toHaveAttribute('data-expanded', 'true'); + expect(level2ChildRow).toHaveAttribute('aria-level', '2'); + expect(level2ChildRow).toHaveAttribute('aria-posinset', '1'); + expect(level2ChildRow).toHaveAttribute('aria-setsize', '3'); + + let level3ChildRow = rows[3]; + expect(level3ChildRow).not.toHaveAttribute('aria-expanded'); + expect(level3ChildRow).toHaveAttribute('aria-level', '3'); + expect(level3ChildRow).toHaveAttribute('aria-posinset', '1'); + expect(level3ChildRow).toHaveAttribute('aria-setsize', '1'); + + let level2ChildRow2 = rows[4]; + expect(level2ChildRow2).not.toHaveAttribute('aria-expanded'); + expect(level2ChildRow2).toHaveAttribute('aria-level', '2'); + expect(level2ChildRow2).toHaveAttribute('aria-posinset', '2'); + expect(level2ChildRow2).toHaveAttribute('aria-setsize', '3'); + + let level2ChildRow3 = rows[5]; + expect(level2ChildRow3).not.toHaveAttribute('aria-expanded'); + expect(level2ChildRow3).toHaveAttribute('aria-level', '2'); + expect(level2ChildRow3).toHaveAttribute('aria-posinset', '3'); + expect(level2ChildRow3).toHaveAttribute('aria-setsize', '3'); + + // Collapse the first row and make sure it's collpased and that the inner rows are gone + await treeTester.toggleRowExpansion({row: 1}); + expect(rowWithChildren).toHaveAttribute('aria-expanded', 'false'); + expect(level2ChildRow).not.toBeInTheDocument(); + }); + }); + + if (renderers.singleSelection) { + describe('single selection', function () { + describeInteractions('interaction', function ({interactionType}) { + // todo add test for using Space on the row to select it + it('can select items', async () => { + let tree = (renderers.singleSelection!)(); + let treeTester = testUtilUser.createTester('Tree', {user, root: tree.container, interactionType}); + + let rows = treeTester.rows; + expect(rows[0]).toHaveAttribute('aria-selected', 'false'); + expect(rows[1]).toHaveAttribute('aria-selected', 'false'); + // disabled rows should not be selectable + expect(rows[2]).not.toHaveAttribute('aria-selected'); + expect(within(rows[2]).getByRole('checkbox')).toHaveAttribute('disabled'); + + await treeTester.toggleRowSelection({row: 0}); + expect(rows[0]).toHaveAttribute('aria-selected', 'true'); + expect(rows[1]).toHaveAttribute('aria-selected', 'false'); + expect(treeTester.selectedRows).toHaveLength(1); + expect(within(treeTester.rows[0]).getByRole('checkbox')).toBeChecked(); + + await treeTester.toggleRowSelection({row: 1}); + expect(rows[0]).toHaveAttribute('aria-selected', 'false'); + expect(rows[1]).toHaveAttribute('aria-selected', 'true'); + expect(treeTester.selectedRows).toHaveLength(1); + expect(within(treeTester.rows[0]).getByRole('checkbox')).not.toBeChecked(); + expect(within(treeTester.rows[1]).getByRole('checkbox')).toBeChecked(); + + await treeTester.toggleRowSelection({row: 2}); + expect(rows[0]).toHaveAttribute('aria-selected', 'false'); + expect(rows[1]).toHaveAttribute('aria-selected', 'true'); + expect(rows[2]).not.toHaveAttribute('aria-selected'); + + await treeTester.toggleRowExpansion({row: 1}); + rows = treeTester.rows; + // row 2 is now the subrow of row 1 because we expanded it + expect(rows[2]).toHaveAttribute('aria-selected', 'false'); + + await treeTester.toggleRowSelection({row: 2}); + expect(rows[0]).toHaveAttribute('aria-selected', 'false'); + expect(rows[1]).toHaveAttribute('aria-selected', 'false'); + expect(rows[2]).toHaveAttribute('aria-selected', 'true'); + + // collapse and re-expand to make sure the selection persists + await treeTester.toggleRowExpansion({row: 1}); + await treeTester.toggleRowExpansion({row: 1}); + rows = treeTester.rows; + expect(rows[2]).toHaveAttribute('aria-selected', 'true'); + + await treeTester.toggleRowSelection({row: 2}); + expect(rows[0]).toHaveAttribute('aria-selected', 'false'); + expect(rows[1]).toHaveAttribute('aria-selected', 'false'); + expect(rows[2]).toHaveAttribute('aria-selected', 'false'); + + await treeTester.toggleRowExpansion({row: 1}); + // items inside a disabled item can be selected + await treeTester.toggleRowExpansion({row: 2}); + rows = treeTester.rows; + + await treeTester.toggleRowSelection({row: 3}); + expect(rows[3]).toHaveAttribute('aria-selected', 'true'); + }); + }); + }); + } + + if (renderers.allInteractionsDisabled) { + describe('all interactions disabled', function () { + describeInteractions('interaction', function ({interactionType}) { + it('should not be able to interact with the tree', async () => { + let tree = (renderers.allInteractionsDisabled!)(); + let treeTester = testUtilUser.createTester('Tree', {user, root: tree.container, interactionType}); + + let rows = treeTester.rows; + expect(rows[2]).toHaveAttribute('aria-expanded', 'false'); + + await treeTester.toggleRowExpansion({row: 2}); + expect(rows[2]).toHaveAttribute('aria-expanded', 'false'); + + await treeTester.toggleRowSelection({row: 2}); + expect(rows[2]).not.toHaveAttribute('aria-selected'); + }); + }); + }); + } + }); +}; diff --git a/packages/react-aria-components/test/ListBox.test.js b/packages/react-aria-components/test/ListBox.test.js index f09d62797eb..c2443fb1582 100644 --- a/packages/react-aria-components/test/ListBox.test.js +++ b/packages/react-aria-components/test/ListBox.test.js @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {act, fireEvent, mockClickDefault, pointerMap, render, within} from '@react-spectrum/test-utils-internal'; +import {act, fireEvent, installPointerEvent, mockClickDefault, pointerMap, render, within} from '@react-spectrum/test-utils-internal'; import { Button, Dialog, DialogTrigger, @@ -27,6 +27,7 @@ import { UNSTABLE_Virtualizer as Virtualizer } from '../'; import React, {useState} from 'react'; +import {User} from '@react-aria/test-utils'; import userEvent from '@testing-library/user-event'; let TestListBox = ({listBoxProps, itemProps}) => ( @@ -60,6 +61,8 @@ let keyPress = (key) => { describe('ListBox', () => { let user; + let testUtilUser = new User(); + beforeAll(() => { user = userEvent.setup({delay: null, pointerMap}); jest.useFakeTimers(); @@ -76,7 +79,7 @@ describe('ListBox', () => { }); it('should have the base set of aria and data attributes', () => { - let {getByRole, getAllByRole} = render( + let {getByRole} = render( Cat Dog @@ -89,14 +92,16 @@ describe('ListBox', () => { ); - let menu = getByRole('listbox'); - expect(menu).toHaveAttribute('data-rac'); - for (let group of getAllByRole('group')) { - expect(group).toHaveAttribute('data-rac'); + let listboxTester = testUtilUser.createTester('ListBox', {root: getByRole('listbox')}); + expect(listboxTester.listbox).toHaveAttribute('data-rac'); + let sections = listboxTester.sections; + for (let section of sections) { + expect(section).toHaveAttribute('data-rac'); } - for (let option of getAllByRole('option')) { + let options = listboxTester.options(); + for (let option of options) { expect(option).toHaveAttribute('data-rac'); } }); @@ -428,17 +433,18 @@ describe('ListBox', () => { }); it('should support selection state', async () => { - let {getAllByRole} = renderListbox({selectionMode: 'multiple'}, {className: ({isSelected}) => isSelected ? 'selected' : ''}); - let option = getAllByRole('option')[0]; + let {getByRole} = renderListbox({selectionMode: 'multiple'}, {className: ({isSelected}) => isSelected ? 'selected' : ''}); + let listboxTester = testUtilUser.createTester('ListBox', {root: getByRole('listbox')}); + let option = listboxTester.options()[0]; expect(option).not.toHaveAttribute('aria-selected', 'true'); expect(option).not.toHaveClass('selected'); - await user.click(option); + await listboxTester.toggleOptionSelection({option}); expect(option).toHaveAttribute('aria-selected', 'true'); expect(option).toHaveClass('selected'); - await user.click(option); + await listboxTester.toggleOptionSelection({option}); expect(option).not.toHaveAttribute('aria-selected', 'true'); expect(option).not.toHaveClass('selected'); }); @@ -459,6 +465,7 @@ describe('ListBox', () => { Kangaroo ); + let items = getAllByRole('option'); expect(items[1]).toHaveAttribute('aria-disabled', 'true'); @@ -468,21 +475,79 @@ describe('ListBox', () => { expect(document.activeElement).toBe(items[2]); }); - it('should support onAction on items', async () => { + it.each` + interactionType + ${'mouse'} + ${'keyboard'} + ${'touch'} + `('should support onAction, interactionType: $interactionType ', async ({interactionType}) => { let onAction = jest.fn(); - let {getAllByRole} = render( + let {getByRole} = render( Cat Dog Kangaroo ); - let items = getAllByRole('option'); - await user.click(items[0]); + let listboxTester = testUtilUser.createTester('ListBox', {root: getByRole('listbox')}); + + let options = listboxTester.options(); + await listboxTester.triggerOptionAction({option: options[0], interactionType}); + expect(onAction).toHaveBeenCalledTimes(1); + }); + + it('should trigger onAction on double click if selectionBehavior="replace"', async () => { + let onAction = jest.fn(); + let {getByRole} = render( + + Cat + Dog + Kangaroo + + ); + let listboxTester = testUtilUser.createTester('ListBox', {root: getByRole('listbox')}); + + let options = listboxTester.options(); + await listboxTester.triggerOptionAction({option: options[0]}); + let selectedOptions = listboxTester.selectedOptions; + expect(selectedOptions).toHaveLength(1); + expect(onAction).not.toHaveBeenCalled(); + + await listboxTester.triggerOptionAction({option: options[1], needsDoubleClick: true}); + selectedOptions = listboxTester.selectedOptions; + expect(selectedOptions).toHaveLength(1); expect(onAction).toHaveBeenCalledTimes(1); }); - it('should support onAction on list ans list items', async () => { + describe('with pointer events', () => { + installPointerEvent(); + it('should trigger selection on long press if both onAction and selection exist (touch only)', async () => { + let onAction = jest.fn(); + let {getByRole} = render( + + Cat + Dog + Kangaroo + + ); + let listboxTester = testUtilUser.createTester('ListBox', {root: getByRole('listbox'), advanceTimer: jest.advanceTimersByTime, interactionType: 'touch'}); + + await listboxTester.toggleOptionSelection({option: listboxTester.options()[0]}); + expect(listboxTester.selectedOptions).toHaveLength(0); + expect(onAction).toHaveBeenCalledTimes(1); + + await listboxTester.toggleOptionSelection({option: listboxTester.options()[0], needsLongPress: true}); + expect(listboxTester.selectedOptions).toHaveLength(1); + expect(listboxTester.selectedOptions[0]).toBe(listboxTester.options()[0]); + expect(onAction).toHaveBeenCalledTimes(1); + + await listboxTester.toggleOptionSelection({option: listboxTester.options()[1]}); + expect(listboxTester.selectedOptions).toHaveLength(2); + expect(listboxTester.selectedOptions[1]).toBe(listboxTester.options()[1]); + }); + }); + + it('should support onAction on list and list items', async () => { let onAction = jest.fn(); let itemAction = jest.fn(); let {getAllByRole} = render( diff --git a/packages/react-aria-components/test/Tabs.test.js b/packages/react-aria-components/test/Tabs.test.js index 5b471156605..bc979391568 100644 --- a/packages/react-aria-components/test/Tabs.test.js +++ b/packages/react-aria-components/test/Tabs.test.js @@ -10,14 +10,15 @@ * governing permissions and limitations under the License. */ -import {act, fireEvent, pointerMap, render, waitFor, within} from '@react-spectrum/test-utils-internal'; +import {fireEvent, pointerMap, render, waitFor, within} from '@react-spectrum/test-utils-internal'; import React from 'react'; import {Tab, TabList, TabPanel, Tabs} from '../'; import {TabsExample} from '../stories/Tabs.stories'; +import {User} from '@react-aria/test-utils'; import userEvent from '@testing-library/user-event'; let renderTabs = (tabsProps, tablistProps, tabProps, tabpanelProps) => render( - + A B @@ -31,24 +32,26 @@ let renderTabs = (tabsProps, tablistProps, tabProps, tabpanelProps) => render( describe('Tabs', () => { let user; + let testUtilUser = new User(); + beforeAll(() => { user = userEvent.setup({delay: null, pointerMap}); }); it('should render tabs with default classes', () => { - let {getByRole, getAllByRole} = renderTabs(); - let tablist = getByRole('tablist'); - let tabs = tablist.closest('.react-aria-Tabs'); + let {getByTestId} = renderTabs(); + let tabs = getByTestId('tabs-wrapper'); + let tabsTester = testUtilUser.createTester('Tabs', {root: tabs}); + let tablist = tabsTester.tablist; expect(tabs).toBeInTheDocument(); expect(tablist).toHaveAttribute('class', 'react-aria-TabList'); expect(tablist).toHaveAttribute('aria-label', 'Test'); - for (let tab of getAllByRole('tab')) { + for (let tab of tabsTester.tabs) { expect(tab).toHaveAttribute('class', 'react-aria-Tab'); } - let tabpanel = getByRole('tabpanel'); - expect(tabpanel).toHaveAttribute('class', 'react-aria-TabPanel'); + expect(tabsTester.tabpanels[0]).toHaveAttribute('class', 'react-aria-TabPanel'); }); it('should render tabs with custom classes', () => { @@ -256,31 +259,30 @@ describe('Tabs', () => { it('selects first tab if all tabs are disabled', async () => { let {getByRole} = renderTabs({}, {}, {isDisabled: true}); + let tabsTester = testUtilUser.createTester('Tabs', {root: getByRole('tablist')}); await user.tab(); - let tablist = getByRole('tablist'); - let tabs = within(tablist).getAllByRole('tab'); - let tabpanel = getByRole('tabpanel'); - expect(tabs[0]).toHaveAttribute('aria-selected', 'true'); - expect(document.activeElement).toBe(tabpanel); + expect(tabsTester.selectedTab).toBe(tabsTester.tabs[0]); + expect(document.activeElement).toBe(tabsTester.tabpanels[0]); }); it('should support selected state', async () => { let onSelectionChange = jest.fn(); - let {getAllByRole} = renderTabs({onSelectionChange}, {}, {className: ({isSelected}) => isSelected ? 'selected' : ''}); - let tabs = getAllByRole('tab'); + let {getByRole} = renderTabs({onSelectionChange}, {}, {className: ({isSelected}) => isSelected ? 'selected' : ''}); + let tabsTester = testUtilUser.createTester('Tabs', {root: getByRole('tablist')}); + let tabs = tabsTester.tabs; expect(tabs[0]).toHaveAttribute('aria-selected', 'true'); expect(tabs[0]).toHaveClass('selected'); - await user.click(tabs[1]); + await tabsTester.triggerTab({tab: 1}); expect(onSelectionChange).toHaveBeenLastCalledWith('b'); expect(tabs[0]).not.toHaveAttribute('aria-selected', 'true'); expect(tabs[0]).not.toHaveClass('selected'); expect(tabs[1]).toHaveAttribute('aria-selected', 'true'); expect(tabs[1]).toHaveClass('selected'); - await user.click(tabs[0]); + await tabsTester.triggerTab({tab: 0}); expect(onSelectionChange).toHaveBeenLastCalledWith('a'); expect(tabs[0]).toHaveAttribute('aria-selected', 'true'); expect(tabs[0]).toHaveClass('selected'); @@ -288,7 +290,7 @@ describe('Tabs', () => { it('should update TabPanel ID when current tab is changed', async () => { let onSelectionChange = jest.fn(); - let {getByRole, getAllByRole} = render( + let {getByRole} = render( First @@ -301,17 +303,16 @@ describe('Tabs', () => { ); - expect(getByRole('tabpanel').getAttribute('id')).toContain('first-element'); - let tabs = getAllByRole('tab'); + let tabsTester = testUtilUser.createTester('Tabs', {root: getByRole('tablist')}); + expect(tabsTester.activeTabpanel.getAttribute('id')).toContain('first-element'); - await user.click(tabs[1]); + await tabsTester.triggerTab({tab: 1}); expect(onSelectionChange).toHaveBeenCalled(); - expect(getByRole('tabpanel').getAttribute('id')).toContain('second-element'); + expect(tabsTester.activeTabpanel.getAttribute('id')).toContain('second-element'); - await user.click(tabs[2]); + await tabsTester.triggerTab({tab: 2}); expect(onSelectionChange).toHaveBeenCalled(); - expect(getByRole('tabpanel').getAttribute('id')).toContain('third-element'); - + expect(tabsTester.activeTabpanel.getAttribute('id')).toContain('third-element'); }); it('should support orientation', () => { @@ -327,6 +328,33 @@ describe('Tabs', () => { expect(tabs).toHaveClass('vertical'); }); + it.each` + interactionType + ${'mouse'} + ${'keyboard'} + ${'touch'} + `('should support changing the selected tab regardless of interaction type, interactionType: $interactionType ', async ({interactionType}) => { + let {getByRole} = renderTabs({orientation: 'vertical'}); + let tabsTester = testUtilUser.createTester('Tabs', {root: getByRole('tablist'), interactionType}); + let tabs = tabsTester.tabs; + + await tabsTester.triggerTab({tab: 0}); + expect(tabsTester.selectedTab).toBe(tabs[0]); + expect(tabsTester.activeTabpanel.getAttribute('aria-labelledby')).toBe(tabs[0].id); + + await tabsTester.triggerTab({tab: 1}); + expect(tabsTester.selectedTab).toBe(tabs[1]); + expect(tabsTester.activeTabpanel.getAttribute('aria-labelledby')).toBe(tabs[1].id); + + await tabsTester.triggerTab({tab: 2}); + expect(tabsTester.selectedTab).toBe(tabs[2]); + expect(tabsTester.activeTabpanel.getAttribute('aria-labelledby')).toBe(tabs[2].id); + + await tabsTester.triggerTab({tab: 1}); + expect(tabsTester.selectedTab).toBe(tabs[1]); + expect(tabsTester.activeTabpanel.getAttribute('aria-labelledby')).toBe(tabs[1].id); + }); + it('should support refs', () => { let tabsRef = React.createRef(); let tabListRef = React.createRef(); @@ -372,30 +400,25 @@ describe('Tabs', () => { console.error = consoleError; }); - it('should support keyboardActivation=manual', () => { + it('should support keyboardActivation=manual', async () => { let onSelectionChange = jest.fn(); let {getByRole} = renderTabs({keyboardActivation: 'manual', onSelectionChange, defaultSelectedKey: 'a'}); + let tabsTester = testUtilUser.createTester('Tabs', {root: getByRole('tablist'), interactionType: 'keyboard'}); - let tablist = getByRole('tablist'); - let tabs = within(tablist).getAllByRole('tab'); - let firstItem = tabs[0]; - let secondItem = tabs[1]; - let thirdItem = tabs[2]; - act(() => {firstItem.focus();}); - expect(firstItem).toHaveAttribute('aria-selected', 'true'); - fireEvent.keyDown(firstItem, {key: 'ArrowRight', code: 39, charCode: 39}); - fireEvent.keyUp(document.activeElement, {key: 'ArrowRight', code: 39, charCode: 39}); - expect(secondItem).toHaveAttribute('aria-selected', 'false'); - expect(document.activeElement).toBe(secondItem); - fireEvent.keyDown(secondItem, {key: 'ArrowRight', code: 39, charCode: 39}); - fireEvent.keyUp(document.activeElement, {key: 'ArrowRight', code: 39, charCode: 39}); - expect(thirdItem).toHaveAttribute('aria-selected', 'false'); - expect(document.activeElement).toBe(thirdItem); - fireEvent.keyDown(thirdItem, {key: 'Enter', code: 13, charCode: 13}); - fireEvent.keyUp(document.activeElement, {key: 'Enter', code: 13, charCode: 13}); - expect(firstItem).toHaveAttribute('aria-selected', 'false'); - expect(secondItem).toHaveAttribute('aria-selected', 'false'); - expect(thirdItem).toHaveAttribute('aria-selected', 'true'); + let tabs = tabsTester.tabs; + await tabsTester.triggerTab({tab: 0}); + + expect(tabs[0]).toHaveAttribute('aria-selected', 'true'); + await tabsTester.triggerTab({tab: 1}); + expect(tabs[1]).toHaveAttribute('aria-selected', 'false'); + expect(document.activeElement).toBe(tabs[1]); + await tabsTester.triggerTab({tab: 2}); + expect(tabs[2]).toHaveAttribute('aria-selected', 'false'); + expect(document.activeElement).toBe(tabs[2]); + await tabsTester.triggerTab({tab: 2, manualActivation: true}); + expect(tabs[1]).toHaveAttribute('aria-selected', 'false'); + expect(tabs[1]).toHaveAttribute('aria-selected', 'false'); + expect(tabs[2]).toHaveAttribute('aria-selected', 'true'); expect(onSelectionChange).toBeCalledTimes(1); }); diff --git a/packages/react-aria-components/test/Tree.test.tsx b/packages/react-aria-components/test/Tree.test.tsx index 89b2fdc064e..e62a810b70a 100644 --- a/packages/react-aria-components/test/Tree.test.tsx +++ b/packages/react-aria-components/test/Tree.test.tsx @@ -11,6 +11,7 @@ */ import {act, fireEvent, mockClickDefault, pointerMap, render, within} from '@react-spectrum/test-utils-internal'; +import {AriaTreeTests} from './AriaTree.test-util'; import {Button, Checkbox, Collection, UNSTABLE_ListLayout as ListLayout, Text, UNSTABLE_Tree, UNSTABLE_TreeItem, UNSTABLE_TreeItemContent, UNSTABLE_Virtualizer as Virtualizer} from '../'; import {composeStories} from '@storybook/react'; import React from 'react'; @@ -187,20 +188,14 @@ describe('Tree', () => { expect(tree).toHaveAttribute('style', expect.stringContaining('width: 200px')); }); - it('should have the base set of aria and data attributes', () => { + it('should have the base set of data attributes', () => { let {getByRole, getAllByRole} = render(); let tree = getByRole('treegrid'); - expect(tree).toHaveAttribute('data-rac'); - expect(tree).toHaveAttribute('aria-label', 'test tree'); expect(tree).not.toHaveAttribute('data-empty'); expect(tree).not.toHaveAttribute('data-focused'); expect(tree).not.toHaveAttribute('data-focus-visible'); for (let row of getAllByRole('row')) { - expect(row).toHaveAttribute('aria-level'); - expect(row).toHaveAttribute('data-level'); - expect(row).toHaveAttribute('aria-posinset'); - expect(row).toHaveAttribute('aria-setsize'); expect(row).toHaveAttribute('data-rac'); expect(row).not.toHaveAttribute('data-selected'); expect(row).not.toHaveAttribute('data-disabled'); @@ -220,66 +215,43 @@ describe('Tree', () => { expect(rowNoChild).toHaveAttribute('aria-label', 'Photos'); expect(rowNoChild).not.toHaveAttribute('aria-expanded'); expect(rowNoChild).not.toHaveAttribute('data-expanded'); - expect(rowNoChild).toHaveAttribute('aria-level', '1'); expect(rowNoChild).toHaveAttribute('data-level', '1'); - expect(rowNoChild).toHaveAttribute('aria-posinset', '1'); - expect(rowNoChild).toHaveAttribute('aria-setsize', '2'); expect(rowNoChild).not.toHaveAttribute('data-has-child-rows'); expect(rowNoChild).toHaveAttribute('data-rac'); let rowWithChildren = rows[1]; // Row has action since it is expandable but not selectable. expect(rowWithChildren).toHaveAttribute('aria-label', 'Projects'); - expect(rowWithChildren).toHaveAttribute('aria-expanded', 'true'); expect(rowWithChildren).toHaveAttribute('data-expanded', 'true'); - expect(rowWithChildren).toHaveAttribute('aria-level', '1'); expect(rowWithChildren).toHaveAttribute('data-level', '1'); - expect(rowWithChildren).toHaveAttribute('aria-posinset', '2'); - expect(rowWithChildren).toHaveAttribute('aria-setsize', '2'); expect(rowWithChildren).toHaveAttribute('data-has-child-rows', 'true'); expect(rowWithChildren).toHaveAttribute('data-rac'); let level2ChildRow = rows[2]; expect(level2ChildRow).toHaveAttribute('aria-label', 'Projects-1'); - expect(level2ChildRow).toHaveAttribute('aria-expanded', 'true'); expect(level2ChildRow).toHaveAttribute('data-expanded', 'true'); - expect(level2ChildRow).toHaveAttribute('aria-level', '2'); expect(level2ChildRow).toHaveAttribute('data-level', '2'); - expect(level2ChildRow).toHaveAttribute('aria-posinset', '1'); - expect(level2ChildRow).toHaveAttribute('aria-setsize', '3'); expect(level2ChildRow).toHaveAttribute('data-has-child-rows', 'true'); expect(level2ChildRow).toHaveAttribute('data-rac'); let level3ChildRow = rows[3]; expect(level3ChildRow).toHaveAttribute('aria-label', 'Projects-1A'); - expect(level3ChildRow).not.toHaveAttribute('aria-expanded'); expect(level3ChildRow).not.toHaveAttribute('data-expanded'); - expect(level3ChildRow).toHaveAttribute('aria-level', '3'); expect(level3ChildRow).toHaveAttribute('data-level', '3'); - expect(level3ChildRow).toHaveAttribute('aria-posinset', '1'); - expect(level3ChildRow).toHaveAttribute('aria-setsize', '1'); expect(level3ChildRow).not.toHaveAttribute('data-has-child-rows'); expect(level3ChildRow).toHaveAttribute('data-rac'); let level2ChildRow2 = rows[4]; expect(level2ChildRow2).toHaveAttribute('aria-label', 'Projects-2'); - expect(level2ChildRow2).not.toHaveAttribute('aria-expanded'); expect(level2ChildRow2).not.toHaveAttribute('data-expanded'); - expect(level2ChildRow2).toHaveAttribute('aria-level', '2'); expect(level2ChildRow2).toHaveAttribute('data-level', '2'); - expect(level2ChildRow2).toHaveAttribute('aria-posinset', '2'); - expect(level2ChildRow2).toHaveAttribute('aria-setsize', '3'); expect(level2ChildRow2).not.toHaveAttribute('data-has-child-rows'); expect(level2ChildRow2).toHaveAttribute('data-rac'); let level2ChildRow3 = rows[5]; expect(level2ChildRow3).toHaveAttribute('aria-label', 'Projects-3'); - expect(level2ChildRow3).not.toHaveAttribute('aria-expanded'); expect(level2ChildRow3).not.toHaveAttribute('data-expanded'); - expect(level2ChildRow3).toHaveAttribute('aria-level', '2'); expect(level2ChildRow3).toHaveAttribute('data-level', '2'); - expect(level2ChildRow3).toHaveAttribute('aria-posinset', '3'); - expect(level2ChildRow3).toHaveAttribute('aria-setsize', '3'); expect(level2ChildRow3).not.toHaveAttribute('data-has-child-rows'); expect(level2ChildRow3).toHaveAttribute('data-rac'); }); @@ -288,9 +260,7 @@ describe('Tree', () => { let {getAllByRole} = render(); let rows = getAllByRole('row'); - expect(rows[1]).toHaveAttribute('aria-label', 'Projects'); expect(rows[1]).toHaveAttribute('data-has-child-rows', 'true'); - expect(rows[1]).toHaveAttribute('aria-selected', 'false'); }); it('should support dynamic trees', () => { @@ -871,22 +841,14 @@ describe('Tree', () => { await user.tab(); expect(document.activeElement).toBe(rows[0]); - expect(rows[0]).toHaveAttribute('aria-expanded', 'true'); expect(rows[0]).toHaveAttribute('data-expanded', 'true'); - expect(rows[0]).toHaveAttribute('aria-level', '1'); - expect(rows[0]).toHaveAttribute('aria-posinset', '1'); - expect(rows[0]).toHaveAttribute('aria-setsize', '2'); expect(rows[0]).toHaveAttribute('data-has-child-rows', 'true'); expect(onExpandedChange).toHaveBeenCalledTimes(0); // Check we can open/close a top level row await trigger(rows[0], 'Enter'); expect(document.activeElement).toBe(rows[0]); - expect(rows[0]).toHaveAttribute('aria-expanded', 'false'); expect(rows[0]).not.toHaveAttribute('data-expanded'); - expect(rows[0]).toHaveAttribute('aria-level', '1'); - expect(rows[0]).toHaveAttribute('aria-posinset', '1'); - expect(rows[0]).toHaveAttribute('aria-setsize', '2'); expect(rows[0]).toHaveAttribute('data-has-child-rows', 'true'); expect(onExpandedChange).toHaveBeenCalledTimes(1); // Note that the children of the parent row will still be in the "expanded" array @@ -896,11 +858,7 @@ describe('Tree', () => { await trigger(rows[0], 'Enter'); expect(document.activeElement).toBe(rows[0]); - expect(rows[0]).toHaveAttribute('aria-expanded', 'true'); expect(rows[0]).toHaveAttribute('data-expanded', 'true'); - expect(rows[0]).toHaveAttribute('aria-level', '1'); - expect(rows[0]).toHaveAttribute('aria-posinset', '1'); - expect(rows[0]).toHaveAttribute('aria-setsize', '2'); expect(rows[0]).toHaveAttribute('data-has-child-rows', 'true'); expect(onExpandedChange).toHaveBeenCalledTimes(2); expect(new Set(onExpandedChange.mock.calls[1][0])).toEqual(new Set(['projects', 'project-2', 'project-5', 'reports', 'reports-1', 'reports-1A', 'reports-1AB'])); @@ -910,27 +868,15 @@ describe('Tree', () => { await user.keyboard('{ArrowDown}'); await user.keyboard('{ArrowDown}'); expect(document.activeElement).toBe(rows[2]); - expect(rows[2]).toHaveAttribute('aria-expanded', 'true'); expect(rows[2]).toHaveAttribute('data-expanded', 'true'); - expect(rows[2]).toHaveAttribute('aria-level', '2'); - expect(rows[2]).toHaveAttribute('aria-posinset', '2'); - expect(rows[2]).toHaveAttribute('aria-setsize', '5'); expect(rows[2]).toHaveAttribute('data-has-child-rows', 'true'); // Check we can close a nested row and it doesn't affect the parent await trigger(rows[2], 'ArrowLeft'); expect(document.activeElement).toBe(rows[2]); - expect(rows[2]).toHaveAttribute('aria-expanded', 'false'); expect(rows[2]).not.toHaveAttribute('data-expanded'); - expect(rows[2]).toHaveAttribute('aria-level', '2'); - expect(rows[2]).toHaveAttribute('aria-posinset', '2'); - expect(rows[2]).toHaveAttribute('aria-setsize', '5'); expect(rows[2]).toHaveAttribute('data-has-child-rows', 'true'); - expect(rows[0]).toHaveAttribute('aria-expanded', 'true'); expect(rows[0]).toHaveAttribute('data-expanded', 'true'); - expect(rows[0]).toHaveAttribute('aria-level', '1'); - expect(rows[0]).toHaveAttribute('aria-posinset', '1'); - expect(rows[0]).toHaveAttribute('aria-setsize', '2'); expect(rows[0]).toHaveAttribute('data-has-child-rows', 'true'); expect(onExpandedChange).toHaveBeenCalledTimes(3); expect(new Set(onExpandedChange.mock.calls[2][0])).toEqual(new Set(['projects', 'project-5', 'reports', 'reports-1', 'reports-1A', 'reports-1AB'])); @@ -956,75 +902,6 @@ describe('Tree', () => { expect(rows).toHaveLength(17); }); - it('should not expand/collapse if disabledBehavior is "all" and the row is disabled', async () => { - let {getAllByRole, rerender} = render(); - let rows = getAllByRole('row'); - expect(rows).toHaveLength(20); - - await user.tab(); - // Since first row is disabled, we can't keyboard focus it - expect(document.activeElement).toBe(rows[1]); - expect(rows[0]).toHaveAttribute('aria-expanded', 'true'); - expect(rows[0]).toHaveAttribute('data-expanded', 'true'); - expect(rows[0]).toHaveAttribute('aria-disabled', 'true'); - expect(rows[0]).toHaveAttribute('data-disabled', 'true'); - expect(onExpandedChange).toHaveBeenCalledTimes(0); - - // Try clicking on first row - await trigger(rows[0], 'Space'); - expect(document.activeElement).toBe(rows[1]); - expect(rows[0]).toHaveAttribute('aria-expanded', 'true'); - expect(rows[0]).toHaveAttribute('data-expanded', 'true'); - expect(onExpandedChange).toHaveBeenCalledTimes(0); - - rerender(); - await user.tab(); - rows = getAllByRole('row'); - expect(rows[0]).toHaveAttribute('aria-expanded', 'false'); - expect(rows[0]).not.toHaveAttribute('data-expanded'); - expect(rows[0]).toHaveAttribute('aria-disabled', 'true'); - expect(rows[0]).toHaveAttribute('data-disabled', 'true'); - expect(onExpandedChange).toHaveBeenCalledTimes(0); - - await trigger(rows[0], 'Space'); - expect(rows[0]).toHaveAttribute('aria-expanded', 'false'); - expect(rows[0]).not.toHaveAttribute('data-expanded'); - expect(onExpandedChange).toHaveBeenCalledTimes(0); - }); - - it('should expand/collapse if disabledBehavior is "selection" and the row is disabled', async () => { - let {getAllByRole} = render(); - let rows = getAllByRole('row'); - - await user.tab(); - expect(document.activeElement).toBe(rows[0]); - expect(rows[0]).toHaveAttribute('aria-expanded', 'true'); - expect(rows[0]).toHaveAttribute('data-expanded', 'true'); - expect(onExpandedChange).toHaveBeenCalledTimes(0); - expect(onSelectionChange).toHaveBeenCalledTimes(0); - - // Since selection is enabled, we need to click the chevron even for disabled rows since it is still regarded as the primary action - let chevron = within(rows[0]).getAllByRole('button')[0]; - await trigger(chevron, 'ArrowLeft'); - // TODO: reenable this when we make it so the chevron button isn't focusable via click - // expect(document.activeElement).toBe(rows[0]); - expect(rows[0]).toHaveAttribute('aria-expanded', 'false'); - expect(rows[0]).not.toHaveAttribute('data-expanded'); - expect(onExpandedChange).toHaveBeenCalledTimes(1); - expect(new Set(onExpandedChange.mock.calls[0][0])).toEqual(new Set(['project-2', 'project-5', 'reports', 'reports-1', 'reports-1A', 'reports-1AB'])); - expect(onSelectionChange).toHaveBeenCalledTimes(0); - - await trigger(chevron); - expect(rows[0]).toHaveAttribute('aria-expanded', 'true'); - expect(rows[0]).toHaveAttribute('data-expanded', 'true'); - expect(onExpandedChange).toHaveBeenCalledTimes(2); - expect(new Set(onExpandedChange.mock.calls[1][0])).toEqual(new Set(['projects', 'project-2', 'project-5', 'reports', 'reports-1', 'reports-1A', 'reports-1AB'])); - expect(onSelectionChange).toHaveBeenCalledTimes(0); - - let disabledCheckbox = within(rows[0]).getByRole('checkbox'); - expect(disabledCheckbox).toHaveAttribute('disabled'); - }); - it('should not expand when clicking/using Enter on the row if the row is selectable', async () => { let {getAllByRole} = render(); let rows = getAllByRole('row'); @@ -1119,35 +996,6 @@ describe('Tree', () => { }); }); - it('should support controlled expansion', async () => { - function ControlledTree() { - let [expandedKeys, setExpandedKeys] = React.useState(new Set([])); - - return ( - - ); - } - - let {getAllByRole} = render(); - let rows = getAllByRole('row'); - expect(rows).toHaveLength(2); - - await user.tab(); - expect(document.activeElement).toBe(rows[0]); - expect(rows[0]).toHaveAttribute('aria-expanded', 'false'); - expect(rows[0]).not.toHaveAttribute('data-expanded'); - - await user.click(rows[0]); - rows = getAllByRole('row'); - expect(rows).toHaveLength(7); - - await user.click(rows[0]); - expect(rows[0]).toHaveAttribute('aria-expanded', 'false'); - expect(rows[0]).not.toHaveAttribute('data-expanded'); - rows = getAllByRole('row'); - expect(rows).toHaveLength(2); - }); - it('should apply the proper attributes to the chevron', async () => { let {getAllByRole} = render(); let rows = getAllByRole('row'); @@ -1325,3 +1173,177 @@ describe('Tree', () => { }); }); }); + + +AriaTreeTests({ + prefix: 'rac-static', + renderers: { + standard: () => render( + + Photos + + + + Projects-1A + + + + Projects-2 + + + Projects-3 + + + + + + Homework-1A + + + + Homework-2 + + + Homework-3 + + + + ), + singleSelection: () => render( + + Photos + + + + Projects-1A + + + + Projects-2 + + + Projects-3 + + + + + + Homework-1A + + + + Homework-2 + + + Homework-3 + + + + ), + allInteractionsDisabled: () => render( + + Photos + + + + Projects-1A + + + + Projects-2 + + + Projects-3 + + + + + + Homework-1A + + + + Homework-2 + + + Homework-3 + + + + ) + } +}); + +let controlledRows = [ + {id: 'photos', name: 'Photos 1'}, + {id: 'projects', name: 'Projects', childItems: [ + {id: 'project-1', name: 'Project 1', childItems: [ + {id: 'project-1A', name: 'Project 1A'} + ]}, + {id: 'project-2', name: 'Project 2'}, + {id: 'project-3', name: 'Project 3'} + ]}, + {id: 'reports', name: 'Reports', childItems: [ + {id: 'reports-1', name: 'Reports 1', childItems: [ + {id: 'reports-1A', name: 'Reports 1A'} + ]}, + {id: 'reports-2', name: 'Reports 2'}, + {id: 'reports-3', name: 'Reports 3'} + ]} +]; + +let ControlledDynamicTreeItem = (props) => { + return ( + + + {({isExpanded, hasChildRows, selectionMode, selectionBehavior}) => ( + <> + {(selectionMode !== 'none' || props.href != null) && selectionBehavior === 'toggle' && ( + + )} + {hasChildRows && } + {props.title || props.children} + + + + )} + + + {(item: any) => ( + + {item.name} + + )} + + + ); +}; + +function ControlledDynamicTree(props) { + let [expanded, setExpanded] = React.useState(new Set([])); + + return ( + + {(item: any) => ( + + {item.name} + + )} + + ); +} + +AriaTreeTests({ + prefix: 'rac-controlled-dynamic', + renderers: { + standard: () => render( + + ), + singleSelection: () => render( + + ), + allInteractionsDisabled: () => render( + + ) + } +}); From 64e1b71410c757c6caaa4d978bbd3228602b7138 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Mon, 6 Jan 2025 10:58:49 -0800 Subject: [PATCH 15/19] review comments --- packages/@react-spectrum/table/docs/TableView.mdx | 2 ++ packages/@react-spectrum/tree/test/TreeView.test.tsx | 1 - packages/dev/docs/pages/react-aria/testing.mdx | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/@react-spectrum/table/docs/TableView.mdx b/packages/@react-spectrum/table/docs/TableView.mdx index e4b5613caae..a12ee169ef3 100644 --- a/packages/@react-spectrum/table/docs/TableView.mdx +++ b/packages/@react-spectrum/table/docs/TableView.mdx @@ -1959,6 +1959,8 @@ behaviors in your test suite. Please also refer to [React Spectrum's test suite](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/table/test/Table.test.js) if you find that the above isn't sufficient when resolving issues in your own test cases. +### Test utils + `@react-aria/test-utils` also offers common table interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the table tester and a sample of how you could use it in your test suite. diff --git a/packages/@react-spectrum/tree/test/TreeView.test.tsx b/packages/@react-spectrum/tree/test/TreeView.test.tsx index 0f1cef8f004..5af771e4069 100644 --- a/packages/@react-spectrum/tree/test/TreeView.test.tsx +++ b/packages/@react-spectrum/tree/test/TreeView.test.tsx @@ -1135,7 +1135,6 @@ describe('Tree', () => { }); describe('empty state', () => { - // TODO it('should allow the user to tab to the empty tree', async () => { function renderEmptyState() { return ( diff --git a/packages/dev/docs/pages/react-aria/testing.mdx b/packages/dev/docs/pages/react-aria/testing.mdx index 94347934e37..f4cd10559ff 100644 --- a/packages/dev/docs/pages/react-aria/testing.mdx +++ b/packages/dev/docs/pages/react-aria/testing.mdx @@ -42,7 +42,7 @@ within the component or to verify that the component's state has changed appropr allow the user to easily look up the subcomponents of the component itself, we hope to simplify the overall test writing experience, leading to easier adoption. These test utilities were inspired by various issues and observations that the maintainers of this library and consumers have experienced when writing tests against our components over the years. It is still very much -a work in progress so if you discover any issues or have any feedback please feel free to report them via [GitHub issues](https://github.com/adobe/react-spectrum/issues)! If you have implemented +a work in progress so if you discover any issues or have any feedback, please feel free to report them via [GitHub issues](https://github.com/adobe/react-spectrum/issues)! If you have implemented any testing utilities yourself that you feel would be a good fit, we would be happy to review any pull requests! Please read our [contributing guide](contribute.html) for more information. From 5a9a2650f27fa52b111ff2b8c7e5018ad52bcb93 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Thu, 9 Jan 2025 15:55:08 -0800 Subject: [PATCH 16/19] small fixes from review --- packages/@react-spectrum/combobox/docs/ComboBox.mdx | 3 ++- packages/@react-spectrum/list/docs/ListView.mdx | 3 ++- packages/@react-spectrum/listbox/docs/ListBox.mdx | 3 ++- packages/@react-spectrum/menu/docs/MenuTrigger.mdx | 3 ++- packages/@react-spectrum/picker/docs/Picker.mdx | 3 ++- packages/@react-spectrum/table/docs/TableView.mdx | 3 ++- packages/@react-spectrum/tabs/docs/Tabs.mdx | 3 ++- packages/@react-spectrum/tree/docs/TreeView.mdx | 3 ++- packages/dev/docs/pages/react-aria/testing.mdx | 1 + packages/react-aria-components/docs/ComboBox.mdx | 3 ++- packages/react-aria-components/docs/GridList.mdx | 3 ++- packages/react-aria-components/docs/ListBox.mdx | 3 ++- packages/react-aria-components/docs/Menu.mdx | 3 ++- packages/react-aria-components/docs/Select.mdx | 3 ++- packages/react-aria-components/docs/Table.mdx | 3 ++- packages/react-aria-components/docs/Tabs.mdx | 3 ++- packages/react-aria-components/docs/Tree.mdx | 3 ++- packages/react-aria-components/test/Tree.test.tsx | 1 + 18 files changed, 34 insertions(+), 16 deletions(-) diff --git a/packages/@react-spectrum/combobox/docs/ComboBox.mdx b/packages/@react-spectrum/combobox/docs/ComboBox.mdx index 063a1b4dc42..3b5db5c92b3 100644 --- a/packages/@react-spectrum/combobox/docs/ComboBox.mdx +++ b/packages/@react-spectrum/combobox/docs/ComboBox.mdx @@ -996,13 +996,14 @@ isn't sufficient when resolving issues in your own test cases. ### Test utils -`@react-aria/test-utils` also offers common combobox interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +`@react-aria/test-utils` offers common combobox interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the combobox tester and a sample of how you could use it in your test suite. ```ts // Combobox.test.ts +import {render, within} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({interactionType: 'mouse'}); diff --git a/packages/@react-spectrum/list/docs/ListView.mdx b/packages/@react-spectrum/list/docs/ListView.mdx index bac1dc9fc48..f514e83baff 100644 --- a/packages/@react-spectrum/list/docs/ListView.mdx +++ b/packages/@react-spectrum/list/docs/ListView.mdx @@ -1195,13 +1195,14 @@ isn't sufficient when resolving issues in your own test cases. ### Test utils -`@react-aria/test-utils` also offers common gridlist interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +`@react-aria/test-utils` offers common gridlist interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the gridlist tester and a sample of how you could use it in your test suite. ```ts // ListView.test.ts +import {render, within} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({interactionType: 'mouse'}); diff --git a/packages/@react-spectrum/listbox/docs/ListBox.mdx b/packages/@react-spectrum/listbox/docs/ListBox.mdx index c71320486e0..f4c13dcacfd 100644 --- a/packages/@react-spectrum/listbox/docs/ListBox.mdx +++ b/packages/@react-spectrum/listbox/docs/ListBox.mdx @@ -413,13 +413,14 @@ isn't sufficient when resolving issues in your own test cases. ### Test utils -`@react-aria/test-utils` also offers common listbox interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +`@react-aria/test-utils` offers common listbox interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the listbox tester and a sample of how you could use it in your test suite. ```ts // ListBox.test.ts +import {render} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({interactionType: 'mouse'}); diff --git a/packages/@react-spectrum/menu/docs/MenuTrigger.mdx b/packages/@react-spectrum/menu/docs/MenuTrigger.mdx index ff48dfc9ffc..726c8c116a8 100644 --- a/packages/@react-spectrum/menu/docs/MenuTrigger.mdx +++ b/packages/@react-spectrum/menu/docs/MenuTrigger.mdx @@ -260,13 +260,14 @@ isn't sufficient when resolving issues in your own test cases. ### Test utils -`@react-aria/test-utils` also offers common menu interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +`@react-aria/test-utils` offers common menu interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the menu tester and a sample of how you could use it in your test suite. ```ts // Menu.test.ts +import {render} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({interactionType: 'mouse'}); diff --git a/packages/@react-spectrum/picker/docs/Picker.mdx b/packages/@react-spectrum/picker/docs/Picker.mdx index 94a633496de..d413ffdc42b 100644 --- a/packages/@react-spectrum/picker/docs/Picker.mdx +++ b/packages/@react-spectrum/picker/docs/Picker.mdx @@ -592,13 +592,14 @@ isn't sufficient when resolving issues in your own test cases. ### Test utils -`@react-aria/test-utils` also offers common select interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +`@react-aria/test-utils` offers common select interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the select tester and a sample of how you could use it in your test suite. ```ts // Picker.test.ts +import {render} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({interactionType: 'mouse'}); diff --git a/packages/@react-spectrum/table/docs/TableView.mdx b/packages/@react-spectrum/table/docs/TableView.mdx index a12ee169ef3..b1c272c04a9 100644 --- a/packages/@react-spectrum/table/docs/TableView.mdx +++ b/packages/@react-spectrum/table/docs/TableView.mdx @@ -1961,13 +1961,14 @@ isn't sufficient when resolving issues in your own test cases. ### Test utils -`@react-aria/test-utils` also offers common table interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +`@react-aria/test-utils` offers common table interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the table tester and a sample of how you could use it in your test suite. ```ts // TableView.test.ts +import {render, within} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({interactionType: 'mouse', advanceTimer: jest.advanceTimersByTime}); diff --git a/packages/@react-spectrum/tabs/docs/Tabs.mdx b/packages/@react-spectrum/tabs/docs/Tabs.mdx index a89ae81377e..58e3d2342c5 100644 --- a/packages/@react-spectrum/tabs/docs/Tabs.mdx +++ b/packages/@react-spectrum/tabs/docs/Tabs.mdx @@ -639,13 +639,14 @@ Tabs features automatic tab collapse behavior and may need specific mocks to tes [React Spectrum's test suite](https://github.com/adobe/react-spectrum/blob/326f48154e301edab425c8198c5c3af72422462b/packages/%40react-spectrum/tabs/test/Tabs.test.js#L58-L62) if you run into any issues with your tests. -`@react-aria/test-utils` also offers common tabs interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +`@react-aria/test-utils` offers common tabs interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the tabs tester and a sample of how you could use it in your test suite. ```ts // Tabs.test.ts +import {render} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({interactionType: 'mouse'}); diff --git a/packages/@react-spectrum/tree/docs/TreeView.mdx b/packages/@react-spectrum/tree/docs/TreeView.mdx index dfb3dc4d25f..915be52f58d 100644 --- a/packages/@react-spectrum/tree/docs/TreeView.mdx +++ b/packages/@react-spectrum/tree/docs/TreeView.mdx @@ -460,13 +460,14 @@ isn't sufficient when resolving issues in your own test cases. ### Test utils -`@react-aria/test-utils` also offers common tree interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +`@react-aria/test-utils` offers common tree interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the tree tester and a sample of how you could use it in your test suite. ```ts // Tree.test.ts +import {render, within} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({interactionType: 'mouse'}); diff --git a/packages/dev/docs/pages/react-aria/testing.mdx b/packages/dev/docs/pages/react-aria/testing.mdx index f4cd10559ff..53432ef4d23 100644 --- a/packages/dev/docs/pages/react-aria/testing.mdx +++ b/packages/dev/docs/pages/react-aria/testing.mdx @@ -69,6 +69,7 @@ pattern, typically including the `root` element (e.g. the menu trigger button, t ```ts // YourTest.test.ts +import {screen} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; // Provide whatever method of advancing timers you use in your test, this example assumes Jest with fake timers diff --git a/packages/react-aria-components/docs/ComboBox.mdx b/packages/react-aria-components/docs/ComboBox.mdx index eaa5500a8a4..7249062d960 100644 --- a/packages/react-aria-components/docs/ComboBox.mdx +++ b/packages/react-aria-components/docs/ComboBox.mdx @@ -1480,13 +1480,14 @@ If you need to customize things even further, such as accessing internal state, ### Test utils -`@react-aria/test-utils` also offers common combobox interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities +`@react-aria/test-utils` offers common combobox interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the combobox tester and a sample of how you could use it in your test suite. ```ts // Combobox.test.ts +import {render} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({interactionType: 'mouse'}); diff --git a/packages/react-aria-components/docs/GridList.mdx b/packages/react-aria-components/docs/GridList.mdx index efd8a5ecde5..083e0684dfc 100644 --- a/packages/react-aria-components/docs/GridList.mdx +++ b/packages/react-aria-components/docs/GridList.mdx @@ -1871,13 +1871,14 @@ If you need to customize things even further, such as accessing internal state, ### Test utils -`@react-aria/test-utils` also offers common gridlist interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities +`@react-aria/test-utils` offers common gridlist interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the gridlist tester and a sample of how you could use it in your test suite. ```ts // GridList.test.ts +import {render, within} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({interactionType: 'mouse'}); diff --git a/packages/react-aria-components/docs/ListBox.mdx b/packages/react-aria-components/docs/ListBox.mdx index 9ed612c9bc0..d05c47dba03 100644 --- a/packages/react-aria-components/docs/ListBox.mdx +++ b/packages/react-aria-components/docs/ListBox.mdx @@ -2095,13 +2095,14 @@ If you need to customize things even further, such as accessing internal state o ### Test utils -`@react-aria/test-utils` also offers common listbox interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities +`@react-aria/test-utils` offers common listbox interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the listbox tester and a sample of how you could use it in your test suite. ```ts // ListBox.test.ts +import {render} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({interactionType: 'mouse'}); diff --git a/packages/react-aria-components/docs/Menu.mdx b/packages/react-aria-components/docs/Menu.mdx index d38451fc078..c532a9557af 100644 --- a/packages/react-aria-components/docs/Menu.mdx +++ b/packages/react-aria-components/docs/Menu.mdx @@ -1145,13 +1145,14 @@ By providing the above contexts, the existing `Button`, `Popover`, and `Menu` co ### Test utils -`@react-aria/test-utils` also offers common menu interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities +`@react-aria/test-utils` offers common menu interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the menu tester and a sample of how you could use it in your test suite. ```ts // Menu.test.ts +import {render} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({interactionType: 'mouse'}); diff --git a/packages/react-aria-components/docs/Select.mdx b/packages/react-aria-components/docs/Select.mdx index f61aec40cfe..e2d3ec78664 100644 --- a/packages/react-aria-components/docs/Select.mdx +++ b/packages/react-aria-components/docs/Select.mdx @@ -1243,13 +1243,14 @@ from your automated accessibility tests as shown [here](./accessibility.html#fal ### Test utils -`@react-aria/test-utils` also offers common select interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities +`@react-aria/test-utils` offers common select interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the select tester and a sample of how you could use it in your test suite. ```ts // Select.test.ts +import {render} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({interactionType: 'mouse'}); diff --git a/packages/react-aria-components/docs/Table.mdx b/packages/react-aria-components/docs/Table.mdx index 2f235606d62..f48fab563cd 100644 --- a/packages/react-aria-components/docs/Table.mdx +++ b/packages/react-aria-components/docs/Table.mdx @@ -2481,13 +2481,14 @@ If you need to customize things even further, such as accessing internal state o ### Test utils -`@react-aria/test-utils` also offers common table interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities +`@react-aria/test-utils` offers common table interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the table tester and a sample of how you could use it in your test suite. ```ts // Table.test.ts +import {render, within} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({interactionType: 'mouse', advanceTimer: jest.advanceTimersByTime}); diff --git a/packages/react-aria-components/docs/Tabs.mdx b/packages/react-aria-components/docs/Tabs.mdx index 2125f9dd21b..63760d341e2 100644 --- a/packages/react-aria-components/docs/Tabs.mdx +++ b/packages/react-aria-components/docs/Tabs.mdx @@ -757,13 +757,14 @@ If you need to customize things even further, such as accessing internal state o ### Test utils -`@react-aria/test-utils` also offers common tabs interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +`@react-aria/test-utils` offers common tabs interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the tabs tester and a sample of how you could use it in your test suite. ```ts // Tabs.test.ts +import {render} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({interactionType: 'mouse'}); diff --git a/packages/react-aria-components/docs/Tree.mdx b/packages/react-aria-components/docs/Tree.mdx index f120c76a056..c42effc2f5d 100644 --- a/packages/react-aria-components/docs/Tree.mdx +++ b/packages/react-aria-components/docs/Tree.mdx @@ -326,13 +326,14 @@ TreeItem also exposes a `--tree-item-level` CSS custom property, which you can u ### Test utils -`@react-aria/test-utils` also offers common tree interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +`@react-aria/test-utils` offers common tree interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the tree tester and a sample of how you could use it in your test suite. ```ts // Tree.test.ts +import {render, within} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({interactionType: 'mouse'}); diff --git a/packages/react-aria-components/test/Tree.test.tsx b/packages/react-aria-components/test/Tree.test.tsx index e62a810b70a..b3c6df16214 100644 --- a/packages/react-aria-components/test/Tree.test.tsx +++ b/packages/react-aria-components/test/Tree.test.tsx @@ -191,6 +191,7 @@ describe('Tree', () => { it('should have the base set of data attributes', () => { let {getByRole, getAllByRole} = render(); let tree = getByRole('treegrid'); + expect(tree).toHaveAttribute('data-rac'); expect(tree).not.toHaveAttribute('data-empty'); expect(tree).not.toHaveAttribute('data-focused'); expect(tree).not.toHaveAttribute('data-focus-visible'); From 17c36d9a1ed9dbc38e036ba8e5d37ea4d7038d84 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Thu, 9 Jan 2025 17:12:33 -0800 Subject: [PATCH 17/19] update testing pages to be more standalone as per review --- .../combobox/docs/ComboBox.mdx | 17 +- .../@react-spectrum/list/docs/ListView.mdx | 17 +- .../@react-spectrum/listbox/docs/ListBox.mdx | 17 +- .../@react-spectrum/menu/docs/MenuTrigger.mdx | 17 +- .../@react-spectrum/picker/docs/Picker.mdx | 17 +- .../@react-spectrum/table/docs/TableView.mdx | 17 +- .../@react-spectrum/table/test/Table.test.js | 3 +- packages/@react-spectrum/tabs/docs/Tabs.mdx | 17 +- .../@react-spectrum/tree/docs/TreeView.mdx | 17 +- .../dev/docs/pages/react-aria/testing.mdx | 185 +++++++++++++++--- .../dev/docs/pages/react-spectrum/testing.mdx | 79 +++++++- .../react-aria-components/docs/ComboBox.mdx | 4 +- .../react-aria-components/docs/GridList.mdx | 4 +- .../react-aria-components/docs/ListBox.mdx | 4 +- packages/react-aria-components/docs/Menu.mdx | 4 +- .../react-aria-components/docs/Select.mdx | 4 +- packages/react-aria-components/docs/Table.mdx | 4 +- packages/react-aria-components/docs/Tabs.mdx | 4 +- packages/react-aria-components/docs/Tree.mdx | 4 +- 19 files changed, 321 insertions(+), 114 deletions(-) diff --git a/packages/@react-spectrum/combobox/docs/ComboBox.mdx b/packages/@react-spectrum/combobox/docs/ComboBox.mdx index 3b5db5c92b3..b4000f4de81 100644 --- a/packages/@react-spectrum/combobox/docs/ComboBox.mdx +++ b/packages/@react-spectrum/combobox/docs/ComboBox.mdx @@ -996,15 +996,14 @@ isn't sufficient when resolving issues in your own test cases. ### Test utils -`@react-aria/test-utils` offers common combobox interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +`@react-spectrum/test-utils` offers common combobox interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the combobox tester and a sample of how you could use it in your test suite. - - ```ts // Combobox.test.ts import {render, within} from '@testing-library/react'; -import {User} from '@react-aria/test-utils'; +import {theme} from '@react-spectrum/theme-default'; +import {User} from '@react-spectrum/test-utils'; let testUtilUser = new User({interactionType: 'mouse'}); // ... @@ -1012,9 +1011,11 @@ let testUtilUser = new User({interactionType: 'mouse'}); it('ComboBox can select an option via keyboard', async function () { // Render your test component/app and initialize the combobox tester let {getByTestId} = render( - - ... - + + + ... + + ); let comboboxTester = testUtilUser.createTester('ComboBox', {root: getByTestId('test-combobox'), interactionType: 'keyboard'}); @@ -1027,3 +1028,5 @@ it('ComboBox can select an option via keyboard', async function () { expect(comboboxTester.listbox).not.toBeInTheDocument(); }); ``` + + diff --git a/packages/@react-spectrum/list/docs/ListView.mdx b/packages/@react-spectrum/list/docs/ListView.mdx index f514e83baff..e00157a6b88 100644 --- a/packages/@react-spectrum/list/docs/ListView.mdx +++ b/packages/@react-spectrum/list/docs/ListView.mdx @@ -1195,15 +1195,14 @@ isn't sufficient when resolving issues in your own test cases. ### Test utils -`@react-aria/test-utils` offers common gridlist interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +`@react-spectrum/test-utils` offers common gridlist interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the gridlist tester and a sample of how you could use it in your test suite. - - ```ts // ListView.test.ts import {render, within} from '@testing-library/react'; -import {User} from '@react-aria/test-utils'; +import {theme} from '@react-spectrum/theme-default'; +import {User} from '@react-spectrum/test-utils'; let testUtilUser = new User({interactionType: 'mouse'}); // ... @@ -1211,9 +1210,11 @@ let testUtilUser = new User({interactionType: 'mouse'}); it('ListView can select a row via keyboard', async function () { // Render your test component/app and initialize the gridlist tester let {getByTestId} = render( - - ... - + + + ... + + ); let gridlistTester = testUtilUser.createTester('GridList', {root: getByTestId('test-gridlist'), interactionType: 'keyboard'}); @@ -1233,3 +1234,5 @@ it('ListView can select a row via keyboard', async function () { expect(gridListTester.selectedRows).toHaveLength(0); }); ``` + + diff --git a/packages/@react-spectrum/listbox/docs/ListBox.mdx b/packages/@react-spectrum/listbox/docs/ListBox.mdx index f4c13dcacfd..d0d9cc9ca84 100644 --- a/packages/@react-spectrum/listbox/docs/ListBox.mdx +++ b/packages/@react-spectrum/listbox/docs/ListBox.mdx @@ -413,15 +413,14 @@ isn't sufficient when resolving issues in your own test cases. ### Test utils -`@react-aria/test-utils` offers common listbox interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +`@react-spectrum/test-utils` offers common listbox interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the listbox tester and a sample of how you could use it in your test suite. - - ```ts // ListBox.test.ts import {render} from '@testing-library/react'; -import {User} from '@react-aria/test-utils'; +import {theme} from '@react-spectrum/theme-default'; +import {User} from '@react-spectrum/test-utils'; let testUtilUser = new User({interactionType: 'mouse'}); // ... @@ -429,9 +428,11 @@ let testUtilUser = new User({interactionType: 'mouse'}); it('ListBox can select an option via keyboard', async function () { // Render your test component/app and initialize the listbox tester let {getByTestId} = render( - - ... - + + + ... + + ); let listboxTester = testUtilUser.createTester('ListBox', {root: getByTestId('test-listbox'), interactionType: 'keyboard'}); @@ -440,3 +441,5 @@ it('ListBox can select an option via keyboard', async function () { expect(onSelectionChange).toBeCalledTimes(1); }); ``` + + diff --git a/packages/@react-spectrum/menu/docs/MenuTrigger.mdx b/packages/@react-spectrum/menu/docs/MenuTrigger.mdx index 726c8c116a8..20561f8a320 100644 --- a/packages/@react-spectrum/menu/docs/MenuTrigger.mdx +++ b/packages/@react-spectrum/menu/docs/MenuTrigger.mdx @@ -260,15 +260,14 @@ isn't sufficient when resolving issues in your own test cases. ### Test utils -`@react-aria/test-utils` offers common menu interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +`@react-spectrum/test-utils` offers common menu interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the menu tester and a sample of how you could use it in your test suite. - - ```ts // Menu.test.ts import {render} from '@testing-library/react'; -import {User} from '@react-aria/test-utils'; +import {theme} from '@react-spectrum/theme-default'; +import {User} from '@react-spectrum/test-utils'; let testUtilUser = new User({interactionType: 'mouse'}); // ... @@ -276,9 +275,11 @@ let testUtilUser = new User({interactionType: 'mouse'}); it('Menu can open its submenu via keyboard', async function () { // Render your test component/app and initialize the menu tester let {getByTestId} = render( - - ... - + + + ... + + ); let menuTester = testUtilUser.createTester('Menu', {root: getByTestId('test-menutrigger'), interactionType: 'keyboard'}); @@ -295,3 +296,5 @@ it('Menu can open its submenu via keyboard', async function () { expect(menuTester.menu).toBeInTheDocument(); }); ``` + + diff --git a/packages/@react-spectrum/picker/docs/Picker.mdx b/packages/@react-spectrum/picker/docs/Picker.mdx index d413ffdc42b..def4ca92a77 100644 --- a/packages/@react-spectrum/picker/docs/Picker.mdx +++ b/packages/@react-spectrum/picker/docs/Picker.mdx @@ -592,15 +592,14 @@ isn't sufficient when resolving issues in your own test cases. ### Test utils -`@react-aria/test-utils` offers common select interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +`@react-spectrum/test-utils` offers common select interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the select tester and a sample of how you could use it in your test suite. - - ```ts // Picker.test.ts import {render} from '@testing-library/react'; -import {User} from '@react-aria/test-utils'; +import {theme} from '@react-spectrum/theme-default'; +import {User} from '@react-spectrum/test-utils'; let testUtilUser = new User({interactionType: 'mouse'}); // ... @@ -608,9 +607,11 @@ let testUtilUser = new User({interactionType: 'mouse'}); it('Picker can select an option via keyboard', async function () { // Render your test component/app and initialize the select tester let {getByTestId} = render( - - ... - + + + ... + + ); let selectTester = testUtilUser.createTester('Select', {root: getByTestId('test-select'), interactionType: 'keyboard'}); let trigger = selectTester.trigger; @@ -621,3 +622,5 @@ it('Picker can select an option via keyboard', async function () { expect(trigger).toHaveTextContent('Cat'); }); ``` + + diff --git a/packages/@react-spectrum/table/docs/TableView.mdx b/packages/@react-spectrum/table/docs/TableView.mdx index b1c272c04a9..00d055a0b4a 100644 --- a/packages/@react-spectrum/table/docs/TableView.mdx +++ b/packages/@react-spectrum/table/docs/TableView.mdx @@ -1961,15 +1961,14 @@ isn't sufficient when resolving issues in your own test cases. ### Test utils -`@react-aria/test-utils` offers common table interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +`@react-spectrum/test-utils` offers common table interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the table tester and a sample of how you could use it in your test suite. - - ```ts // TableView.test.ts import {render, within} from '@testing-library/react'; -import {User} from '@react-aria/test-utils'; +import {theme} from '@react-spectrum/theme-default'; +import {User} from '@react-spectrum/test-utils'; let testUtilUser = new User({interactionType: 'mouse', advanceTimer: jest.advanceTimersByTime}); // ... @@ -1977,9 +1976,11 @@ let testUtilUser = new User({interactionType: 'mouse', advanceTimer: jest.advanc it('TableView can toggle row selection', async function () { // Render your test component/app and initialize the table tester let {getByTestId} = render( - - ... - + + + ... + + ); let tableTester = testUtilUser.createTester('Table', {root: getByTestId('test-table')}); expect(tableTester.selectedRows).toHaveLength(0); @@ -2000,3 +2001,5 @@ it('TableView can toggle row selection', async function () { expect(tableTester.selectedRows).toHaveLength(0); }); ``` + + diff --git a/packages/@react-spectrum/table/test/Table.test.js b/packages/@react-spectrum/table/test/Table.test.js index 757732b5fe7..d65fec14d72 100644 --- a/packages/@react-spectrum/table/test/Table.test.js +++ b/packages/@react-spectrum/table/test/Table.test.js @@ -12,7 +12,7 @@ jest.mock('@react-aria/live-announcer'); jest.mock('@react-aria/utils/src/scrollIntoView'); -import {act, fireEvent, installPointerEvent, mockClickDefault, pointerMap, render as renderComponent, within} from '@react-spectrum/test-utils-internal'; +import {act, fireEvent, installPointerEvent, mockClickDefault, pointerMap, render as renderComponent, User, within} from '@react-spectrum/test-utils-internal'; import {ActionButton, Button} from '@react-spectrum/button'; import Add from '@spectrum-icons/workflow/Add'; import {announce} from '@react-aria/live-announcer'; @@ -35,7 +35,6 @@ import * as stories from '../stories/Table.stories'; import {Switch} from '@react-spectrum/switch'; import {TextField} from '@react-spectrum/textfield'; import {theme} from '@react-spectrum/theme-default'; -import {User} from '@react-aria/test-utils'; import userEvent from '@testing-library/user-event'; let { diff --git a/packages/@react-spectrum/tabs/docs/Tabs.mdx b/packages/@react-spectrum/tabs/docs/Tabs.mdx index 58e3d2342c5..fac61e4b15b 100644 --- a/packages/@react-spectrum/tabs/docs/Tabs.mdx +++ b/packages/@react-spectrum/tabs/docs/Tabs.mdx @@ -639,15 +639,14 @@ Tabs features automatic tab collapse behavior and may need specific mocks to tes [React Spectrum's test suite](https://github.com/adobe/react-spectrum/blob/326f48154e301edab425c8198c5c3af72422462b/packages/%40react-spectrum/tabs/test/Tabs.test.js#L58-L62) if you run into any issues with your tests. -`@react-aria/test-utils` offers common tabs interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +`@react-spectrum/test-utils` offers common tabs interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the tabs tester and a sample of how you could use it in your test suite. - - ```ts // Tabs.test.ts import {render} from '@testing-library/react'; -import {User} from '@react-aria/test-utils'; +import {theme} from '@react-spectrum/theme-default'; +import {User} from '@react-spectrum/test-utils'; let testUtilUser = new User({interactionType: 'mouse'}); // ... @@ -655,9 +654,11 @@ let testUtilUser = new User({interactionType: 'mouse'}); it('Tabs can change selection via keyboard', async function () { // Render your test component/app and initialize the listbox tester let {getByTestId} = render( - - ... - + + + ... + + ); let tabsTester = testUtilUser.createTester('Tabs', {root: getByTestId('test-tabs'), interactionType: 'keyboard'}); @@ -669,3 +670,5 @@ it('Tabs can change selection via keyboard', async function () { expect(tabsTester.selectedTab).toBe(tabs[1]); }); ``` + + diff --git a/packages/@react-spectrum/tree/docs/TreeView.mdx b/packages/@react-spectrum/tree/docs/TreeView.mdx index 915be52f58d..c160dfac825 100644 --- a/packages/@react-spectrum/tree/docs/TreeView.mdx +++ b/packages/@react-spectrum/tree/docs/TreeView.mdx @@ -460,15 +460,14 @@ isn't sufficient when resolving issues in your own test cases. ### Test utils -`@react-aria/test-utils` offers common tree interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities +`@react-spectrum/test-utils` offers common tree interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the tree tester and a sample of how you could use it in your test suite. - - ```ts // Tree.test.ts import {render, within} from '@testing-library/react'; -import {User} from '@react-aria/test-utils'; +import {theme} from '@react-spectrum/theme-default'; +import {User} from '@react-spectrum/test-utils'; let testUtilUser = new User({interactionType: 'mouse'}); // ... @@ -476,9 +475,11 @@ let testUtilUser = new User({interactionType: 'mouse'}); it('TreeView can select a row via keyboard', async function () { // Render your test component/app and initialize the Tree tester let {getByTestId} = render( - - ... - + + + ... + + ); let treeTester = testUtilUser.createTester('Tree', {root: getByTestId('test-tree'), interactionType: 'keyboard'}); @@ -495,3 +496,5 @@ it('TreeView can select a row via keyboard', async function () { expect(within(treeTester.rows[0]).getByRole('checkbox')).not.toBeChecked(); }); ``` + + diff --git a/packages/dev/docs/pages/react-aria/testing.mdx b/packages/dev/docs/pages/react-aria/testing.mdx index 53432ef4d23..9a3cd1949f8 100644 --- a/packages/dev/docs/pages/react-aria/testing.mdx +++ b/packages/dev/docs/pages/react-aria/testing.mdx @@ -26,25 +26,146 @@ category: Concepts This page describes how to test an application built with React Aria. It documents the available testing utilities available for each aria pattern and how they can be used to simulate common user interactions. -## React Aria test utils +## Introduction -### Introduction +Running automated tests on your application helps ensure that it continues to work as expected over time. +You can use testing tools like [Enzyme](https://enzymejs.github.io/enzyme/) or +[React Testing Library](https://testing-library.com/docs/react-testing-library/intro) along with test runners like +[Jest](https://jestjs.io) or [Mocha](https://mochajs.org) to test applications built with React Aria Components or hooks. These generally +work quite well out of the box but there are a few things to consider to ensure your tests are the best they +can be. -As both the adoption of our component libraries and the complexity of the components offered has grown, various testing pain points have surfaced, both from within the maintaining team and from consumers of the library itself. -The test writer may not be familiar with the internal structure of the component they are testing against and thus are unable to easily target/interact with the desired element within the component. Alternatively, the specifics -of what events to simulate for various interaction modes can be onerous to figure out and adds unnecessary friction for new adopters. +The information below covers best practices when writing tests, and be sure to checkout our [test utils](./react-aria-test-utils) that incorporate these +strategies under the hood, helping streamline the test writing practice for you. -To address this, we've created [@react-aria/test-utils](https://www.npmjs.com/package/@react-aria/test-utils) which features a set of testing utilities that aims to make writing unit tests easier for consumers of our component libraries -or for users who have built their own components following the respective ARIA pattern specification. By using the ARIA specification for any given component pattern as a source of truth, -we can make assumptions about the existence of specific aria attributes that allow us to navigate the component's DOM structure. Similarly, we can also expect that the component -permits specific interaction patterns described by the ARIA pattern specification and thus accurately simulate those interactions, using the aforementioned aria attributes to target the proper node -within the component or to verify that the component's state has changed appropriately post-interaction. By providing utilities to simulate these standard interactions and getters that -allow the user to easily look up the subcomponents of the component itself, we hope to simplify the overall test writing experience, leading to easier adoption. +## Testing semantics + +Many testing libraries expect you to query for elements in the DOM tree. For example, you might have a test +that renders a login page, finds the username and password fields, and simulates filling them out and +submitting the form. + +The recommended way to query for React Aria Components and their internals is by semantics. React Aria +Components implement [ARIA patterns](https://www.w3.org/TR/wai-aria-practices-1.2/). ARIA is a W3C standard +that specifies the semantics for many UI components. This is used to expose them to assistive technology +such as screen readers, but can also be used in tests to operate the application programmatically. These semantics +are much less likely to change over time, and while other DOM nodes may be added or removed, the semantics are more likely to stay stable. + +The main attribute to look for when querying is the [role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques). +This attribute represents the type of element a DOM node represents, e.g. a button, list option, or tab. + +### React Testing Library + +[React Testing Library](https://testing-library.com/docs/react-testing-library/intro) is useful because it +enforces that you write tests using semantics instead of implementation details. We use React Testing Library +to test React Aria itself, and it's quite easy to [query](https://testing-library.com/docs/dom-testing-library/api-queries) +elements by role, text, label, etc. + +```tsx +import {render} from '@testing-library/react'; + +let tree = render(); +let option = tree.getByRole('button'); +``` + +### Enzyme + +In Enzyme, you can [use CSS selectors](https://enzymejs.github.io/enzyme/docs/api/ReactWrapper/find.html) to find +elements. You can query by role using an attribute selector: + +```tsx +import {mount} from 'enzyme'; + +let wrapper = mount(); +let option = wrapper.find('[role="option"]'); +``` + +## Test ids + +Querying by semantics covers many scenarios, but what if you have many buttons on a page? How do you find the specific button +you're looking for in a test? In many cases this could be done by querying by the text in the button or an +accessibility label associated with it, but sometimes this might change over time or may be affected by things like +translations in different languages. In these cases, you may need a way to identify specific elements in tests, and that's +where test ids come in. + +React Aria Components pass all [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes) +through to their underlying DOM nodes, which allows you to use an attribute like `data-testid` to identify +a particular instance of a component. For example, you could add test ids to the two input elements +in a login form and use them to find the username and password fields. + +This example uses React Testing Library, but the idea could be applied in a similar way with Enzyme or other +testing libraries. + +```tsx +import {render} from '@testing-library/react'; +import {Input, Label, TextField} from 'react-aria-components'; + +function LoginForm() { + return ( + <> + + + + + + + + + + ); +} + +let tree = render(); +let username = tree.getByTestId('username'); +let password = tree.getByTestId('password'); +``` -These test utilities were inspired by various issues and observations that the maintainers of this library and consumers have experienced when writing tests against our components over the years. It is still very much -a work in progress so if you discover any issues or have any feedback, please feel free to report them via [GitHub issues](https://github.com/adobe/react-spectrum/issues)! If you have implemented -any testing utilities yourself that you feel would be a good fit, we would be happy to review any pull requests! Please read our [contributing guide](contribute.html) -for more information. +## Triggering events + +Most testing libraries include a way to simulate events on an element. React Aria Components rely on +many different browser events to support different devices and platforms, so it's important to simulate +these correctly in your tests. For example, rather than only simulating a click event, the tests should +simulate all of the events that would occur if a real user were interacting with the component. + +For example, a click is really a `mousemove` and `mouseover` the target, followed +by `mousedown`, `focus`, and `mouseup` events, and finally a `click` event. If you only simulated the `click` +event, you would be missing all of these other preceding events that occur in real-world situations and this +may make your test not work correctly. The implementation of the component may also change in the future to +expect these events, making your test brittle. In addition, browsers have default behavior that occurs on +certain events which would be missing, like focusing elements on mouse down, and toggling checkboxes on click. + +The best way to handle this is with the [user-event](https://github.com/testing-library/user-event) library. +This lets you trigger high level interactions like a user would, and the library handles firing all of the individual +events that make up that interaction. If you use this library rather than firing events manually, your tests +will simulate real-world behavior much better and work as expected. + +user-event can handle many types of interactions, e.g. clicks, tabbing, typing, etc. This example shows how you could +use it to render a login form and enter text in each field and click the submit button, just as a real user would. + +```tsx +import {render} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +let tree = render(); + +// Click on the username field to focus it, and enter the value. +userEvent.click(tree.getByLabelText('Username')); +userEvent.type(document.activeElement, 'devon'); + +// Tab to the password field, and enter the value. +userEvent.tab(); +userEvent.type(document.activeElement, 'Pas$w0rd'); + +// Tab to the submit button and click it. +userEvent.tab(); +userEvent.click(document.activeElement); +``` + +## React Aria test utils + +[@react-aria/test-utils](https://www.npmjs.com/package/@react-aria/test-utils) is a set of testing utilities that aims to make writing unit tests easier for consumers of React Aria +or for users who have built their own components following the respective ARIA pattern specification. By using the ARIA specification for any given component pattern as a source of truth, +we can make assumptions about the existence of various aria attributes in a component. This allows us to navigate the component's DOM structure, simulate common interactions, and verify the +the resulting state of the component. ### Installation @@ -59,27 +180,27 @@ to be on React 18+ in order for these utilities to work. ### Setup -Once installed, you can access the `User` that `@react-aria/test-utils` provides in your test file as shown below. This user only needs to be initialized once and accepts two options: `interactionType` and `advanceTimer`. `interactionType` will -initialize what mode of interaction (mouse, keyboard, or touch) will be used by default. This can be overridden at the pattern tester or interaction execution level if required. `advanceTimer` accepts a function that when called should advance timers (fake timers) -or wait for the required time (real timers). This is required for certain interactions (e.g. long press) that some of the patterns support. - -Once the `User` is initialized, you can use its `createTester` method to initialize a specific ARIA pattern tester in your test cases. This gives you access to that pattern's specific utilities that you can then call -within your test to query for specific subcomponents or simulate common interactions. `createTester` requires two arguments, the first being the name of the ARIA pattern tester you are creating and the second being a set of initialization options specific to that -pattern, typically including the `root` element (e.g. the menu trigger button, table, etc). See [below](#patterns) for more details on what is supported for each individual ARIA pattern tester. +Once installed, you can access the `User` that `@react-aria/test-utils` provides in your test file as shown below. This user only needs to be initialized once and then can be used to generate +specific ARIA pattern tester in your test cases. This gives you access to that pattern's specific utilities that you can then call within your test to query for specific subcomponents or simulate common interactions. +See [below](#patterns) for what patterns are currently supported. ```ts // YourTest.test.ts import {screen} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; -// Provide whatever method of advancing timers you use in your test, this example assumes Jest with fake timers +// Provide whatever method of advancing timers you use in your test, this example assumes Jest with fake timers. +// 'interactionType' specifies what mode of interaction should be simulated by the tester +// 'advanceTimer' is used by the tester to advance the timers in the tests for specific interactions (e.g. long press) let testUtilUser = new User({interactionType: 'mouse', advanceTimer: jest.advanceTimersByTime}); // ... it('my test case', async function () { - // Render your test component/app and initialize the table tester + // Render your test component/app render(); + // Initialize the table tester via providing the 'Table' pattern name and the root element of said table let table = testUtilUser.createTester('Table', {root: screen.getByTestId('test_table')}); + // ... }); ``` @@ -93,18 +214,18 @@ See below for the full definition of the `User` object. Below is a list of the ARIA patterns testers currently supported by `createTester`. See the accompanying component testing docs pages for a sample of how to use the testers in your test suite. -- [React Aria Components ComboBox](ComboBox.html#test-utils) and [React Spectrum ComboBox](../react-spectrum/ComboBox.html#test-utils) +- [ComboBox](ComboBox.html#test-utils) -- [React Aria Components GridList](GridList.html#test-utils) and [React Spectrum ListView](../react-spectrum/ListView.html#test-utils) +- [GridList](GridList.html#test-utils) -- [React Aria Components ListBox](ListBox.html#test-utils) and [React Spectrum ListBox](../react-spectrum/ListBox.html#test-utils) +- [ListBox](ListBox.html#test-utils) -- [React Aria Components Menu](Menu.html#test-utils) and [React Spectrum MenuTrigger](../react-spectrum/MenuTrigger.html#test-utils) +- [Menu](Menu.html#test-utils) -- [React Aria Components Select](Select.html#test-utils) and [React Spectrum Picker](../react-spectrum/Picker.html#test-utils) +- [Select](Select.html#test-utils) -- [React Aria Components Table](Table.html#test-utils) and [React Spectrum TableView](../react-spectrum/TableView.html#test-utils) +- [Table](Table.html#test-utils) -- [React Aria Components Tabs](Tabs.html#test-utils) and [React Spectrum Tabs](../react-spectrum/Tabs.html#test-utils) +- [Tabs](Tabs.html#test-utils) -- [React Aria Components Tree](Tree.html#test-utils) and [React Spectrum TreeView](../react-spectrum/TreeView.html#test-utils) +- [Tree](Tree.html#test-utils) diff --git a/packages/dev/docs/pages/react-spectrum/testing.mdx b/packages/dev/docs/pages/react-spectrum/testing.mdx index 04fc213564d..6b511d90345 100644 --- a/packages/dev/docs/pages/react-spectrum/testing.mdx +++ b/packages/dev/docs/pages/react-spectrum/testing.mdx @@ -10,7 +10,7 @@ governing permissions and limitations under the License. */} import {Layout} from '@react-spectrum/docs'; export default Layout; import testUtilDocs from 'docs:@react-spectrum/test-utils'; -import {TypeLink} from '@react-spectrum/docs'; +import {ClassAPI, TypeLink} from '@react-spectrum/docs'; --- category: Concepts @@ -248,7 +248,7 @@ Components like ComboBox and Picker render a different experience on mobile vers components are rendered only against a specific experience, you'll need to mock the window screen width accordingly. We currently use a [screen width of 700px](https://github.com/adobe/react-spectrum/blob/5e487532e3ca4714513fb6ab64f7a78d9d1ca281/packages/%40react-spectrum/utils/src/useIsMobileDevice.ts#L15-L23) as the breakpoint between mobile and desktop, so you should mock the `width` getter of `window.screen` to a value greater or less than this value accordingly. You may use and - from the `@react-spectrum/test-utils` package to handle this width mocking for you in Jest: + from the [@react-spectrum/test-utils](https://www.npmjs.com/package/@react-spectrum/test-utils) package to handle this width mocking for you in Jest: The example below shows how you would set this up in Jest. @@ -337,12 +337,6 @@ fireEvent.mouseMove(thumb, {pageX: 50}); fireEvent.mouseUp(thumb, {pageX: 50}); ``` -### Test Utilities - -In addition to some of the test utilities mentioned above, `@react-spectrum/test-utils` re-exports the same test utils available in `@react-aria/test-utils`, including -the ARIA pattern testers documented [here](../react-aria/testing.html#react-aria-test-utils). Those testers can be used with React Spectrum components as well and can be combined with the generalized -testing advice above. - ## Snapshot tests If you are using React 16 or 17, you may run into an issue where the ids generated by the React Spectrum components are changing on every snapshot. To remedy this, simply wrap your component in a [SSRProvider](../react-aria/SSRProvider.html). @@ -357,3 +351,72 @@ import {SSRProvider, Provider, lightTheme} from '@adobe/react-spectrum'; ``` + +## React Spectrum test utils + +In addition to the test utilities mentioned above, [@react-spectrum/test-utils](https://www.npmjs.com/package/@react-spectrum/test-utils) re-exports the same test utils available in `@react-aria/test-utils`, including +the ARIA pattern testers. These testers are set of testing utilities that aims to make writing unit tests easier for consumers of React Spectrum. By using the ARIA specification for any given component pattern as a source of truth, +we can make assumptions about the existence of various aria attributes in a component. This allows us to navigate the component's DOM structure, simulate common interactions, and verify the resulting state of the component. + +### Installation + +`@react-spectrum/test-utils` can be installed using a package manager like [npm](https://docs.npmjs.com/cli/npm) or [yarn](https://classic.yarnpkg.com/lang/en/). + +``` +yarn add --dev @react-spectrum/test-utils +``` + +Please note that this library uses [@testing-library/react@15](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event](https://www.npmjs.com/package/@testing-library/user-event/v/13.1.5). This means that you need +to be on React 18+ in order for these utilities to work. + +### Setup + +Once installed, you can access the `User` that `@react-spectrum/test-utils` provides in your test file as shown below. This user only needs to be initialized once and then can be used to generate +specific ARIA pattern tester in your test cases. This gives you access to that pattern's specific utilities that you can then call within your test to query for specific subcomponents or simulate common interactions. +See [below](#patterns) for what patterns are currently supported. + +```ts +// YourTest.test.ts +import {screen} from '@testing-library/react'; +import {User} from '@react-spectrum/test-utils'; + +// Provide whatever method of advancing timers you use in your test, this example assumes Jest with fake timers. +// 'interactionType' specifies what mode of interaction should be simulated by the tester +// 'advanceTimer' is used by the tester to advance the timers in the tests for specific interactions (e.g. long press) +let testUtilUser = new User({interactionType: 'mouse', advanceTimer: jest.advanceTimersByTime}); +// ... + +it('my test case', async function () { + // Render your test component/app + render(); + // Initialize the table tester via providing the 'Table' pattern name and the root element of said table + let table = testUtilUser.createTester('Table', {root: screen.getByTestId('test_table')}); + + // ... +}); +``` + +See below for the full definition of the `User` object. + + + +### Patterns + +Below is a list of the ARIA patterns testers currently supported by `createTester`. See the accompanying component testing docs pages for a sample of how to use +the testers in your test suite. + +- [ComboBox](ComboBox.html#test-utils) + +- [ListView](ListView.html#test-utils) + +- [ListBox](ListBox.html#test-utils) + +- [MenuTrigger](MenuTrigger.html#test-utils) + +- [Picker](Picker.html#test-utils) + +- [TableView](TableView.html#test-utils) + +- [Tabs](Tabs.html#test-utils) + +- [TreeView](TreeView.html#test-utils) diff --git a/packages/react-aria-components/docs/ComboBox.mdx b/packages/react-aria-components/docs/ComboBox.mdx index 7249062d960..8d24a49711a 100644 --- a/packages/react-aria-components/docs/ComboBox.mdx +++ b/packages/react-aria-components/docs/ComboBox.mdx @@ -1483,8 +1483,6 @@ If you need to customize things even further, such as accessing internal state, `@react-aria/test-utils` offers common combobox interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the combobox tester and a sample of how you could use it in your test suite. - - ```ts // Combobox.test.ts import {render} from '@testing-library/react'; @@ -1511,3 +1509,5 @@ it('ComboBox can select an option via keyboard', async function () { expect(comboboxTester.listbox).not.toBeInTheDocument(); }); ``` + + diff --git a/packages/react-aria-components/docs/GridList.mdx b/packages/react-aria-components/docs/GridList.mdx index 083e0684dfc..603033c6de9 100644 --- a/packages/react-aria-components/docs/GridList.mdx +++ b/packages/react-aria-components/docs/GridList.mdx @@ -1874,8 +1874,6 @@ If you need to customize things even further, such as accessing internal state, `@react-aria/test-utils` offers common gridlist interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the gridlist tester and a sample of how you could use it in your test suite. - - ```ts // GridList.test.ts import {render, within} from '@testing-library/react'; @@ -1909,3 +1907,5 @@ it('GridList can select a row via keyboard', async function () { expect(gridListTester.selectedRows).toHaveLength(0); }); ``` + + diff --git a/packages/react-aria-components/docs/ListBox.mdx b/packages/react-aria-components/docs/ListBox.mdx index d05c47dba03..a8167d502e4 100644 --- a/packages/react-aria-components/docs/ListBox.mdx +++ b/packages/react-aria-components/docs/ListBox.mdx @@ -2098,8 +2098,6 @@ If you need to customize things even further, such as accessing internal state o `@react-aria/test-utils` offers common listbox interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the listbox tester and a sample of how you could use it in your test suite. - - ```ts // ListBox.test.ts import {render} from '@testing-library/react'; @@ -2122,3 +2120,5 @@ it('ListBox can select an option via keyboard', async function () { expect(onSelectionChange).toBeCalledTimes(1); }); ``` + + diff --git a/packages/react-aria-components/docs/Menu.mdx b/packages/react-aria-components/docs/Menu.mdx index c532a9557af..7c7c4d9f87f 100644 --- a/packages/react-aria-components/docs/Menu.mdx +++ b/packages/react-aria-components/docs/Menu.mdx @@ -1148,8 +1148,6 @@ By providing the above contexts, the existing `Button`, `Popover`, and `Menu` co `@react-aria/test-utils` offers common menu interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the menu tester and a sample of how you could use it in your test suite. - - ```ts // Menu.test.ts import {render} from '@testing-library/react'; @@ -1180,3 +1178,5 @@ it('Menu can open its submenu via keyboard', async function () { expect(menuTester.menu).toBeInTheDocument(); }); ``` + + diff --git a/packages/react-aria-components/docs/Select.mdx b/packages/react-aria-components/docs/Select.mdx index e2d3ec78664..5f6d15a52b2 100644 --- a/packages/react-aria-components/docs/Select.mdx +++ b/packages/react-aria-components/docs/Select.mdx @@ -1246,8 +1246,6 @@ from your automated accessibility tests as shown [here](./accessibility.html#fal `@react-aria/test-utils` offers common select interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the select tester and a sample of how you could use it in your test suite. - - ```ts // Select.test.ts import {render} from '@testing-library/react'; @@ -1271,3 +1269,5 @@ it('Select can select an option via keyboard', async function () { expect(trigger).toHaveTextContent('Cat'); }); ``` + + diff --git a/packages/react-aria-components/docs/Table.mdx b/packages/react-aria-components/docs/Table.mdx index f48fab563cd..5a26ee4b19d 100644 --- a/packages/react-aria-components/docs/Table.mdx +++ b/packages/react-aria-components/docs/Table.mdx @@ -2484,8 +2484,6 @@ If you need to customize things even further, such as accessing internal state o `@react-aria/test-utils` offers common table interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the table tester and a sample of how you could use it in your test suite. - - ```ts // Table.test.ts import {render, within} from '@testing-library/react'; @@ -2520,3 +2518,5 @@ it('Table can toggle row selection', async function () { expect(tableTester.selectedRows).toHaveLength(0); }); ``` + + diff --git a/packages/react-aria-components/docs/Tabs.mdx b/packages/react-aria-components/docs/Tabs.mdx index 63760d341e2..80b62f3d3e6 100644 --- a/packages/react-aria-components/docs/Tabs.mdx +++ b/packages/react-aria-components/docs/Tabs.mdx @@ -760,8 +760,6 @@ If you need to customize things even further, such as accessing internal state o `@react-aria/test-utils` offers common tabs interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the tabs tester and a sample of how you could use it in your test suite. - - ```ts // Tabs.test.ts import {render} from '@testing-library/react'; @@ -787,3 +785,5 @@ it('Tabs can change selection via keyboard', async function () { expect(tabsTester.selectedTab).toBe(tabs[1]); }); ``` + + diff --git a/packages/react-aria-components/docs/Tree.mdx b/packages/react-aria-components/docs/Tree.mdx index c42effc2f5d..09cb00c83b1 100644 --- a/packages/react-aria-components/docs/Tree.mdx +++ b/packages/react-aria-components/docs/Tree.mdx @@ -329,8 +329,6 @@ TreeItem also exposes a `--tree-item-level` CSS custom property, which you can u `@react-aria/test-utils` offers common tree interaction utilities which you may find helpful when writing tests. See [here](../react-aria/testing.html#react-aria-test-utils) for more information on how to setup these utilities in your tests. Below is the full definition of the tree tester and a sample of how you could use it in your test suite. - - ```ts // Tree.test.ts import {render, within} from '@testing-library/react'; @@ -361,3 +359,5 @@ it('Tree can select a row via keyboard', async function () { expect(within(treeTester.rows[0]).getByRole('checkbox')).not.toBeChecked(); }); ``` + + From 7c5ed5cc970aa4793fea8ade0f01ae96d563b29e Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 10 Jan 2025 09:43:31 -0800 Subject: [PATCH 18/19] add alpha badge --- packages/dev/docs/pages/react-aria/testing.mdx | 4 ++-- packages/dev/docs/pages/react-spectrum/testing.mdx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/dev/docs/pages/react-aria/testing.mdx b/packages/dev/docs/pages/react-aria/testing.mdx index 9a3cd1949f8..ab742ad23c6 100644 --- a/packages/dev/docs/pages/react-aria/testing.mdx +++ b/packages/dev/docs/pages/react-aria/testing.mdx @@ -7,7 +7,7 @@ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTA OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */} -import {Layout} from '@react-spectrum/docs'; +import {Layout, VersionBadge} from '@react-spectrum/docs'; export default Layout; import testUtilDocs from 'docs:@react-aria/test-utils'; import combobox from 'docs:@react-aria/test-utils/src/combobox.ts'; @@ -160,7 +160,7 @@ userEvent.tab(); userEvent.click(document.activeElement); ``` -## React Aria test utils +## React Aria test utils [@react-aria/test-utils](https://www.npmjs.com/package/@react-aria/test-utils) is a set of testing utilities that aims to make writing unit tests easier for consumers of React Aria or for users who have built their own components following the respective ARIA pattern specification. By using the ARIA specification for any given component pattern as a source of truth, diff --git a/packages/dev/docs/pages/react-spectrum/testing.mdx b/packages/dev/docs/pages/react-spectrum/testing.mdx index 6b511d90345..4d91fbe7078 100644 --- a/packages/dev/docs/pages/react-spectrum/testing.mdx +++ b/packages/dev/docs/pages/react-spectrum/testing.mdx @@ -7,7 +7,7 @@ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTA OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */} -import {Layout} from '@react-spectrum/docs'; +import {Layout, VersionBadge} from '@react-spectrum/docs'; export default Layout; import testUtilDocs from 'docs:@react-spectrum/test-utils'; import {ClassAPI, TypeLink} from '@react-spectrum/docs'; @@ -352,7 +352,7 @@ import {SSRProvider, Provider, lightTheme} from '@adobe/react-spectrum'; ``` -## React Spectrum test utils +## React Spectrum test utils In addition to the test utilities mentioned above, [@react-spectrum/test-utils](https://www.npmjs.com/package/@react-spectrum/test-utils) re-exports the same test utils available in `@react-aria/test-utils`, including the ARIA pattern testers. These testers are set of testing utilities that aims to make writing unit tests easier for consumers of React Spectrum. By using the ARIA specification for any given component pattern as a source of truth, From 1273feae0f3ded407e427a008c84a64b4ae14f17 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Mon, 13 Jan 2025 11:16:08 -0800 Subject: [PATCH 19/19] review comments --- packages/@react-aria/test-utils/src/combobox.ts | 2 +- .../@react-spectrum/picker/test/Picker.test.js | 3 +-- packages/dev/docs/pages/react-aria/testing.mdx | 17 ++--------------- .../dev/docs/pages/react-spectrum/testing.mdx | 17 ++--------------- 4 files changed, 6 insertions(+), 33 deletions(-) diff --git a/packages/@react-aria/test-utils/src/combobox.ts b/packages/@react-aria/test-utils/src/combobox.ts index c496e8f2ea7..f60ee66f88b 100644 --- a/packages/@react-aria/test-utils/src/combobox.ts +++ b/packages/@react-aria/test-utils/src/combobox.ts @@ -119,7 +119,7 @@ export class ComboBoxTester { } /** - * Returns a option matching the specified index or text content. + * Returns an option matching the specified index or text content. */ findOption(opts: {optionIndexOrText: number | string}): HTMLElement { let { diff --git a/packages/@react-spectrum/picker/test/Picker.test.js b/packages/@react-spectrum/picker/test/Picker.test.js index 0590e533cfb..36421aaf844 100644 --- a/packages/@react-spectrum/picker/test/Picker.test.js +++ b/packages/@react-spectrum/picker/test/Picker.test.js @@ -1085,8 +1085,7 @@ describe('Picker', function () { expect(picker).toHaveTextContent('Two'); }); - // TODO: re-enable after merge - it.skip('can select items with the Enter key', async function () { + it('can select items with the Enter key', async function () { let {getByRole} = render( diff --git a/packages/dev/docs/pages/react-aria/testing.mdx b/packages/dev/docs/pages/react-aria/testing.mdx index ab742ad23c6..dd26321bb55 100644 --- a/packages/dev/docs/pages/react-aria/testing.mdx +++ b/packages/dev/docs/pages/react-aria/testing.mdx @@ -29,8 +29,7 @@ for each aria pattern and how they can be used to simulate common user interacti ## Introduction Running automated tests on your application helps ensure that it continues to work as expected over time. -You can use testing tools like [Enzyme](https://enzymejs.github.io/enzyme/) or -[React Testing Library](https://testing-library.com/docs/react-testing-library/intro) along with test runners like +You can use testing tools like [React Testing Library](https://testing-library.com/docs/react-testing-library/intro) along with test runners like [Jest](https://jestjs.io) or [Mocha](https://mochajs.org) to test applications built with React Aria Components or hooks. These generally work quite well out of the box but there are a few things to consider to ensure your tests are the best they can be. @@ -67,18 +66,6 @@ let tree = render(); let option = tree.getByRole('button'); ``` -### Enzyme - -In Enzyme, you can [use CSS selectors](https://enzymejs.github.io/enzyme/docs/api/ReactWrapper/find.html) to find -elements. You can query by role using an attribute selector: - -```tsx -import {mount} from 'enzyme'; - -let wrapper = mount(); -let option = wrapper.find('[role="option"]'); -``` - ## Test ids Querying by semantics covers many scenarios, but what if you have many buttons on a page? How do you find the specific button @@ -92,7 +79,7 @@ through to their underlying DOM nodes, which allows you to use an attribute like a particular instance of a component. For example, you could add test ids to the two input elements in a login form and use them to find the username and password fields. -This example uses React Testing Library, but the idea could be applied in a similar way with Enzyme or other +This example uses React Testing Library, but the idea could be applied in a similar way with other testing libraries. ```tsx diff --git a/packages/dev/docs/pages/react-spectrum/testing.mdx b/packages/dev/docs/pages/react-spectrum/testing.mdx index 4d91fbe7078..2bfc2600d20 100644 --- a/packages/dev/docs/pages/react-spectrum/testing.mdx +++ b/packages/dev/docs/pages/react-spectrum/testing.mdx @@ -24,8 +24,7 @@ query the DOM tree for elements and simulate user interactions. ## Introduction Running automated tests on your application helps ensure that it continues to work as expected over time. -You can use testing tools like [Enzyme](https://enzymejs.github.io/enzyme/) or -[React Testing Library](https://testing-library.com/docs/react-testing-library/intro) along with test runners like +You can use testing tools like [React Testing Library](https://testing-library.com/docs/react-testing-library/intro) along with test runners like [Jest](https://jestjs.io) or [Mocha](https://mochajs.org) to test React Spectrum applications. These generally work quite well out of the box but there are a few things to consider to ensure your tests are the best they can be. @@ -72,18 +71,6 @@ let tree = render(); let option = tree.getByRole('button'); ``` -### Enzyme - -In Enzyme, you can [use CSS selectors](https://enzymejs.github.io/enzyme/docs/api/ReactWrapper/find.html) to find -elements. You can query by role using an attribute selector: - -```tsx -import {mount} from 'enzyme'; - -let wrapper = mount(); -let option = wrapper.find('[role="option"]'); -``` - ## Test ids Querying by semantics covers many scenarios, but what if you have many buttons on a page? How do you find the specific button @@ -97,7 +84,7 @@ through to their underlying DOM nodes, which allows you to use an attribute like a particular instance of a component. For example, you could add test ids to the two input elements in a login form and use them to find the username and password fields. -This example uses React Testing Library, but the idea could be applied in a similar way with Enzyme or other +This example uses React Testing Library, but the idea could be applied in a similar way with other testing libraries. ```tsx