-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat: Add CheckBoxGroup/Dialog/RadioGroup test utils #9039
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
base: main
Are you sure you want to change the base?
Conversation
Build successful! 🎉 |
Build successful! 🎉 |
Build successful! 🎉 |
} | ||
|
||
if (!this.checkboxgroup.contains(document.activeElement)) { | ||
act(() => checkboxes[0].focus()); |
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.
Should we do this by default? It can sometimes cause behaviour that doesn't match real life. Maybe there should be an option to force focus, but otherwise we throw if they aren't in the checkbox group already?
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.
From feedback for other users and my own experience from converting tests in Quarry to use the test utils, there is often a lot of friction when people are integrating the utils into the flow of their existing test and expect that calling these "toggle/trigger" functions should just work, even if active focus is in a separate place in their app and the real life use case would be to press Tab 4x to get to the checkboxgroup/etc. I think ideally everyones tests would simulate the exact user flows but realistically I'd like there to be as little friction as possible in adoption.
if (document.activeElement !== this._checkboxgroup && !this._checkboxgroup.contains(document.activeElement)) { | ||
act(() => this._checkboxgroup.focus()); | ||
} | ||
|
||
await this.keyboardNavigateToCheckbox({checkbox}); |
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 keyboardNavigateToCheckbox
already handle this forced focus?
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.
oh true, this came from a copy pasta where this was necessary to simulate the collection focus -> inner collection element (e.g. row/cell) focus flow that actually happens in browser for other components
this._overlayType = overlayType || 'modal'; | ||
|
||
// Handle case where element provided is a wrapper of the trigger button | ||
let trigger = within(root).queryByRole('button'); |
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.
&& aria-expanded is an attribute on the button?
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.
for buttons that open a modal type dialog, they don't get aria-expanded
necessarly: https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/dialog/
async close(): Promise<void> { | ||
let dialog = this._dialog; | ||
if (dialog) { | ||
await this.user.keyboard('[Escape]'); |
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.
only for isKeyboardDismissable?
maybe close could take an argument to a button to click for that modality?
also, there's the hidden dismiss buttons for popovers if modality is virtual
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.
I opted to start with simplicity first, especially since dialog content is so custom that the user is probably already querying for buttons that can close the dialog and thus can simply trigger a press themselves. If they aren't querying for a specific close button already, then they probably aren't testing that close
uses a specific interaction mode and just want to close the dialog via a standard method. As for virtual modality, it depends on the implementation and I don't think if attempting to simulate a screenreader action is really that accurate with the tooling we have right now
My current philosophy for the test utils is that they should be used alongside your standard user events and element queries rather than be a wholesale replacement. Therefore, if the util doesn't cut down on the number of lines you write or help query for elements in a way that it also is asserting for roles/state then it doesn't need to handle it.
if (!trigger.hasAttribute('disabled')) { | ||
if (interactionType === 'mouse' || interactionType === 'touch') { | ||
if (interactionType === 'mouse') { | ||
await this.user.click(trigger); | ||
} else { | ||
await this.user.pointer({target: trigger, keys: '[TouchA]'}); | ||
} | ||
} else if (interactionType === 'keyboard') { | ||
act(() => trigger.focus()); | ||
await this.user.keyboard('[Enter]'); | ||
} |
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.
if (!trigger.hasAttribute('disabled')) { | |
if (interactionType === 'mouse' || interactionType === 'touch') { | |
if (interactionType === 'mouse') { | |
await this.user.click(trigger); | |
} else { | |
await this.user.pointer({target: trigger, keys: '[TouchA]'}); | |
} | |
} else if (interactionType === 'keyboard') { | |
act(() => trigger.focus()); | |
await this.user.keyboard('[Enter]'); | |
} | |
if (!trigger.hasAttribute('disabled')) { | |
if (interactionType === 'mouse') { | |
await this.user.click(trigger); | |
} else if (interactionType === 'touch') { | |
await this.user.pointer({target: trigger, keys: '[TouchA]'}); | |
} else if (interactionType === 'keyboard') { | |
act(() => trigger.focus()); | |
await this.user.keyboard('[Enter]'); | |
} |
similar issue with trigger.focus that i brought up above
if (dialog && document.activeElement !== this._trigger && dialog.contains(document.activeElement)) { | ||
this._dialog = dialog; | ||
} else { | ||
// TODO: is it too brittle to throw here? |
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.
seems fine? were you running into unexpected cases?
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.
mainly still unsure if timing might throw off this check, but seems ok so far
act(() => this._radiogroup.focus()); | ||
} | ||
|
||
let radioOrientation = this._radiogroup.getAttribute('aria-orientation') || 'horizontal'; |
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.
seems like keyboardNavigateToRadio should determine the orientation, no need to pass it in?
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.
ya I might refactor this when I refactor some common flows out of the utils into generalized patterns (aka keyboardNavigateToElement might become a common util instead)
} | ||
|
||
if (interactionType === 'keyboard') { | ||
if (document.activeElement !== this._radiogroup && !this._radiogroup.contains(document.activeElement)) { |
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.
I think this is handled in keyboardNavigateToRadio already
let dialogTester = testUtilUser.createTester('Dialog', {root: dialogTrigger, interactionType: 'keyboard', overlayType: 'modal'}); | ||
await dialogTester.open(); | ||
let dialog = dialogTester.dialog; | ||
// TODO: also weird that it is dialog.dialog? |
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.
?
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.
whoops, this was when I had named the tester dialog
rather than dialogTester
, forgot to remove
|
||
let dialog = getByRole('dialog'); | ||
let dialogTrigger = document.activeElement! as HTMLElement; | ||
// TODO: this is a weird case where the popover isn't actually linked to the button, behaving more like a modal |
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.
good point, should this be attached?
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.
yeah, should it be a popover? Then again https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/datepicker-dialog/ is similar so maybe not a problem
Build successful! 🎉 |
Closes
✅ Pull Request Checklist:
📝 Test Instructions:
If you wanna test the S2 RadioGroup fix, simply go to the S2 storybook and set it to horizontal + rtl. ArrowLeft/Right should move focus to the left and right as expected.
🧢 Your Project:
RSP