-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Initial aria test util docs #7145
base: main
Are you sure you want to change the base?
Changes from 7 commits
0991467
9def1bd
d6e7578
4b5bad1
4b28f24
256e859
00a2fb0
d460519
aa1f7f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,20 +11,38 @@ | |
*/ | ||
|
||
import {act, waitFor, within} from '@testing-library/react'; | ||
import {BaseTesterOpts, UserOpts} from './user'; | ||
import {ComboBoxTesterOpts, UserOpts} from './types'; | ||
|
||
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'] | ||
} | ||
|
||
export interface ComboBoxOptions extends UserOpts, BaseTesterOpts { | ||
user: any, | ||
trigger?: HTMLElement | ||
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; | ||
private _interactionType: UserOpts['interactionType']; | ||
private _combobox: HTMLElement; | ||
private _trigger: HTMLElement | undefined; | ||
private _trigger: HTMLElement; | ||
|
||
constructor(opts: ComboBoxOptions) { | ||
constructor(opts: ComboBoxTesterOpts) { | ||
let {root, trigger, user, interactionType} = opts; | ||
this.user = user; | ||
this._interactionType = interactionType || 'mouse'; | ||
|
@@ -52,11 +70,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. Defaults to using the interaction type set on the combobox tester. | ||
*/ | ||
async open(opts: ComboBoxOpenOpts = {}) { | ||
let {triggerBehavior = 'manual', interactionType = this._interactionType} = opts; | ||
let trigger = this.trigger; | ||
let combobox = this.combobox; | ||
|
@@ -96,9 +120,13 @@ export class ComboBoxTester { | |
return true; | ||
} | ||
}); | ||
}; | ||
} | ||
|
||
selectOption = async (opts: {option?: HTMLElement, optionText?: string, triggerBehavior?: 'focus' | 'manual', interactionType?: UserOpts['interactionType']} = {}) => { | ||
/** | ||
* 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: ComboBoxSelectOpts = {}) { | ||
let {optionText, option, triggerBehavior, interactionType = this._interactionType} = opts; | ||
if (!this.combobox.getAttribute('aria-controls')) { | ||
await this.open({triggerBehavior}); | ||
|
@@ -130,9 +158,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,43 +177,56 @@ export class ComboBoxTester { | |
} | ||
}); | ||
} | ||
}; | ||
} | ||
|
||
get combobox() { | ||
/** | ||
* Returns the combobox. | ||
*/ | ||
get combobox(): HTMLElement { | ||
return this._combobox; | ||
} | ||
|
||
get trigger() { | ||
/** | ||
* Returns the combobox trigger button if present. | ||
*/ | ||
get trigger(): HTMLElement { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if present would imply that this should be. So we should either update to that or change the sentence
|
||
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; | ||
return listBoxId ? document.getElementById(listBoxId) || null : null; | ||
} | ||
|
||
options = (opts: {element?: HTMLElement} = {}): HTMLElement[] | never[] => { | ||
let {element} = opts; | ||
element = element || this.listbox; | ||
/** | ||
* Returns the combobox's sections if present. | ||
*/ | ||
get sections(): HTMLElement[] { | ||
let listbox = this.listbox; | ||
return listbox ? within(listbox).queryAllByRole('group') : []; | ||
} | ||
|
||
/** | ||
* 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; | ||
let options = []; | ||
if (element) { | ||
options = within(element).queryAllByRole('option'); | ||
} | ||
|
||
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; | ||
return focusedOptionId ? document.getElementById(focusedOptionId) : null; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,35 +11,73 @@ | |
*/ | ||
|
||
import {act, within} from '@testing-library/react'; | ||
import {BaseTesterOpts, UserOpts} from './user'; | ||
import {GridListTesterOpts, UserOpts} from './types'; | ||
import {pressElement} from './events'; | ||
|
||
export interface GridListOptions 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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what happens if an index isn't supplied? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if it's either text OR index, maybe overloads would be more suitable? |
||
/** | ||
* 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'; | ||
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. Defaults to using the interaction type set on the gridlist tester. | ||
*/ | ||
async toggleRowSelection(opts: GridListToggleRowOpts = {}) { | ||
let {index, text, interactionType = this._interactionType} = opts; | ||
|
||
let row = this.findRow({index, text}); | ||
|
@@ -50,11 +88,14 @@ 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}) => { | ||
/** | ||
* Returns a row matching the specified index or text content. | ||
*/ | ||
findRow(opts: {index?: number, text?: string}) { | ||
let { | ||
index, | ||
text | ||
|
@@ -71,11 +112,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. Defaults to using the interaction type set on the gridlist tester. | ||
*/ | ||
async triggerRowAction(opts: GridListRowActionOpts) { | ||
let { | ||
index, | ||
text, | ||
|
@@ -94,23 +138,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 { | ||
return this._gridlist; | ||
} | ||
|
||
get rows() { | ||
/** | ||
* Returns the gridlist's rows if any. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these 'any' are fine because the array could be empty |
||
*/ | ||
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} = {}) => { | ||
let {element} = opts; | ||
return within(element || this.gridlist).queryAllByRole('gridcell'); | ||
}; | ||
/** | ||
* 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; | ||
return within(element).queryAllByRole('gridcell'); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
? won't this cause some warnings during
yarn install
?