From a9a829d06b4f2b2db85c7985fe9750d35d58a2da Mon Sep 17 00:00:00 2001 From: Taylor Jones Date: Thu, 22 Oct 2026 14:33:26 -0500 Subject: [PATCH] test(react): migrate action components to browser locators Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0e8e36b7-9bf7-441f-b8fb-844f81f27d32 --- .../react/src/ActionBar/ActionBar.test.tsx | 213 +++--- .../react/src/ActionList/ActionList.test.tsx | 36 +- .../react/src/ActionList/Description.test.tsx | 41 +- packages/react/src/ActionList/Group.test.tsx | 61 +- .../react/src/ActionList/Heading.test.tsx | 39 +- packages/react/src/ActionList/Item.test.tsx | 282 ++++---- .../react/src/ActionList/LinkItem.test.tsx | 37 +- .../react/src/ActionMenu/ActionMenu.test.tsx | 662 +++++++++--------- .../AnchoredOverlay/AnchoredOverlay.test.tsx | 64 +- 9 files changed, 710 insertions(+), 725 deletions(-) diff --git a/packages/react/src/ActionBar/ActionBar.test.tsx b/packages/react/src/ActionBar/ActionBar.test.tsx index 9afa538368f..23aa52a96ca 100644 --- a/packages/react/src/ActionBar/ActionBar.test.tsx +++ b/packages/react/src/ActionBar/ActionBar.test.tsx @@ -1,6 +1,6 @@ +import {page, userEvent} from 'vitest/browser' import {describe, expect, it, afterEach, vi} from 'vitest' -import {render, screen, act, within} from '@testing-library/react' -import userEvent from '@testing-library/user-event' +import {render, act, fireEvent} from '@testing-library/react' import React, {createRef, useState} from 'react' import ActionBar from './' import {BoldIcon, ItalicIcon, CodeIcon} from '@primer/octicons-react' @@ -14,9 +14,8 @@ describe('ActionBar', () => { }) it('renders icon elements in the overflow menu', async () => { - const user = userEvent.setup() render( -
+
} aria-label="Bold" /> {
, ) - await user.click(await screen.findByRole('button', {name: 'More items'})) + await expect.element(page.getByRole('toolbar')).toHaveAttribute('data-has-overflow', 'true') + await userEvent.click(page.getByRole('button', {name: 'More items', exact: true})) - expect(within(screen.getByRole('menuitem', {name: 'Bold'})).getByTestId('action-icon')).toBeInTheDocument() - expect(within(screen.getByRole('menuitem', {name: 'Formatting'})).getByTestId('menu-icon')).toBeInTheDocument() + await expect + .element(page.getByRole('menuitem', {name: 'Bold', exact: true}).getByTestId('action-icon')) + .toBeInTheDocument() + await expect + .element(page.getByRole('menuitem', {name: 'Formatting', exact: true}).getByTestId('menu-icon')) + .toBeInTheDocument() }) it('renders icon elements in menu items and submenu triggers', async () => { - const user = userEvent.setup() render( { , ) - await user.click(screen.getByRole('button', {name: 'Formatting'})) + await userEvent.click(page.getByRole('button', {name: 'Formatting', exact: true})) - const item = within(screen.getByRole('menuitem', {name: 'Bold'})) - expect(item.getByTestId('item-leading-icon')).toBeInTheDocument() - expect(item.getByTestId('item-trailing-icon')).toBeInTheDocument() - const submenu = within(screen.getByRole('menuitem', {name: 'More formatting'})) - expect(submenu.getByTestId('submenu-leading-icon')).toBeInTheDocument() - expect(submenu.getByTestId('submenu-trailing-icon')).toBeInTheDocument() + const item = page.getByRole('menuitem', {name: 'Bold', exact: true}) + await expect.element(item.getByTestId('item-leading-icon')).toBeInTheDocument() + await expect.element(item.getByTestId('item-trailing-icon')).toBeInTheDocument() + const submenu = page.getByRole('menuitem', {name: 'More formatting', exact: true}) + await expect.element(submenu.getByTestId('submenu-leading-icon')).toBeInTheDocument() + await expect.element(submenu.getByTestId('submenu-trailing-icon')).toBeInTheDocument() }) it('should not trigger disabled button', () => { @@ -77,13 +80,13 @@ describe('ActionBar', () => { , ) - const button = screen.getByRole('button') - button.click() + const button = page.getByRole('button') + fireEvent.click(button.element()) expect(onClick).not.toHaveBeenCalled() }) - it('should trigger non-disabled button', () => { + it('should trigger non-disabled button', async () => { const onClick = vi.fn() render( @@ -91,14 +94,13 @@ describe('ActionBar', () => { , ) - const button = screen.getByRole('button') - button.click() + const button = page.getByRole('button') + await userEvent.click(button) expect(onClick).toHaveBeenCalled() }) it('should not trigger disabled button with spacebar or enter', async () => { - const user = userEvent.setup() const onClick = vi.fn() render( @@ -106,19 +108,18 @@ describe('ActionBar', () => { , ) - const button = screen.getByRole('button') + const button = page.getByRole('button') act(() => { - button.focus() + button.element().focus() }) - await user.keyboard('{Enter}') + await userEvent.keyboard('{Enter}') expect(onClick).not.toHaveBeenCalled() }) it('should trigger non-disabled button with spacebar or enter', async () => { - const user = userEvent.setup() const onClick = vi.fn() render( @@ -126,13 +127,13 @@ describe('ActionBar', () => { , ) - const button = screen.getByRole('button') + const button = page.getByRole('button') act(() => { - button.focus() + button.element().focus() }) - await user.keyboard('{Enter}') + await userEvent.keyboard('{Enter}') expect(onClick).toHaveBeenCalled() }) @@ -143,17 +144,17 @@ describe('ActionBar.Button', () => { vi.clearAllMocks() }) - it('renders a text button with its children as the accessible name', () => { + it('renders a text button with its children as the accessible name', async () => { render( Save , ) - expect(screen.getByRole('button', {name: 'Save'})).toBeInTheDocument() + await expect.element(page.getByRole('button', {name: 'Save', exact: true})).toBeInTheDocument() }) - it('should trigger non-disabled button', () => { + it('should trigger non-disabled button', async () => { const onClick = vi.fn() render( @@ -161,7 +162,7 @@ describe('ActionBar.Button', () => { , ) - screen.getByRole('button', {name: 'Save'}).click() + await userEvent.click(page.getByRole('button', {name: 'Save', exact: true})) expect(onClick).toHaveBeenCalled() }) @@ -176,13 +177,12 @@ describe('ActionBar.Button', () => { , ) - screen.getByRole('button', {name: 'Save'}).click() + fireEvent.click(page.getByRole('button', {name: 'Save', exact: true}).element()) expect(onClick).not.toHaveBeenCalled() }) it('should not trigger disabled button with spacebar or enter', async () => { - const user = userEvent.setup() const onClick = vi.fn() render( @@ -192,20 +192,20 @@ describe('ActionBar.Button', () => { , ) - const button = screen.getByRole('button', {name: 'Save'}) + const button = page.getByRole('button', {name: 'Save', exact: true}) act(() => { - button.focus() + button.element().focus() }) - await user.keyboard('{Enter}') + await userEvent.keyboard('{Enter}') expect(onClick).not.toHaveBeenCalled() }) }) describe('ActionBar Registry System', () => { - it('should preserve order with deep nesting', () => { + it('should preserve order with deep nesting', async () => { render(
@@ -218,14 +218,14 @@ describe('ActionBar Registry System', () => { , ) - const buttons = screen.getAllByRole('button') + const buttons = page.getByRole('button').all() expect(buttons).toHaveLength(3) - expect(buttons[0]).toHaveAccessibleName('First') - expect(buttons[1]).toHaveAccessibleName('Second') - expect(buttons[2]).toHaveAccessibleName('Third') + await expect.element(buttons[0]).toHaveAccessibleName('First') + await expect.element(buttons[1]).toHaveAccessibleName('Second') + await expect.element(buttons[2]).toHaveAccessibleName('Third') }) - it('should preserve group order with deep nesting', () => { + it('should preserve group order with deep nesting', async () => { render(
@@ -244,11 +244,11 @@ describe('ActionBar Registry System', () => { , ) - const buttons = screen.getAllByRole('button') + const buttons = page.getByRole('button').all() expect(buttons).toHaveLength(3) - expect(buttons[0]).toHaveAccessibleName('First') - expect(buttons[1]).toHaveAccessibleName('Second') - expect(buttons[2]).toHaveAccessibleName('Third') + await expect.element(buttons[0]).toHaveAccessibleName('First') + await expect.element(buttons[1]).toHaveAccessibleName('Second') + await expect.element(buttons[2]).toHaveAccessibleName('Third') }) it('should handle conditional rendering without breaking order', async () => { @@ -274,31 +274,30 @@ describe('ActionBar Registry System', () => { ) } - const user = userEvent.setup() render() // Initially should have 3 buttons - expect(screen.getAllByRole('button', {name: /First|Second|Third/})).toHaveLength(3) + expect(page.getByRole('button', {name: /First|Second|Third/}).all()).toHaveLength(3) // Hide first button - await user.click(screen.getByText('Hide first')) + await userEvent.click(page.getByText('Hide first', {exact: true})) - const buttonsAfterHide = screen.getAllByRole('button', {name: /Second|Third/}) + const buttonsAfterHide = page.getByRole('button', {name: /Second|Third/}).all() expect(buttonsAfterHide).toHaveLength(2) - expect(buttonsAfterHide[0]).toHaveAccessibleName('Second') - expect(buttonsAfterHide[1]).toHaveAccessibleName('Third') + await expect.element(buttonsAfterHide[0]).toHaveAccessibleName('Second') + await expect.element(buttonsAfterHide[1]).toHaveAccessibleName('Third') // Show first button again - await user.click(screen.getByText('Show all')) + await userEvent.click(page.getByText('Show all', {exact: true})) - const buttonsAfterShow = screen.getAllByRole('button', {name: /First|Second|Third/}) + const buttonsAfterShow = page.getByRole('button', {name: /First|Second|Third/}).all() expect(buttonsAfterShow).toHaveLength(3) - expect(buttonsAfterShow[0]).toHaveAccessibleName('First') - expect(buttonsAfterShow[1]).toHaveAccessibleName('Second') - expect(buttonsAfterShow[2]).toHaveAccessibleName('Third') + await expect.element(buttonsAfterShow[0]).toHaveAccessibleName('First') + await expect.element(buttonsAfterShow[1]).toHaveAccessibleName('Second') + await expect.element(buttonsAfterShow[2]).toHaveAccessibleName('Third') }) - it('should handle fragments and array mapping', () => { + it('should handle fragments and array mapping', async () => { render( <> @@ -311,12 +310,12 @@ describe('ActionBar Registry System', () => { , ) - const buttons = screen.getAllByRole('button') + const buttons = page.getByRole('button').all() expect(buttons).toHaveLength(4) - expect(buttons[0]).toHaveAccessibleName('In fragment') - expect(buttons[1]).toHaveAccessibleName('Mapped 1') - expect(buttons[2]).toHaveAccessibleName('Mapped 2') - expect(buttons[3]).toHaveAccessibleName('After fragment') + await expect.element(buttons[0]).toHaveAccessibleName('In fragment') + await expect.element(buttons[1]).toHaveAccessibleName('Mapped 1') + await expect.element(buttons[2]).toHaveAccessibleName('Mapped 2') + await expect.element(buttons[3]).toHaveAccessibleName('After fragment') }) it('should handle rapid re-renders without losing registry data', async () => { @@ -335,18 +334,17 @@ describe('ActionBar Registry System', () => { ) } - const user = userEvent.setup() render() // Rapidly trigger re-renders for (let i = 0; i < 10; i++) { - await user.click(screen.getByText('Increment')) + await userEvent.click(page.getByText('Increment', {exact: true})) } - expect(screen.getByRole('button', {name: 'Button 10'})).toBeInTheDocument() + await expect.element(page.getByRole('button', {name: 'Button 10', exact: true})).toBeInTheDocument() }) - it('should handle zero-width scenarios gracefully', () => { + it('should handle zero-width scenarios gracefully', async () => { render(
@@ -357,7 +355,7 @@ describe('ActionBar Registry System', () => { // Component should still render even with zero width // Button is unlabeled because the label is hidden, so we select by test id instead - expect(screen.getByTestId('zero-width-button')).toBeInTheDocument() + await expect.element(page.getByTestId('zero-width-button')).toBeInTheDocument() }) it('should clean up registry on unmount', async () => { @@ -378,54 +376,53 @@ describe('ActionBar Registry System', () => { ) } - const user = userEvent.setup() render() - expect(screen.getByRole('button', {name: 'Will unmount'})).toBeInTheDocument() + await expect.element(page.getByRole('button', {name: 'Will unmount', exact: true})).toBeInTheDocument() - await user.click(screen.getByText('Unmount')) + await userEvent.click(page.getByText('Unmount', {exact: true})) - expect(screen.queryByRole('button', {name: 'Will unmount'})).not.toBeInTheDocument() + await expect.element(page.getByRole('button', {name: 'Will unmount', exact: true})).not.toBeInTheDocument() }) }) describe('ActionBar gap prop', () => { - it('defaults to condensed', () => { + it('defaults to condensed', async () => { render( , ) - const toolbar = screen.getByRole('toolbar') - expect(toolbar).toHaveAttribute('data-gap', 'condensed') + const toolbar = page.getByRole('toolbar') + await expect.element(toolbar).toHaveAttribute('data-gap', 'condensed') }) - it('applies provided gap scale (none)', () => { + it('applies provided gap scale (none)', async () => { render( , ) - const toolbar = screen.getByRole('toolbar') - expect(toolbar).toHaveAttribute('data-gap', 'none') + const toolbar = page.getByRole('toolbar') + await expect.element(toolbar).toHaveAttribute('data-gap', 'none') }) - it('applies provided gap scale (condensed)', () => { + it('applies provided gap scale (condensed)', async () => { render( , ) - const toolbar = screen.getByRole('toolbar') - expect(toolbar).toHaveAttribute('data-gap', 'condensed') + const toolbar = page.getByRole('toolbar') + await expect.element(toolbar).toHaveAttribute('data-gap', 'condensed') }) }) describe('ActionBar.Menu returnFocusRef', () => { - it('accepts returnFocusRef prop', () => { + it('accepts returnFocusRef prop', async () => { const returnFocusRef = createRef() render(
@@ -443,11 +440,10 @@ describe('ActionBar.Menu returnFocusRef', () => {
, ) - expect(screen.getByRole('button', {name: 'More options'})).toBeInTheDocument() + await expect.element(page.getByRole('button', {name: 'More options', exact: true})).toBeInTheDocument() }) it('returns focus to returnFocusRef when menu is closed', async () => { - const user = userEvent.setup() const returnFocusRef = createRef() render( @@ -466,24 +462,23 @@ describe('ActionBar.Menu returnFocusRef', () => {
, ) - const menuButton = screen.getByRole('button', {name: 'More options'}) + const menuButton = page.getByRole('button', {name: 'More options', exact: true}) // Open the menu - await user.click(menuButton) + await userEvent.click(menuButton) // Verify menu is open - expect(screen.getByRole('menu')).toBeInTheDocument() + await expect.element(page.getByRole('menu')).toBeInTheDocument() // Close the menu by pressing Escape - await user.keyboard('{Escape}') + await userEvent.keyboard('{Escape}') // Verify focus is returned to the returnFocusRef element - const returnFocusTarget = screen.getByTestId('return-focus-target') - expect(document.activeElement).toEqual(returnFocusTarget) + const returnFocusTarget = page.getByTestId('return-focus-target') + await expect.element(returnFocusTarget).toHaveFocus() }) it('returns focus to returnFocusRef when menu item is selected', async () => { - const user = userEvent.setup() const returnFocusRef = createRef() const onClick = vi.fn() @@ -503,41 +498,39 @@ describe('ActionBar.Menu returnFocusRef', () => {
, ) - const menuButton = screen.getByRole('button', {name: 'More options'}) + const menuButton = page.getByRole('button', {name: 'More options', exact: true}) // Open the menu - await user.click(menuButton) + await userEvent.click(menuButton) // Click a menu item - await user.click(screen.getByRole('menuitem', {name: 'Option 1'})) + await userEvent.click(page.getByRole('menuitem', {name: 'Option 1', exact: true})) // Verify focus is returned to the returnFocusRef element - const returnFocusTarget = screen.getByTestId('return-focus-target') - expect(document.activeElement).toEqual(returnFocusTarget) + const returnFocusTarget = page.getByTestId('return-focus-target') + await expect.element(returnFocusTarget).toHaveFocus() }) it('returns focus to anchor button when returnFocusRef is not provided', async () => { - const user = userEvent.setup() - render( , ) - const menuButton = screen.getByRole('button', {name: 'More options'}) + const menuButton = page.getByRole('button', {name: 'More options', exact: true}) // Open the menu - await user.click(menuButton) + await userEvent.click(menuButton) // Verify menu is open - expect(screen.getByRole('menu')).toBeInTheDocument() + await expect.element(page.getByRole('menu')).toBeInTheDocument() // Close the menu by pressing Escape - await user.keyboard('{Escape}') + await userEvent.keyboard('{Escape}') // Verify focus returns to the menu button (default behavior) - expect(document.activeElement).toEqual(menuButton) + await expect.element(menuButton).toHaveFocus() }) }) @@ -564,15 +557,15 @@ describe('ActionBar data-component attributes', () => { expect(iconButton).toBeInTheDocument() }) - it('renders ActionBar.Button with a text label', () => { + it('renders ActionBar.Button with a text label', async () => { render( Save , ) - const button = screen.getByRole('button', {name: 'Save'}) - expect(button).toBeInTheDocument() + const button = page.getByRole('button', {name: 'Save', exact: true}) + await expect.element(button).toBeInTheDocument() }) it('renders ActionBar.VerticalDivider with data-component attribute', () => { @@ -602,14 +595,14 @@ describe('ActionBar data-component attributes', () => { expect(group).toBeInTheDocument() }) - it('renders ActionBar.Menu.IconButton with data-component attribute', () => { + it('renders ActionBar.Menu.IconButton with data-component attribute', async () => { render( , ) - const menuButton = screen.getByRole('button', {name: 'More options'}) - expect(menuButton).toHaveAttribute('data-component', 'ActionBar.Menu.IconButton') + const menuButton = page.getByRole('button', {name: 'More options', exact: true}) + await expect.element(menuButton).toHaveAttribute('data-component', 'ActionBar.Menu.IconButton') }) }) diff --git a/packages/react/src/ActionList/ActionList.test.tsx b/packages/react/src/ActionList/ActionList.test.tsx index e12b21c5bb6..74da5f666a1 100644 --- a/packages/react/src/ActionList/ActionList.test.tsx +++ b/packages/react/src/ActionList/ActionList.test.tsx @@ -1,6 +1,6 @@ +import {page, userEvent} from 'vitest/browser' import {describe, it, expect, vi} from 'vitest' -import {render as HTMLRender} from '@testing-library/react' -import userEvent from '@testing-library/user-event' +import {render as HTMLRender, act, fireEvent} from '@testing-library/react' import {ActionList} from '.' import {createRef} from 'react' import {ActionListContainerContext} from './ActionListContainerContext' @@ -50,23 +50,25 @@ describe('ActionList', () => { ) await userEvent.tab() // tab into the story, this should focus on the first button - expect(document.activeElement).toHaveTextContent('Option 1') + await expect.element(page.getByRole('option', {name: 'Option 1', exact: true})).toHaveFocus() await userEvent.keyboard('{ArrowDown}') - expect(document.activeElement).toHaveTextContent('Option 2') + await expect.element(page.getByRole('option', {name: 'Option 2', exact: true})).toHaveFocus() await userEvent.keyboard('{ArrowDown}') - expect(document.activeElement).toHaveTextContent('Option 3') + await expect.element(page.getByRole('option', {name: 'Option 3', exact: true})).toHaveFocus() await userEvent.keyboard('{ArrowDown}') - expect(document.activeElement).toHaveTextContent('Option 4') + await expect.element(page.getByRole('option', {name: 'Option 4', exact: true})).toHaveFocus() await userEvent.keyboard('{ArrowDown}') - expect(document.activeElement).toHaveAccessibleName('Option 5') - expect(document.activeElement).toHaveAccessibleDescription('Unavailable due to an outage') + const inactiveOption = page.getByRole('option', {name: 'Option 5', exact: true}) + await expect.element(inactiveOption).toHaveFocus() + await expect.element(inactiveOption).toHaveAccessibleName('Option 5') + await expect.element(inactiveOption).toHaveAccessibleDescription('Unavailable due to an outage') await userEvent.keyboard('{ArrowUp}') - expect(document.activeElement).toHaveTextContent('Option 4') + await expect.element(page.getByRole('option', {name: 'Option 4', exact: true})).toHaveFocus() }) it('divider should support a custom `className`', () => { @@ -139,10 +141,10 @@ describe('ActionList', () => { ) await userEvent.tab() // tab into the story, this should focus on the first button - expect(document.activeElement).toHaveTextContent('Option 1') + await expect.element(page.getByRole('option', {name: 'Option 1', exact: true})).toHaveFocus() await userEvent.keyboard('{ArrowDown}') - expect(document.activeElement).toHaveTextContent('Option 1') + await expect.element(page.getByRole('option', {name: 'Option 1', exact: true})).toHaveFocus() expect(container.querySelector('li[aria-disabled="true"]')?.nextElementSibling).toHaveTextContent('Option 4') expect(container.querySelector('li[aria-disabled="true"]')?.nextElementSibling).toHaveAttribute('tabindex', '0') @@ -540,7 +542,7 @@ describe('ActionList with role="tree"', () => { , ) - const item = document.querySelector('[role="treeitem"]')! + const item = page.getByRole('treeitem') await userEvent.click(item) expect(onSelect).toHaveBeenCalledTimes(1) }) @@ -555,8 +557,10 @@ describe('ActionList with role="tree"', () => { , ) - const item = document.querySelector('[role="treeitem"]')! - ;(item as HTMLElement).focus() + const item = page.getByRole('treeitem') + act(() => { + item.element().focus() + }) await userEvent.keyboard('{Enter}') expect(onSelect).toHaveBeenCalledTimes(1) }) @@ -571,8 +575,8 @@ describe('ActionList with role="tree"', () => { , ) - const item = document.querySelector('[role="treeitem"]')! - await userEvent.click(item) + const item = page.getByRole('treeitem') + fireEvent.click(item.element()) expect(onSelect).not.toHaveBeenCalled() }) diff --git a/packages/react/src/ActionList/Description.test.tsx b/packages/react/src/ActionList/Description.test.tsx index 5312364912e..2f92d1ac41a 100644 --- a/packages/react/src/ActionList/Description.test.tsx +++ b/packages/react/src/ActionList/Description.test.tsx @@ -1,3 +1,4 @@ +import {page} from 'vitest/browser' import {expect, it, describe} from 'vitest' import {render as HTMLRender} from '@testing-library/react' import {ActionList} from '.' @@ -6,8 +7,8 @@ import classes from './ActionList.module.css' describe('ActionList.Description', () => { implementsClassName(ActionList.Description, classes.Description) - it('should render the description as inline without truncation by default', () => { - const {getByText} = HTMLRender( + it('should render the description as inline without truncation by default', async () => { + HTMLRender( Item 1Item 1 description @@ -15,14 +16,14 @@ describe('ActionList.Description', () => { , ) - const description = getByText('Item 1 description') - expect(description.tagName).toBe('SPAN') - expect(description).toHaveStyle('flex-basis: auto') - expect(description).not.toHaveStyle('overflow: ellipsis') - expect(description).not.toHaveStyle('white-space: nowrap') + const description = page.getByText('Item 1 description', {exact: true}) + expect(description.element().tagName).toBe('SPAN') + await expect.element(description).toHaveStyle('flex-basis: auto') + await expect.element(description).not.toHaveStyle('overflow: ellipsis') + await expect.element(description).not.toHaveStyle('white-space: nowrap') }) - it('should render the description as `Truncate` when truncate is true', () => { - const {getByText} = HTMLRender( + it('should render the description as `Truncate` when truncate is true', async () => { + HTMLRender( Item 1Item 1 description @@ -30,18 +31,18 @@ describe('ActionList.Description', () => { , ) - const description = getByText('Item 1 description') - expect(description.tagName).toBe('DIV') + const description = page.getByText('Item 1 description', {exact: true}) + expect(description.element().tagName).toBe('DIV') // For button-semantic items, the native title is suppressed in favor of // a keyboard-accessible Tooltip rendered by the parent Item. - expect(description).toHaveAttribute('title', '') - expect(description).toHaveStyle('flex-basis: auto') - expect(description).toHaveStyle('text-overflow: ellipsis') - expect(description).toHaveStyle('overflow: hidden') - expect(description).toHaveStyle('white-space: nowrap') + await expect.element(description).toHaveAttribute('title', '') + await expect.element(description).toHaveStyle('flex-basis: auto') + await expect.element(description).toHaveStyle('text-overflow: ellipsis') + await expect.element(description).toHaveStyle('overflow: hidden') + await expect.element(description).toHaveStyle('white-space: nowrap') }) it('should render the description in a new line when variant is block', () => { - const {getByText} = HTMLRender( + HTMLRender( Item 1Item 1 description @@ -49,9 +50,9 @@ describe('ActionList.Description', () => { , ) - const description = getByText('Item 1 description') - expect(description.tagName).toBe('SPAN') - expect(description.parentElement?.parentElement).toHaveAttribute( + const description = page.getByText('Item 1 description', {exact: true}) + expect(description.element().tagName).toBe('SPAN') + expect(description.element().parentElement?.parentElement).toHaveAttribute( 'data-component', 'ActionList.Item--DividerContainer', ) diff --git a/packages/react/src/ActionList/Group.test.tsx b/packages/react/src/ActionList/Group.test.tsx index 46d43d28923..034c26f8a98 100644 --- a/packages/react/src/ActionList/Group.test.tsx +++ b/packages/react/src/ActionList/Group.test.tsx @@ -1,3 +1,4 @@ +import {page} from 'vitest/browser' import {describe, it, expect} from 'vitest' import {render as HTMLRender} from '@testing-library/react' import {PlusIcon} from '@primer/octicons-react' @@ -45,7 +46,7 @@ describe('ActionList.Group', () => { }) it('should render the ActionList.GroupHeading component as a heading with the given heading level', async () => { - const container = HTMLRender( + HTMLRender( Heading @@ -53,9 +54,9 @@ describe('ActionList.Group', () => { , ) - const heading = container.getByRole('heading', {level: 2}) - expect(heading).toBeInTheDocument() - expect(heading).toHaveTextContent('Group Heading') + const heading = page.getByRole('heading', {level: 2}) + await expect.element(heading).toBeInTheDocument() + await expect.element(heading).toHaveTextContent('Group Heading') }) it('should throw an error if ActionList.GroupHeading is used without an `as` prop when no role is specified (for list role)', async () => { withExpectedConsoleError(() => { @@ -75,7 +76,7 @@ describe('ActionList.Group', () => { }) }) it('should render the ActionList.GroupHeading component as a span (not a heading tag) when role is specified as listbox', async () => { - const container = HTMLRender( + HTMLRender( Heading @@ -83,12 +84,12 @@ describe('ActionList.Group', () => { , ) - const label = container.getByText('Group Heading') - expect(label).toBeInTheDocument() - expect(label.tagName).toEqual('SPAN') + const label = page.getByText('Group Heading', {exact: true}) + await expect.element(label).toBeInTheDocument() + expect(label.element().tagName).toEqual('SPAN') }) it('should render the ActionList.GroupHeading component as a span with role="presentation" and aria-hidden="true" when role is specified as listbox', async () => { - const container = HTMLRender( + HTMLRender( Heading @@ -96,13 +97,13 @@ describe('ActionList.Group', () => { , ) - const label = container.getByText('Group Heading') - const wrapper = label.parentElement + const label = page.getByText('Group Heading', {exact: true}) + const wrapper = label.element().parentElement expect(wrapper).toHaveAttribute('role', 'presentation') expect(wrapper).toHaveAttribute('aria-hidden', 'true') }) it('should label the list with the group heading id', async () => { - const {container, getByText} = HTMLRender( + const {container} = HTMLRender( Heading @@ -112,11 +113,11 @@ describe('ActionList.Group', () => { , ) const list = container.querySelector(`li[data-test-id='ActionList.Group'] > ul`) - const heading = getByText('Group Heading') - expect(list).toHaveAttribute('aria-labelledby', heading.id) + const heading = page.getByText('Group Heading', {exact: true}) + expect(list).toHaveAttribute('aria-labelledby', heading.element().id) }) it('should NOT label the list with the group heading id when role is specified', async () => { - const {container, getByText} = HTMLRender( + const {container} = HTMLRender( Heading @@ -126,8 +127,8 @@ describe('ActionList.Group', () => { , ) const list = container.querySelector(`li[data-test-id='ActionList.Group'] > ul`) - const heading = getByText('Group Heading') - expect(list).not.toHaveAttribute('aria-labelledby', heading.id) + const heading = page.getByText('Group Heading', {exact: true}) + expect(list).not.toHaveAttribute('aria-labelledby', heading.element().id) }) it('should label the list with aria-label if it is specified', async () => { @@ -144,8 +145,8 @@ describe('ActionList.Group', () => { }) describe('GroupHeading.TrailingAction (behind feature flag)', () => { - it('renders GroupHeading.TrailingAction as a sibling of the heading element when the feature flag is enabled', () => { - const {getByRole} = HTMLRender( + it('renders GroupHeading.TrailingAction as a sibling of the heading element when the feature flag is enabled', async () => { + HTMLRender( Heading @@ -160,20 +161,20 @@ describe('ActionList.Group', () => { , ) - const heading = getByRole('heading', {level: 2}) - const button = getByRole('button', {name: 'New field'}) + const heading = page.getByRole('heading', {level: 2}) + const button = page.getByRole('button', {name: 'New field', exact: true}) // The button must NOT be inside the heading element - expect(heading).not.toContainElement(button) + await expect.element(heading).not.toContainElement(button.element()) // The heading text should not include the button's accessible name - expect(heading).toHaveTextContent('Group Heading') - expect(heading.textContent).not.toContain('New field') + await expect.element(heading).toHaveTextContent('Group Heading') + expect(heading.element().textContent).not.toContain('New field') // The button should be inside the same wrapper as the heading - expect(heading.parentElement).toContainElement(button) + expect(heading.element().parentElement).toContainElement(button.element()) }) - it('passes GroupHeading.TrailingAction through as a child of the heading when the feature flag is disabled', () => { - const {getByRole} = HTMLRender( + it('passes GroupHeading.TrailingAction through as a child of the heading when the feature flag is disabled', async () => { + HTMLRender( Heading @@ -186,12 +187,12 @@ describe('ActionList.Group', () => { , ) - const heading = getByRole('heading', {level: 2}) - const button = getByRole('button', {name: 'New field'}) + const heading = page.getByRole('heading', {level: 2}) + const button = page.getByRole('button', {name: 'New field', exact: true}) // Without the flag, the slot is not consumed: the button passes // through and still renders inside the heading element. - expect(heading).toContainElement(button) + await expect.element(heading).toContainElement(button.element()) }) it('throws when GroupHeading.TrailingAction is used inside an ActionMenu (menu role) and the feature flag is enabled', () => { diff --git a/packages/react/src/ActionList/Heading.test.tsx b/packages/react/src/ActionList/Heading.test.tsx index 2bd6b5f72c5..a4b98765384 100644 --- a/packages/react/src/ActionList/Heading.test.tsx +++ b/packages/react/src/ActionList/Heading.test.tsx @@ -1,3 +1,4 @@ +import {page} from 'vitest/browser' import {describe, it, expect} from 'vitest' import {render as HTMLRender} from '@testing-library/react' import BaseStyles from '../BaseStyles' @@ -20,61 +21,61 @@ describe('ActionList.Heading', () => { ) it('should render the ActionList.Heading component as a heading with the given heading level', async () => { - const container = HTMLRender( + HTMLRender( Heading , ) - const heading = container.getByRole('heading', {level: 1}) - expect(heading).toBeInTheDocument() - expect(heading).toHaveTextContent('Heading') + const heading = page.getByRole('heading', {level: 1}) + await expect.element(heading).toBeInTheDocument() + await expect.element(heading).toHaveTextContent('Heading') }) it('should not wrap the heading in a span', async () => { - const {getByRole} = HTMLRender( + HTMLRender( Heading , ) - const heading = getByRole('heading', {level: 1}) - expect(heading.parentElement?.tagName).not.toBe('SPAN') - expect(heading).toHaveClass(classes.ActionListHeader) + const heading = page.getByRole('heading', {level: 1}) + expect(heading.element().parentElement?.tagName).not.toBe('SPAN') + await expect.element(heading).toHaveClass(classes.ActionListHeader) }) it('should apply the visually-hidden class to the heading when visuallyHidden is set', async () => { - const {getByRole} = HTMLRender( + HTMLRender( Heading , ) - const heading = getByRole('heading', {level: 1}) - expect(heading).toHaveClass(visuallyHiddenClasses.InternalVisuallyHidden) - expect(heading).toHaveClass(classes.ActionListHeader) + const heading = page.getByRole('heading', {level: 1}) + await expect.element(heading).toHaveClass(visuallyHiddenClasses.InternalVisuallyHidden) + await expect.element(heading).toHaveClass(classes.ActionListHeader) }) it('should not apply the visually-hidden class to the heading by default', async () => { - const {getByRole} = HTMLRender( + HTMLRender( Heading , ) - const heading = getByRole('heading', {level: 1}) - expect(heading).not.toHaveClass(visuallyHiddenClasses.InternalVisuallyHidden) - expect(heading).toHaveClass(classes.ActionListHeader) + const heading = page.getByRole('heading', {level: 1}) + await expect.element(heading).not.toHaveClass(visuallyHiddenClasses.InternalVisuallyHidden) + await expect.element(heading).toHaveClass(classes.ActionListHeader) }) it('should label the action list with the heading id', async () => { - const {container, getByRole} = HTMLRender( + const {container} = HTMLRender( Heading Item , ) const list = container.querySelector('ul') - const heading = getByRole('heading', {level: 1}) - expect(list).toHaveAttribute('aria-labelledby', heading.id) + const heading = page.getByRole('heading', {level: 1}) + expect(list).toHaveAttribute('aria-labelledby', heading.element().id) }) it('should throw an error when ActionList.Heading is used within ActionMenu context', async () => { diff --git a/packages/react/src/ActionList/Item.test.tsx b/packages/react/src/ActionList/Item.test.tsx index 0a34bacba76..83af185a381 100644 --- a/packages/react/src/ActionList/Item.test.tsx +++ b/packages/react/src/ActionList/Item.test.tsx @@ -1,6 +1,6 @@ +import {page, userEvent} from 'vitest/browser' import {describe, it, expect, vi} from 'vitest' -import {render as HTMLRender, waitFor, fireEvent} from '@testing-library/react' -import userEvent from '@testing-library/user-event' +import {render as HTMLRender, fireEvent, act} from '@testing-library/react' import React, {type JSX} from 'react' import {ActionList} from '.' import {AnchoredOverlay} from '../AnchoredOverlay' @@ -67,102 +67,113 @@ describe('ActionList.Item', () => { implementsClassName(ActionList.LinkItem) it('should have aria-keyshortcuts applied to the correct element', async () => { const {container} = HTMLRender() - const linkOptions = await waitFor(() => container.querySelectorAll('a')) + const linkOptions = container.querySelectorAll('a') expect(linkOptions[0]).toHaveAttribute('aria-keyshortcuts', 'd') expect(linkOptions[0].parentElement).not.toHaveAttribute('aria-keyshortcuts', 'd') }) it('should fire onSelect on click and keypress', async () => { - const component = HTMLRender() - const options = await waitFor(() => component.getAllByRole('option')) - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[1]).toHaveAttribute('aria-selected', 'false') - fireEvent.click(options[1]) - expect(options[0]).toHaveAttribute('aria-selected', 'false') - expect(options[1]).toHaveAttribute('aria-selected', 'true') - // We pass keycode here to navigate a implementation detail in react-testing-library - // https://github.com/testing-library/react-testing-library/issues/269#issuecomment-455854112 - fireEvent.keyPress(options[0], {key: 'Enter', charCode: 13}) - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[1]).toHaveAttribute('aria-selected', 'false') - fireEvent.keyPress(options[1], {key: ' ', charCode: 32}) - expect(options[0]).toHaveAttribute('aria-selected', 'false') - expect(options[1]).toHaveAttribute('aria-selected', 'true') + HTMLRender() + const options = page.getByRole('option').all() + await expect.element(options[0]).toHaveAttribute('aria-selected', 'true') + await expect.element(options[1]).toHaveAttribute('aria-selected', 'false') + await userEvent.click(options[1]) + await expect.element(options[0]).toHaveAttribute('aria-selected', 'false') + await expect.element(options[1]).toHaveAttribute('aria-selected', 'true') + act(() => { + options[0].element().focus() + }) + await userEvent.keyboard('{Enter}') + await expect.element(options[0]).toHaveAttribute('aria-selected', 'true') + await expect.element(options[1]).toHaveAttribute('aria-selected', 'false') + act(() => { + options[1].element().focus() + }) + await userEvent.keyboard(' ') + await expect.element(options[0]).toHaveAttribute('aria-selected', 'false') + await expect.element(options[1]).toHaveAttribute('aria-selected', 'true') }) it('should skip onSelect on disabled items', async () => { - const component = HTMLRender() - const options = await waitFor(() => component.getAllByRole('option')) - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[2]).toHaveAttribute('aria-selected', 'false') - fireEvent.click(options[2]) - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[2]).toHaveAttribute('aria-selected', 'false') - fireEvent.keyPress(options[2], {key: 'Enter', charCode: 13}) - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[2]).toHaveAttribute('aria-selected', 'false') + HTMLRender() + const options = page.getByRole('option').all() + await expect.element(options[0]).toHaveAttribute('aria-selected', 'true') + await expect.element(options[2]).toHaveAttribute('aria-selected', 'false') + fireEvent.click(options[2].element()) + await expect.element(options[0]).toHaveAttribute('aria-selected', 'true') + await expect.element(options[2]).toHaveAttribute('aria-selected', 'false') + act(() => { + options[2].element().focus() + }) + await userEvent.keyboard('{Enter}') + await expect.element(options[0]).toHaveAttribute('aria-selected', 'true') + await expect.element(options[2]).toHaveAttribute('aria-selected', 'false') }) it('should skip onSelect on inactive items', async () => { - const component = HTMLRender() - const options = await waitFor(() => component.getAllByRole('option')) - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[3]).toHaveAttribute('aria-selected', 'false') - fireEvent.click(options[3]) - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[3]).toHaveAttribute('aria-selected', 'false') - fireEvent.keyPress(options[3], {key: 'Enter', charCode: 13}) - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[3]).toHaveAttribute('aria-selected', 'false') + HTMLRender() + const options = page.getByRole('option').all() + await expect.element(options[0]).toHaveAttribute('aria-selected', 'true') + await expect.element(options[3]).toHaveAttribute('aria-selected', 'false') + fireEvent.click(options[3].element()) + await expect.element(options[0]).toHaveAttribute('aria-selected', 'true') + await expect.element(options[3]).toHaveAttribute('aria-selected', 'false') + act(() => { + options[3].element().focus() + }) + await userEvent.keyboard('{Enter}') + await expect.element(options[0]).toHaveAttribute('aria-selected', 'true') + await expect.element(options[3]).toHaveAttribute('aria-selected', 'false') }) it('should skip onSelect on loading items', async () => { - const component = HTMLRender() - const options = await waitFor(() => component.getAllByRole('option')) - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[4]).toHaveAttribute('aria-selected', 'false') - fireEvent.click(options[4]) - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[4]).toHaveAttribute('aria-selected', 'false') - fireEvent.keyPress(options[3], {key: 'Enter', charCode: 13}) - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[4]).toHaveAttribute('aria-selected', 'false') + HTMLRender() + const options = page.getByRole('option').all() + await expect.element(options[0]).toHaveAttribute('aria-selected', 'true') + await expect.element(options[4]).toHaveAttribute('aria-selected', 'false') + fireEvent.click(options[4].element()) + await expect.element(options[0]).toHaveAttribute('aria-selected', 'true') + await expect.element(options[4]).toHaveAttribute('aria-selected', 'false') + act(() => { + options[3].element().focus() + }) + await userEvent.keyboard('{Enter}') + await expect.element(options[0]).toHaveAttribute('aria-selected', 'true') + await expect.element(options[4]).toHaveAttribute('aria-selected', 'false') }) it('should not crash when clicking an item without an onSelect', async () => { - const component = HTMLRender( + HTMLRender( Primer React , ) - const option = await waitFor(() => component.getByRole('option')) - expect(option).toBeInTheDocument() - fireEvent.click(option) - fireEvent.keyPress(option, {key: 'Enter', charCode: 13}) - expect(option).toBeInTheDocument() + const option = page.getByRole('option') + await expect.element(option).toBeInTheDocument() + await userEvent.click(option) + await userEvent.keyboard('{Enter}') + await expect.element(option).toBeInTheDocument() }) it('should focus the button around the alert icon when tabbing to an inactive item', async () => { - const component = HTMLRender() - const inactiveIndicatorButton = await waitFor(() => component.getByRole('button', {name: 'Inactive item'})) + HTMLRender() + const inactiveIndicatorButton = page.getByRole('button', {name: 'Inactive item', exact: true}) await userEvent.tab() await userEvent.tab() await userEvent.tab() await userEvent.tab() await userEvent.tab() await userEvent.tab() // focuses 6th element, which is inactive - expect(inactiveIndicatorButton).toHaveFocus() - expect(document.activeElement).toHaveAccessibleDescription('Unavailable due to an outage') + await expect.element(inactiveIndicatorButton).toHaveFocus() + await expect.element(inactiveIndicatorButton).toHaveAccessibleDescription('Unavailable due to an outage') }) it('should focus the option or menu item when moving focus to an inactive item **in a listbox**', async () => { - const component = HTMLRender() - const inactiveOption = await waitFor(() => component.getByRole('option', {name: projects[3].name})) + HTMLRender() + const inactiveOption = page.getByRole('option', {name: projects[3].name}) await userEvent.tab() // get focus on first element await userEvent.keyboard('{ArrowDown}') await userEvent.keyboard('{ArrowDown}') await userEvent.keyboard('{ArrowDown}') - expect(inactiveOption).toHaveFocus() - expect(document.activeElement).toHaveAccessibleDescription(projects[3].inactiveText as string) + await expect.element(inactiveOption).toHaveFocus() + await expect.element(inactiveOption).toHaveAccessibleDescription(projects[3].inactiveText as string) }) it('should behave as inactive if both inactiveText and loading props are passed', async () => { - const component = HTMLRender() - const inactiveIndicatorButton = await waitFor(() => - component.getByRole('button', {name: 'Loading and inactive item'}), - ) + HTMLRender() + const inactiveIndicatorButton = page.getByRole('button', {name: 'Loading and inactive item', exact: true}) await userEvent.tab() await userEvent.tab() await userEvent.tab() @@ -170,33 +181,33 @@ describe('ActionList.Item', () => { await userEvent.tab() await userEvent.tab() await userEvent.tab() // focuses 7th element, which is inactive AND has a loading prop - expect(inactiveIndicatorButton).toHaveFocus() - expect(document.activeElement).toHaveAccessibleDescription('Unavailable due to an outage') + await expect.element(inactiveIndicatorButton).toHaveFocus() + await expect.element(inactiveIndicatorButton).toHaveAccessibleDescription('Unavailable due to an outage') }) it('should behave as inactive if both inactiveText and loading props are passed **in a listbox**', async () => { - const component = HTMLRender() - const inactiveOption = await waitFor(() => component.getByRole('option', {name: projects[5].name})) + HTMLRender() + const inactiveOption = page.getByRole('option', {name: projects[5].name}) await userEvent.tab() // get focus on first element await userEvent.keyboard('{ArrowDown}') await userEvent.keyboard('{ArrowDown}') await userEvent.keyboard('{ArrowDown}') await userEvent.keyboard('{ArrowDown}') await userEvent.keyboard('{ArrowDown}') - expect(inactiveOption).toHaveFocus() - expect(document.activeElement).toHaveAccessibleDescription(projects[5].inactiveText as string) + await expect.element(inactiveOption).toHaveFocus() + await expect.element(inactiveOption).toHaveAccessibleDescription(projects[5].inactiveText as string) }) it('should call onClick for a link item', async () => { const onClick = vi.fn() - const component = HTMLRender( + HTMLRender( Primer React , ) - const link = await waitFor(() => component.getByRole('link')) - fireEvent.click(link) + const link = page.getByRole('link') + await userEvent.click(link) expect(onClick).toHaveBeenCalled() }) it('should render ActionList.Item as button', async () => { @@ -207,9 +218,9 @@ describe('ActionList.Item', () => { , ) const button = container.querySelector('button') - expect(button).toHaveTextContent('Item 1') + await expect.element(button).toHaveTextContent('Item 1') // Ensure passed prop "disabled" is applied to the button - expect(button).toHaveAttribute('aria-disabled', 'true') + await expect.element(button).toHaveAttribute('aria-disabled', 'true') const listItems = container.querySelectorAll('li') expect(listItems.length).toBe(2) }) @@ -254,7 +265,7 @@ describe('ActionList.Item', () => { }) it('should do action when trailing action is clicked', async () => { const onClick = vi.fn() - const component = HTMLRender( + HTMLRender( Item 1 @@ -262,8 +273,8 @@ describe('ActionList.Item', () => { , ) - const trailingAction = await waitFor(() => component.getByRole('button', {name: 'Action'})) - fireEvent.click(trailingAction) + const trailingAction = page.getByRole('button', {name: 'Action', exact: true}) + await userEvent.click(trailingAction) expect(onClick).toHaveBeenCalled() }) it('should focus the trailing action', async () => { @@ -276,26 +287,27 @@ describe('ActionList.Item', () => { , ) await userEvent.tab() - expect(document.activeElement).toHaveTextContent('Item 1') + await expect.element(page.getByRole('button', {name: 'Item 1', exact: true})).toHaveFocus() await userEvent.tab() - expect(document.activeElement).toHaveAccessibleName('Action') + await expect.element(page.getByRole('button', {name: 'Action', exact: true})).toHaveFocus() }) it('should only trigger a key event once', async () => { const mockOnSelect = vi.fn() - const user = userEvent.setup() - const {getByRole} = HTMLRender( + HTMLRender( Item 1 , ) - const item = getByRole('button') - item.focus() - expect(document.activeElement).toBe(item) - await user.keyboard('{Enter}') + const item = page.getByRole('button') + act(() => { + item.element().focus() + }) + await expect.element(item).toHaveFocus() + await userEvent.keyboard('{Enter}') expect(mockOnSelect).toHaveBeenCalledTimes(1) }) it('should not render buttons when role is specified', async () => { - const {getByRole} = HTMLRender( + HTMLRender( Item 1 Item 2 @@ -304,22 +316,22 @@ describe('ActionList.Item', () => { Item 5 , ) - const option = getByRole('option') - expect(option.tagName).toBe('LI') - expect(option.textContent).toBe('Item 1') - const menuItem = getByRole('menuitem') - expect(menuItem.tagName).toBe('LI') - const menuItemCheckbox = getByRole('menuitemcheckbox') - expect(menuItemCheckbox.tagName).toBe('LI') - const menuItemRadio = getByRole('menuitemradio') - expect(menuItemRadio.tagName).toBe('LI') - const button = getByRole('button') - expect(button.parentElement?.tagName).toBe('LI') - expect(button.textContent).toBe('Item 5') + const option = page.getByRole('option') + expect(option.element().tagName).toBe('LI') + expect(option.element().textContent).toBe('Item 1') + const menuItem = page.getByRole('menuitem') + expect(menuItem.element().tagName).toBe('LI') + const menuItemCheckbox = page.getByRole('menuitemcheckbox') + expect(menuItemCheckbox.element().tagName).toBe('LI') + const menuItemRadio = page.getByRole('menuitemradio') + expect(menuItemRadio.element().tagName).toBe('LI') + const button = page.getByRole('button') + expect(button.element().parentElement?.tagName).toBe('LI') + expect(button.element().textContent).toBe('Item 5') }) it('should add `role="option"` if `role="listbox"` and `selectionVariant` is present', async () => { - const {getAllByRole} = HTMLRender( + HTMLRender( Item 1 Item 2 @@ -327,84 +339,82 @@ describe('ActionList.Item', () => { Item 4 , ) - const options = getAllByRole('option') - expect(options[0]).toBeInTheDocument() + const options = page.getByRole('option').all() + await expect.element(options[0]).toBeInTheDocument() expect(options).toHaveLength(4) }) - it('should add `aria-describedby` to items with a description', () => { - const {getByRole} = HTMLRender( + it('should add `aria-describedby` to items with a description', async () => { + HTMLRender( Item, Description , ) - const item = getByRole('button') - expect(item).toHaveAttribute('aria-describedby') - expect(item).toHaveTextContent('Item, Description') - expect(item).toHaveAccessibleDescription('Description') + const item = page.getByRole('button') + await expect.element(item).toHaveAttribute('aria-describedby') + await expect.element(item).toHaveTextContent('Item, Description') + await expect.element(item).toHaveAccessibleDescription('Description') }) - it('should add `aria-describedby` to items with a description when `role=listbox` is applied', () => { - const {getByRole} = HTMLRender( + it('should add `aria-describedby` to items with a description when `role=listbox` is applied', async () => { + HTMLRender( Item, Description , ) - const item = getByRole('option') - expect(item).toHaveAttribute('aria-describedby') - expect(item).toHaveTextContent('Item, Description') - expect(item).toHaveAccessibleDescription('Description') + const item = page.getByRole('option') + await expect.element(item).toHaveAttribute('aria-describedby') + await expect.element(item).toHaveTextContent('Item, Description') + await expect.element(item).toHaveAccessibleDescription('Description') }) - it('should add role="tab" when ActionList has role="tablist"', () => { - const {getAllByRole} = HTMLRender( + it('should add role="tab" when ActionList has role="tablist"', async () => { + HTMLRender( Tab 1 Tab 2 Tab 3 , ) - const tabs = getAllByRole('tab') - expect(tabs[0]).toBeInTheDocument() + const tabs = page.getByRole('tab').all() + await expect.element(tabs[0]).toBeInTheDocument() expect(tabs).toHaveLength(3) }) - it('should allow role="tab" on the li element', () => { - const {getAllByRole} = HTMLRender( + it('should allow role="tab" on the li element', async () => { + HTMLRender( Tab 1 Tab 2 Tab 3 , ) - const tabs = getAllByRole('tab') - expect(tabs[0]).toBeInTheDocument() - expect(tabs[0].nodeType).toBe(Node.ELEMENT_NODE) + const tabs = page.getByRole('tab').all() + await expect.element(tabs[0]).toBeInTheDocument() + expect(tabs[0].element().nodeType).toBe(Node.ELEMENT_NODE) expect(tabs).toHaveLength(3) }) it('should forward a list-semantic item ref only to the outer list item', () => { const ref = vi.fn<(node: HTMLLIElement | null) => void>() - const {getByRole} = HTMLRender( + HTMLRender( Primer React , ) - const option = getByRole('option') + const option = page.getByRole('option') - expect(ref.mock.calls.filter(([node]) => node !== null)).toEqual([[option]]) - expect(option.tagName).toBe('LI') + expect(ref.mock.calls.filter(([node]) => node !== null)).toEqual([[option.element()]]) + expect(option.element().tagName).toBe('LI') }) it('should preserve consumer ref when tooltip wraps trigger', async () => { - const user = userEvent.setup() - function TestComponent() { const anchorRef = React.useRef(null) const [open, setOpen] = React.useState(false) @@ -433,18 +443,16 @@ describe('ActionList.Item', () => { ) } - const {getByText, queryByRole} = HTMLRender() + HTMLRender() // Overlay should not be visible initially - expect(queryByRole('menu')).not.toBeInTheDocument() + await expect.element(page.getByRole('menu')).not.toBeInTheDocument() // Click the item to open the anchored overlay - await user.click(getByText('Convert to issue')) + await userEvent.click(page.getByText('Convert to issue', {exact: true})) // The overlay should open — this fails if Tooltip overwrites the consumer ref - await waitFor(() => { - expect(queryByRole('menu')).toBeInTheDocument() - }) + await expect.element(page.getByRole('menu')).toBeInTheDocument() }) describe('data attributes derived from slots', () => { diff --git a/packages/react/src/ActionList/LinkItem.test.tsx b/packages/react/src/ActionList/LinkItem.test.tsx index 3c2536ab531..4ff5a0de035 100644 --- a/packages/react/src/ActionList/LinkItem.test.tsx +++ b/packages/react/src/ActionList/LinkItem.test.tsx @@ -1,5 +1,6 @@ +import {page, userEvent} from 'vitest/browser' import {describe, it, expect, vi} from 'vitest' -import {render, screen, fireEvent} from '@testing-library/react' +import {render} from '@testing-library/react' import React from 'react' import {ActionList} from '.' import Link from '../Link' @@ -8,16 +9,16 @@ import {implementsClassName} from '../utils/testing' describe('ActionList.LinkItem', () => { implementsClassName(ActionList.LinkItem) - it('renders as an anchor by default', () => { + it('renders as an anchor by default', async () => { render( Home , ) - const link = screen.getByRole('link', {name: 'Home'}) - expect(link).toHaveAttribute('href', '#home') - expect(link.tagName).toBe('A') + const link = page.getByRole('link', {name: 'Home', exact: true}) + await expect.element(link).toHaveAttribute('href', '#home') + expect(link.element().tagName).toBe('A') }) it('forwards its ref to the link when wrapped in a tooltip', () => { @@ -31,10 +32,10 @@ describe('ActionList.LinkItem', () => { , ) - expect(ref.current).toBe(screen.getByRole('link', {name: 'Home'})) + expect(ref.current).toBe(page.getByRole('link', {name: 'Home', exact: true}).element()) }) - it('calls onClick handler', () => { + it('calls onClick handler', async () => { const onClick = vi.fn() render( @@ -45,12 +46,12 @@ describe('ActionList.LinkItem', () => { , ) - fireEvent.click(screen.getByRole('link', {name: 'Home'})) + await userEvent.click(page.getByRole('link', {name: 'Home', exact: true})) expect(onClick).toHaveBeenCalledTimes(1) }) describe('as prop', () => { - it('supports polymorphic LinkItem with as prop', () => { + it('supports polymorphic LinkItem with as prop', async () => { const CustomLink = React.forwardRef< HTMLAnchorElement, React.AnchorHTMLAttributes & {custom: boolean} @@ -69,12 +70,12 @@ describe('ActionList.LinkItem', () => { , ) - const link = screen.getByRole('link', {name: 'Docs'}) - expect(link).toHaveAttribute('data-custom-link', 'true') - expect(link).toHaveAttribute('href', '#docs') + const link = page.getByRole('link', {name: 'Docs', exact: true}) + await expect.element(link).toHaveAttribute('data-custom-link', 'true') + await expect.element(link).toHaveAttribute('href', '#docs') }) - it('passes through additional props to the element specified by as', () => { + it('passes through additional props to the element specified by as', async () => { render( @@ -83,13 +84,13 @@ describe('ActionList.LinkItem', () => { , ) - const homeLink = screen.getByTestId('home-link') - expect(homeLink).toHaveAttribute('href', '#home') - expect(homeLink).toHaveAttribute('data-inline', 'true') + const homeLink = page.getByTestId('home-link') + await expect.element(homeLink).toHaveAttribute('href', '#home') + await expect.element(homeLink).toHaveAttribute('data-inline', 'true') }) }) - it('renders inactive text when inactiveText is provided', () => { + it('renders inactive text when inactiveText is provided', async () => { render( @@ -98,6 +99,6 @@ describe('ActionList.LinkItem', () => { , ) - expect(screen.getByText('Unavailable due to an outage')).toBeInTheDocument() + await expect.element(page.getByText('Unavailable due to an outage', {exact: true})).toBeInTheDocument() }) }) diff --git a/packages/react/src/ActionMenu/ActionMenu.test.tsx b/packages/react/src/ActionMenu/ActionMenu.test.tsx index bec8b1b18a3..62168df2fdb 100644 --- a/packages/react/src/ActionMenu/ActionMenu.test.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.test.tsx @@ -1,6 +1,6 @@ +import {page, userEvent} from 'vitest/browser' import {describe, expect, it, vi, beforeEach} from 'vitest' -import {render as HTMLRender, waitFor, act, within} from '@testing-library/react' -import userEvent from '@testing-library/user-event' +import {render as HTMLRender, waitFor, act} from '@testing-library/react' import type React from 'react' import {useRef, useState} from 'react' import BaseStyles from '../BaseStyles' @@ -185,63 +185,62 @@ describe('ActionMenu', () => { it('renders data-component attributes for ActionMenu parts', async () => { const component = HTMLRender() - const user = userEvent.setup() - const trigger = component.getByRole('button', {name: 'Toggle Menu'}) - expect(trigger).toHaveAttribute('data-component', 'ActionMenu.Button') + const trigger = page.getByRole('button', {name: 'Toggle Menu', exact: true}) + await expect.element(trigger).toHaveAttribute('data-component', 'ActionMenu.Button') - await user.click(trigger) + await userEvent.click(trigger) expect(component.baseElement.querySelector('[data-component="ActionMenu.Overlay"]')).not.toBeNull() expect(component.baseElement.querySelector('[data-component="AnchoredOverlay"]')).toBeNull() }) it('should open Menu on MenuButton click', async () => { - const component = HTMLRender() - const button = component.getByRole('button') + HTMLRender() + const button = page.getByRole('button') - const user = userEvent.setup() - await user.click(button) + await userEvent.click(button) - expect(component.getByRole('menu')).toBeInTheDocument() + await expect.element(page.getByRole('menu')).toBeInTheDocument() }) it('should open Menu on MenuButton keypress', async () => { - const component = HTMLRender() - const button = component.getByRole('button') + HTMLRender() + const button = page.getByRole('button') - const user = userEvent.setup() - button.focus() - await user.keyboard('{Enter}') + act(() => { + button.element().focus() + }) + await userEvent.keyboard('{Enter}') - expect(component.getByRole('menu')).toBeInTheDocument() + await expect.element(page.getByRole('menu')).toBeInTheDocument() }) it('should close Menu on selecting an action with click', async () => { - const component = HTMLRender() - const button = component.getByRole('button') + HTMLRender() + const button = page.getByRole('button') - const user = userEvent.setup() - await user.click(button) + await userEvent.click(button) - const menuItems = component.getAllByRole('menuitem') - await user.click(menuItems[0]) + const menuItems = page.getByRole('menuitem').all() + await userEvent.click(menuItems[0]) - expect(component.queryByRole('menu')).toBeNull() + await expect.element(page.getByRole('menu')).not.toBeInTheDocument() }) it('should close Menu on selecting an action with Enter', async () => { - const component = HTMLRender() - const button = component.getByRole('button') + HTMLRender() + const button = page.getByRole('button') - const user = userEvent.setup() - await user.click(button) + await userEvent.click(button) - const menuItems = component.getAllByRole('menuitem') - menuItems[0].focus() - await user.keyboard('{Enter}') + const menuItems = page.getByRole('menuitem').all() + act(() => { + menuItems[0].element().focus() + }) + await userEvent.keyboard('{Enter}') - expect(component.queryByRole('menu')).toBeNull() + await expect.element(page.getByRole('menu')).not.toBeInTheDocument() }) describe('preventFocusOnClose', () => { @@ -256,37 +255,39 @@ describe('ActionMenu', () => { {gesture: 'Escape', preventFocusOnClose: true}, {gesture: 'outside-click', preventFocusOnClose: true}, ])('handles $gesture with preventFocusOnClose=$preventFocusOnClose', async ({gesture, preventFocusOnClose}) => { - const component = HTMLRender( + HTMLRender( <> , ) - const user = userEvent.setup() - const trigger = component.getByRole('button', {name: 'Toggle Menu'}) - await user.click(trigger) - const item = component.getByRole('menuitem', {name: 'New file'}) - item.focus() + const trigger = page.getByRole('button', {name: 'Toggle Menu', exact: true}) + const triggerElement = trigger.element() + await userEvent.click(trigger) + const item = page.getByRole('menuitem', {name: 'New file', exact: true}) + act(() => { + item.element().focus() + }) - const focus = vi.spyOn(trigger, 'focus') + const focus = vi.spyOn(triggerElement, 'focus') if (gesture === 'item-click') { - await user.click(item) + await userEvent.click(item) } else if (gesture === 'outside-click') { - await user.click(component.getByRole('button', {name: 'Outside'})) + await userEvent.click(page.getByRole('button', {name: 'Outside', exact: true})) } else { - await user.keyboard(gesture === 'Space' ? ' ' : `{${gesture}}`) + await userEvent.keyboard(gesture === 'Space' ? ' ' : `{${gesture}}`) } - expect(component.queryByRole('menu')).not.toBeInTheDocument() + await expect.element(page.getByRole('menu')).not.toBeInTheDocument() if (preventFocusOnClose) { expect(focus).not.toHaveBeenCalled() - expect(trigger).not.toHaveFocus() + await expect.element(trigger).not.toHaveFocus() } else { expect(focus).toHaveBeenCalled() - expect(trigger).toHaveFocus() + await expect.element(trigger).toHaveFocus() } if (gesture === 'outside-click') { - expect(component.getByRole('button', {name: 'Outside'})).toHaveFocus() + await expect.element(page.getByRole('button', {name: 'Outside', exact: true})).toHaveFocus() } }) @@ -294,7 +295,7 @@ describe('ActionMenu', () => { 'handles anchor-click closure with preventFocusOnClose=%s', async preventFocusOnClose => { const returnFocusRef = {current: null as HTMLButtonElement | null} - const component = HTMLRender( + HTMLRender( @@ -307,21 +308,20 @@ describe('ActionMenu', () => { , ) - const user = userEvent.setup() - const trigger = component.getByRole('button', {name: 'Toggle Menu'}) - const returnFocusTarget = component.getByRole('button', {name: 'Return focus'}) + const trigger = page.getByRole('button', {name: 'Toggle Menu', exact: true}) + const returnFocusTarget = page.getByRole('button', {name: 'Return focus', exact: true}) - await user.click(trigger) - const focus = vi.spyOn(returnFocusTarget, 'focus') - await user.click(trigger) + await userEvent.click(trigger) + const focus = vi.spyOn(returnFocusTarget.element(), 'focus') + await userEvent.click(trigger) - expect(component.queryByRole('menu')).not.toBeInTheDocument() + await expect.element(page.getByRole('menu')).not.toBeInTheDocument() if (preventFocusOnClose) { expect(focus).not.toHaveBeenCalled() - expect(returnFocusTarget).not.toHaveFocus() + await expect.element(returnFocusTarget).not.toHaveFocus() } else { expect(focus).toHaveBeenCalled() - expect(returnFocusTarget).toHaveFocus() + await expect.element(returnFocusTarget).toHaveFocus() } }, ) @@ -343,12 +343,12 @@ describe('ActionMenu', () => { const component = HTMLRender(example(true)) const focus = vi.spyOn(returnFocusRef.current, 'focus') - await waitFor(() => expect(component.getByRole('menuitem', {name: 'New file'})).toHaveFocus()) + await expect.element(page.getByRole('menuitem', {name: 'New file', exact: true})).toHaveFocus() component.rerender(example(false)) - expect(component.queryByRole('menu')).not.toBeInTheDocument() + await expect.element(page.getByRole('menu')).not.toBeInTheDocument() expect(focus).not.toHaveBeenCalled() - expect(component.getByRole('button', {name: 'Toggle Menu'})).not.toHaveFocus() + await expect.element(page.getByRole('button', {name: 'Toggle Menu', exact: true})).not.toHaveFocus() }) it('does not focus a captured anchor that an action replaces', async () => { @@ -370,208 +370,199 @@ describe('ActionMenu', () => { ) } - const component = HTMLRender() - const user = userEvent.setup() - const trigger = component.getByRole('button', {name: 'Link'}) - await user.click(trigger) - const focus = vi.spyOn(trigger, 'focus') + HTMLRender() + const trigger = page.getByRole('button', {name: 'Link', exact: true}) + const triggerElement = trigger.element() + await userEvent.click(trigger) + const focus = vi.spyOn(triggerElement, 'focus') - await user.click(component.getByRole('menuitem', {name: 'Convert to preview'})) + await userEvent.click(page.getByRole('menuitem', {name: 'Convert to preview', exact: true})) - expect(component.queryByRole('menu')).not.toBeInTheDocument() - expect(trigger).not.toBeInTheDocument() - expect(component.getByRole('button', {name: 'Preview'})).not.toHaveFocus() + await expect.element(page.getByRole('menu')).not.toBeInTheDocument() + expect(triggerElement).not.toBeInTheDocument() + await expect.element(page.getByRole('button', {name: 'Preview', exact: true})).not.toHaveFocus() expect(focus).not.toHaveBeenCalled() }) }) it('should not close Menu if event is prevented', async () => { - const component = HTMLRender() - const button = component.getByRole('button') + HTMLRender() + const button = page.getByRole('button') - const user = userEvent.setup() - await user.click(button) + await userEvent.click(button) - const menuItems = component.getAllByRole('menuitem') - await user.click(menuItems[3]) + const menuItems = page.getByRole('menuitem').all() + await userEvent.click(menuItems[3]) // menu should still be open - expect(component.getByRole('menu')).toBeInTheDocument() + await expect.element(page.getByRole('menu')).toBeInTheDocument() }) it('should be able to select an Item with selectionVariant', async () => { - const component = HTMLRender() - const button = component.getByRole('button', {name: /^options/i}) + HTMLRender() + const button = page.getByRole('button', {name: /^options/i}) - const user = userEvent.setup() - await user.click(button) + await userEvent.click(button) // select first item by role, that would close the menu - await user.click(component.getAllByRole('menuitemradio')[0]) - expect(component.queryByRole('menu')).not.toBeInTheDocument() + await userEvent.click(page.getByRole('menuitemradio').all()[0]) + await expect.element(page.getByRole('menu')).not.toBeInTheDocument() // open menu again and check if the first option is checked - await user.click(button) - expect(component.getAllByRole('menuitemradio')[0]).toHaveAttribute('aria-checked', 'true') + await userEvent.click(button) + await expect.element(page.getByRole('menuitemradio').all()[0]).toHaveAttribute('aria-checked', 'true') }) it('should assign the right roles with groups & mixed selectionVariant', async () => { - const component = HTMLRender() + HTMLRender() - const button = component.getByRole('button', { + const button = page.getByRole('button', { name: 'Group by Stage', + exact: true, }) - const user = userEvent.setup() - await user.click(button) + await userEvent.click(button) - expect(component.getByLabelText('Status')).toHaveAttribute('role', 'menuitemradio') - expect(component.getByLabelText('Clear Group by')).toHaveAttribute('role', 'menuitem') + await expect.element(page.getByLabelText('Status', {exact: true})).toHaveAttribute('role', 'menuitemradio') + await expect.element(page.getByLabelText('Clear Group by', {exact: true})).toHaveAttribute('role', 'menuitem') }) it('should be able to select an Item with aria-keyshortcuts after opening Menu with click', async () => { - const component = HTMLRender() - const button = component.getByRole('button') + HTMLRender() + const button = page.getByRole('button') - const user = userEvent.setup() - await user.click(button) + await userEvent.click(button) - expect(component.queryByRole('menu')).toBeInTheDocument() + await expect.element(page.getByRole('menu')).toBeInTheDocument() // linkItem button is the active element at this point - await user.keyboard('{ArrowDown}{s}') + await userEvent.keyboard('{ArrowDown}{s}') - expect(component.getAllByRole('menuitem')[4]).toEqual(document.activeElement) - await user.keyboard('{ArrowUp}') - expect(component.getAllByRole('menuitem')[3]).toEqual(document.activeElement) + await expect.element(page.getByRole('menuitem').all()[4]).toHaveFocus() + await userEvent.keyboard('{ArrowUp}') + await expect.element(page.getByRole('menuitem').all()[3]).toHaveFocus() // assumes mnemonics aria-keyshortcuts are ignored - await user.keyboard('{g}') - expect(component.getAllByRole('menuitem')[3]).toEqual(document.activeElement) + await userEvent.keyboard('{g}') + await expect.element(page.getByRole('menuitem').all()[3]).toHaveFocus() }) it('should select last element when ArrowUp is pressed after opening Menu with click', async () => { - const component = HTMLRender() + HTMLRender() - const button = component.getByRole('button') + const button = page.getByRole('button') - const user = userEvent.setup() - await user.click(button) + await userEvent.click(button) - expect(component.queryByRole('menu')).toBeInTheDocument() + await expect.element(page.getByRole('menu')).toBeInTheDocument() // button should be the active element // assumes button is the active element at this point - await user.keyboard('{ArrowUp}') + await userEvent.keyboard('{ArrowUp}') - expect(component.getAllByRole('menuitem').pop()).toEqual(document.activeElement) + await expect.element(page.getByRole('menuitem').last()).toHaveFocus() }) it('should close the menu if Tab is pressed and move to next element', async () => { - const component = HTMLRender( + HTMLRender( <> , ) - const anchor = component.getByRole('button') + const anchor = page.getByRole('button') - const user = userEvent.setup() - await user.tab() // tab into the story, this should focus on the first button - expect(anchor).toEqual(document.activeElement) // trust, but verify + await userEvent.tab() // tab into the story, this should focus on the first button + await expect.element(anchor).toHaveFocus() // trust, but verify - await user.keyboard('{Enter}') - expect(component.queryByRole('menu')).toBeInTheDocument() - expect(component.getAllByRole('menuitem')[0]).toEqual(document.activeElement) + await userEvent.keyboard('{Enter}') + await expect.element(page.getByRole('menu')).toBeInTheDocument() + await expect.element(page.getByRole('menuitem').all()[0]).toHaveFocus() - // TODO: revisit why we need this waitFor - await waitFor(async () => { - await user.tab() - expect(document.activeElement).toEqual(component.getByPlaceholderText('next focusable element')) - expect(component.queryByRole('menu')).not.toBeInTheDocument() - }) + await userEvent.tab() + await expect.element(page.getByPlaceholder('next focusable element')).toHaveFocus() + await expect.element(page.getByRole('menu')).not.toBeInTheDocument() }) it('should wrap focus when ArrowDown is pressed on the last element', async () => { - const component = HTMLRender() - const button = component.getByRole('button') + HTMLRender() + const button = page.getByRole('button') - const user = userEvent.setup() - await user.click(button) + await userEvent.click(button) - expect(component.queryByRole('menu')).toBeInTheDocument() - const menuItems = component.getAllByRole('menuitem') + await expect.element(page.getByRole('menu')).toBeInTheDocument() + const menuItems = page.getByRole('menuitem').all() // TODO: Fix the focus trap from taking over focus control // https://github.com/primer/react/issues/6434 // expect(menuItems[0]).toEqual(document.activeElement) - await user.keyboard('{ArrowDown}') - expect(menuItems[1]).toEqual(document.activeElement) + await userEvent.keyboard('{ArrowDown}') + await expect.element(menuItems[1]).toHaveFocus() // TODO: Removed one ArrowDown to account for the focus trap starting at the second element - // await user.keyboard('{ArrowDown}') - await user.keyboard('{ArrowDown}') - await user.keyboard('{ArrowDown}') - await user.keyboard('{ArrowDown}') - expect(menuItems[menuItems.length - 1]).toEqual(document.activeElement) // last elememt - - await user.keyboard('{ArrowDown}') - expect(menuItems[0]).toEqual(document.activeElement) // wrap to first + // await userEvent.keyboard('{ArrowDown}') + await userEvent.keyboard('{ArrowDown}') + await userEvent.keyboard('{ArrowDown}') + await userEvent.keyboard('{ArrowDown}') + await expect.element(menuItems[menuItems.length - 1]).toHaveFocus() // last elememt + + await userEvent.keyboard('{ArrowDown}') + await expect.element(menuItems[0]).toHaveFocus() // wrap to first }) it('should open menu on menu button click and it is wrapped with tooltip', async () => { - const component = HTMLRender() - const button = component.getByRole('button') + HTMLRender() + const button = page.getByRole('button') - const user = userEvent.setup() - await user.click(button) + await userEvent.click(button) - expect(component.getByRole('menu')).toBeInTheDocument() + await expect.element(page.getByRole('menu')).toBeInTheDocument() }) it('should open menu on menu button click and it is wrapped with tooltip v2', async () => { - const component = HTMLRender( + HTMLRender( ExampleWithTooltipV2( Toggle Menu , ), ) - const button = component.getByRole('button') + const button = page.getByRole('button') - const user = userEvent.setup() - await user.click(button) + await userEvent.click(button) - expect(component.getByRole('menu')).toBeInTheDocument() + await expect.element(page.getByRole('menu')).toBeInTheDocument() }) it('should display tooltip when menu button is focused', async () => { - const component = HTMLRender() - const button = component.getByRole('button') - button.focus() - expect(component.getByRole('tooltip')).toBeInTheDocument() + HTMLRender() + const button = page.getByRole('button') + act(() => { + button.element().focus() + }) + await expect.element(page.getByRole('tooltip')).toBeInTheDocument() }) it('should display tooltip v2 when menu button is focused', async () => { - const component = HTMLRender( + HTMLRender( ExampleWithTooltipV2( Toggle Menu , ), ) - const button = component.getByRole('button') + const button = page.getByRole('button') act(() => { - button.focus() + button.element().focus() }) - expect(component.getByRole('tooltip', {hidden: true})).toBeInTheDocument() + await expect.element(page.getByRole('tooltip', {includeHidden: true})).toBeInTheDocument() }) it('should open menu on menu anchor click and it is wrapped with tooltip v2', async () => { - const component = HTMLRender( + HTMLRender( ExampleWithTooltipV2( @@ -580,16 +571,15 @@ describe('ActionMenu', () => { , ), ) - const button = component.getByRole('button') + const button = page.getByRole('button') - const user = userEvent.setup() - await user.click(button) + await userEvent.click(button) - expect(component.getByRole('menu')).toBeInTheDocument() + await expect.element(page.getByRole('menu')).toBeInTheDocument() }) it('should display tooltip v2 and menu anchor is focused', async () => { - const component = HTMLRender( + HTMLRender( ExampleWithTooltipV2( @@ -598,17 +588,17 @@ describe('ActionMenu', () => { , ), ) - const button = component.getByRole('button') + const button = page.getByRole('button') act(() => { - button.focus() + button.element().focus() }) - expect(component.getByRole('tooltip', {hidden: true})).toBeInTheDocument() + await expect.element(page.getByRole('tooltip', {includeHidden: true})).toBeInTheDocument() }) it('should pass the "id" prop from ActionMenu.Button to the HTML button', async () => { const buttonId = 'toggle-menu-custom-id' - const component = HTMLRender( + HTMLRender( Toggle Menu @@ -629,13 +619,13 @@ describe('ActionMenu', () => { , ) - const button = component.getByRole('button') + const button = page.getByRole('button') - expect(button.id).toBe(buttonId) + expect(button.element().id).toBe(buttonId) }) it('should pass the "id" prop from ActionMenu.Anchor to anchor child', async () => { const buttonId = 'toggle-menu-custom-id' - const component = HTMLRender( + HTMLRender( @@ -658,13 +648,13 @@ describe('ActionMenu', () => { , ) - const button = component.getByRole('button') + const button = page.getByRole('button') - expect(button.id).toBe(buttonId) + expect(button.element().id).toBe(buttonId) }) it('should use the tooltip id to name the menu when the anchor is icon button', async () => { - const component = HTMLRender( + HTMLRender( @@ -683,133 +673,131 @@ describe('ActionMenu', () => { , ) - const toggleButton = component.getByRole('button', {name: 'More actions'}) + const toggleButton = page.getByRole('button', {name: 'More actions', exact: true}) await userEvent.click(toggleButton) - expect(toggleButton).toHaveAttribute('aria-labelledby') - expect(component.getByRole('menu')).toHaveAttribute('aria-labelledby', toggleButton.getAttribute('aria-labelledby')) + await expect.element(toggleButton).toHaveAttribute('aria-labelledby') + await expect + .element(page.getByRole('menu')) + .toHaveAttribute('aria-labelledby', toggleButton.element().getAttribute('aria-labelledby')) }) describe('submenus', () => { it('sets `aria-haspopup` and `aria-expanded` on submenu anchors', async () => { - const component = HTMLRender() - const user = userEvent.setup() + HTMLRender() - const baseAnchor = component.getByRole('button', {name: 'Toggle Menu'}) - await user.click(baseAnchor) + const baseAnchor = page.getByRole('button', {name: 'Toggle Menu', exact: true}) + await userEvent.click(baseAnchor) - const submenuAnchor = component.getByRole('menuitem', {name: 'Paste special'}) - expect(submenuAnchor).toHaveAttribute('aria-haspopup') - await user.click(submenuAnchor) - expect(submenuAnchor).toHaveAttribute('aria-expanded') + const submenuAnchor = page.getByRole('menuitem', {name: 'Paste special', exact: true}) + await expect.element(submenuAnchor).toHaveAttribute('aria-haspopup') + await userEvent.click(submenuAnchor) + await expect.element(submenuAnchor).toHaveAttribute('aria-expanded') - const subSubmenuAnchor = component.getByRole('menuitem', {name: 'Paste from'}) - expect(subSubmenuAnchor).toHaveAttribute('aria-haspopup') - await user.click(subSubmenuAnchor) - expect(subSubmenuAnchor).toHaveAttribute('aria-expanded') + const subSubmenuAnchor = page.getByRole('menuitem', {name: 'Paste from', exact: true}) + await expect.element(subSubmenuAnchor).toHaveAttribute('aria-haspopup') + await userEvent.click(subSubmenuAnchor) + await expect.element(subSubmenuAnchor).toHaveAttribute('aria-expanded') }) it('sets labels on submenus', async () => { - const component = HTMLRender() - const user = userEvent.setup() + HTMLRender() - const baseAnchor = component.getByRole('button', {name: 'Toggle Menu'}) - await user.click(baseAnchor) + const baseAnchor = page.getByRole('button', {name: 'Toggle Menu', exact: true}) + await userEvent.click(baseAnchor) - const submenuAnchor = component.getByRole('menuitem', {name: 'Paste special'}) - await user.click(submenuAnchor) + const submenuAnchor = page.getByRole('menuitem', {name: 'Paste special', exact: true}) + await userEvent.click(submenuAnchor) - const submenu = component.getByRole('menu', {name: 'Paste special'}) - await waitFor(() => { - expect(submenu).toBeVisible() - }) + const submenu = page.getByRole('menu', {name: 'Paste special', exact: true}) + await expect.element(submenu).toBeVisible() - const subSubmenuAnchor = within(submenu).getByRole('menuitem', {name: 'Paste from'}) - await user.click(subSubmenuAnchor) + const subSubmenuAnchor = submenu.getByRole('menuitem', {name: 'Paste from', exact: true}) + await userEvent.click(subSubmenuAnchor) - const subSubmenu = component.getByRole('menu', {name: 'Paste from'}) + const subSubmenu = page.getByRole('menu', {name: 'Paste from', exact: true}) - await waitFor(() => { - expect(subSubmenu).toBeVisible() - }) + await expect.element(subSubmenu).toBeVisible() }) it('does not open top-level menu on right arrow key press', async () => { - const component = HTMLRender() - const user = userEvent.setup() + HTMLRender() - const baseAnchor = component.getByRole('button', {name: 'Toggle Menu'}) - baseAnchor.focus() + const baseAnchor = page.getByRole('button', {name: 'Toggle Menu', exact: true}) + act(() => { + baseAnchor.element().focus() + }) - await user.keyboard('{ArrowRight}') - expect(component.queryByRole('menu')).not.toBeInTheDocument() - expect(baseAnchor).not.toHaveAttribute('aria-expanded', 'true') + await userEvent.keyboard('{ArrowRight}') + await expect.element(page.getByRole('menu')).not.toBeInTheDocument() + await expect.element(baseAnchor).not.toHaveAttribute('aria-expanded', 'true') }) it('opens submenus on enter or right arrow key press', async () => { - const component = HTMLRender() - const user = userEvent.setup() - - const baseAnchor = component.getByRole('button', {name: 'Toggle Menu'}) - await user.click(baseAnchor) - - const submenuAnchor = component.getByRole('menuitem', {name: 'Paste special'}) - expect(submenuAnchor).toHaveAttribute('aria-haspopup', 'true') - submenuAnchor.focus() - await user.keyboard('{Enter}') - expect(submenuAnchor).toHaveAttribute('aria-expanded', 'true') - - const subSubmenuAnchor = component.getByRole('menuitem', {name: 'Paste from'}) - subSubmenuAnchor.focus() - await user.keyboard('{ArrowRight}') - expect(subSubmenuAnchor).toHaveAttribute('aria-expanded', 'true') + HTMLRender() + + const baseAnchor = page.getByRole('button', {name: 'Toggle Menu', exact: true}) + await userEvent.click(baseAnchor) + + const submenuAnchor = page.getByRole('menuitem', {name: 'Paste special', exact: true}) + await expect.element(submenuAnchor).toHaveAttribute('aria-haspopup', 'true') + act(() => { + submenuAnchor.element().focus() + }) + await userEvent.keyboard('{Enter}') + await expect.element(submenuAnchor).toHaveAttribute('aria-expanded', 'true') + + const subSubmenuAnchor = page.getByRole('menuitem', {name: 'Paste from', exact: true}) + act(() => { + subSubmenuAnchor.element().focus() + }) + await userEvent.keyboard('{ArrowRight}') + await expect.element(subSubmenuAnchor).toHaveAttribute('aria-expanded', 'true') }) it('closes top menu on escape or left arrow key press', async () => { - const component = HTMLRender() - const user = userEvent.setup() + HTMLRender() - const baseAnchor = component.getByRole('button', {name: 'Toggle Menu'}) - await user.click(baseAnchor) + const baseAnchor = page.getByRole('button', {name: 'Toggle Menu', exact: true}) + await userEvent.click(baseAnchor) - const submenuAnchor = component.getByRole('menuitem', {name: 'Paste special'}) - await user.click(submenuAnchor) + const submenuAnchor = page.getByRole('menuitem', {name: 'Paste special', exact: true}) + await userEvent.click(submenuAnchor) - const subSubmenuAnchor = component.getByRole('menuitem', {name: 'Paste from'}) - await user.click(subSubmenuAnchor) + const subSubmenuAnchor = page.getByRole('menuitem', {name: 'Paste from', exact: true}) + await userEvent.click(subSubmenuAnchor) - expect(subSubmenuAnchor).toHaveAttribute('aria-expanded', 'true') + await expect.element(subSubmenuAnchor).toHaveAttribute('aria-expanded', 'true') - await user.keyboard('{Escape}') - expect(subSubmenuAnchor).not.toHaveAttribute('aria-expanded', 'true') - expect(submenuAnchor).toHaveAttribute('aria-expanded', 'true') + await userEvent.keyboard('{Escape}') + await expect.element(subSubmenuAnchor).not.toHaveAttribute('aria-expanded', 'true') + await expect.element(submenuAnchor).toHaveAttribute('aria-expanded', 'true') - await user.keyboard('{ArrowLeft}') - expect(submenuAnchor).not.toHaveAttribute('aria-expanded', 'true') + await userEvent.keyboard('{ArrowLeft}') + await expect.element(submenuAnchor).not.toHaveAttribute('aria-expanded', 'true') - expect(baseAnchor).toHaveAttribute('aria-expanded', 'true') + await expect.element(baseAnchor).toHaveAttribute('aria-expanded', 'true') }) it('closes all menus when an item is selected', async () => { - const component = HTMLRender() - const user = userEvent.setup() + HTMLRender() - const baseAnchor = component.getByRole('button', {name: 'Toggle Menu'}) - await user.click(baseAnchor) + const baseAnchor = page.getByRole('button', {name: 'Toggle Menu', exact: true}) + await userEvent.click(baseAnchor) - const submenuAnchor = component.getByRole('menuitem', {name: 'Paste special'}) - await user.click(submenuAnchor) + const submenuAnchor = page.getByRole('menuitem', {name: 'Paste special', exact: true}) + await userEvent.click(submenuAnchor) - const subSubmenuAnchor = component.getByRole('menuitem', {name: 'Paste from'}) - await user.click(subSubmenuAnchor) + const subSubmenuAnchor = page.getByRole('menuitem', {name: 'Paste from', exact: true}) + await userEvent.click(subSubmenuAnchor) - const subSubmenuItem = component.getByRole('menuitem', {name: 'Current clipboard'}) - await user.click(subSubmenuItem) + const subSubmenuItem = page.getByRole('menuitem', {name: 'Current clipboard', exact: true}) + await userEvent.click(subSubmenuItem) - expect(baseAnchor).not.toHaveAttribute('aria-expanded', 'true') + await expect.element(baseAnchor).not.toHaveAttribute('aria-expanded', 'true') }) it('supports className prop on ActionMenu.Anchor with css anchor positioning flag', async () => { - const component = HTMLRender( + HTMLRender( @@ -834,12 +822,12 @@ describe('ActionMenu', () => { , ) - const anchor = component.getByRole('button', {name: 'Toggle Menu'}) - expect(anchor).toHaveClass('test-class') + const anchor = page.getByRole('button', {name: 'Toggle Menu', exact: true}) + await expect.element(anchor).toHaveClass('test-class') }) it('supports className prop on ActionMenu.Button with css anchor positioning flag', async () => { - const component = HTMLRender( + HTMLRender( @@ -862,12 +850,12 @@ describe('ActionMenu', () => { , ) - const button = component.getByRole('button', {name: 'Toggle Menu'}) - expect(button).toHaveClass('test-class') + const button = page.getByRole('button', {name: 'Toggle Menu', exact: true}) + await expect.element(button).toHaveClass('test-class') }) it('supports className prop on ActionMenu.Anchor', async () => { - const component = HTMLRender( + HTMLRender( @@ -892,12 +880,12 @@ describe('ActionMenu', () => { , ) - const anchor = component.getByRole('button', {name: 'Toggle Menu'}) - expect(anchor).toHaveClass('test-class') + const anchor = page.getByRole('button', {name: 'Toggle Menu', exact: true}) + await expect.element(anchor).toHaveClass('test-class') }) it('supports className prop on ActionMenu.Button', async () => { - const component = HTMLRender( + HTMLRender( @@ -920,36 +908,36 @@ describe('ActionMenu', () => { , ) - const button = component.getByRole('button', {name: 'Toggle Menu'}) - expect(button).toHaveClass('test-class') + const button = page.getByRole('button', {name: 'Toggle Menu', exact: true}) + await expect.element(button).toHaveClass('test-class') }) it('keeps anchor-name and position-anchor linked when the anchor is replaced while the menu is open', async () => { - const user = userEvent.setup() const component = HTMLRender() - const initialAnchor = component.getByRole('button', {name: 'Open menu'}) - await user.click(initialAnchor) + const initialAnchor = page.getByRole('button', {name: 'Open menu', exact: true}) + const initialAnchorElement = initialAnchor.element() + await userEvent.click(initialAnchor) const overlay = component.baseElement.querySelector('[data-component="ActionMenu.Overlay"]') as HTMLElement expect(overlay).not.toBeNull() - const initialAnchorName = initialAnchor.style.getPropertyValue('anchor-name') + const initialAnchorName = initialAnchor.element().style.getPropertyValue('anchor-name') const initialPositionAnchor = overlay.style.getPropertyValue('position-anchor') expect(initialAnchorName).not.toBe('') expect(initialPositionAnchor).not.toBe('') expect(initialPositionAnchor).toBe(initialAnchorName) // Click the item that remounts the anchor while keeping the menu open - const switchItem = component.getByRole('menuitem', {name: 'Switch anchor'}) - await user.click(switchItem) + const switchItem = page.getByRole('menuitem', {name: 'Switch anchor', exact: true}) + await userEvent.click(switchItem) - const newAnchor = component.getByRole('button', {name: 'Open menu'}) - expect(newAnchor).not.toBe(initialAnchor) + const newAnchor = page.getByRole('button', {name: 'Open menu', exact: true}) + expect(newAnchor.element()).not.toBe(initialAnchorElement) // The new anchor should have the same anchor-name re-applied, and the // overlay should still reference it via position-anchor. - expect(newAnchor.style.getPropertyValue('anchor-name')).toBe(initialAnchorName) + expect(newAnchor.element().style.getPropertyValue('anchor-name')).toBe(initialAnchorName) expect(overlay.style.getPropertyValue('position-anchor')).toBe(initialPositionAnchor) }) }) @@ -959,7 +947,7 @@ describe('ActionMenu', () => { const mockOnClick = vi.fn() const mockOnKeyDown = vi.fn() - const component = HTMLRender( + HTMLRender( Open menu @@ -973,21 +961,20 @@ describe('ActionMenu', () => { , ) - const user = userEvent.setup() - const button = component.getByRole('button') - await user.click(button) + const button = page.getByRole('button') + await userEvent.click(button) - expect(component.getByRole('menu')).toBeInTheDocument() + await expect.element(page.getByRole('menu')).toBeInTheDocument() expect(mockOnClick).toHaveBeenCalledTimes(1) // select and close menu - const menuItems = component.getAllByRole('menuitem') - await user.click(menuItems[0]) - expect(component.queryByRole('menu')).toBeNull() + const menuItems = page.getByRole('menuitem').all() + await userEvent.click(menuItems[0]) + await expect.element(page.getByRole('menu')).not.toBeInTheDocument() - expect(button).toEqual(document.activeElement) - await user.keyboard('{Enter}') - expect(component.queryByRole('menu')).toBeInTheDocument() + await expect.element(button).toHaveFocus() + await userEvent.keyboard('{Enter}') + await expect.element(page.getByRole('menu')).toBeInTheDocument() expect(mockOnKeyDown).toHaveBeenCalledTimes(1) }) @@ -995,7 +982,7 @@ describe('ActionMenu', () => { const mockOnClick = vi.fn() const mockOnKeyDown = vi.fn() - const component = HTMLRender( + HTMLRender( { , ) - const user = userEvent.setup() - const button = component.getByRole('button') - await user.click(button) + const button = page.getByRole('button') + await userEvent.click(button) - expect(component.getByRole('menu')).toBeInTheDocument() + await expect.element(page.getByRole('menu')).toBeInTheDocument() expect(mockOnClick).toHaveBeenCalledTimes(1) // select and close menu - const menuItems = component.getAllByRole('menuitem') - await user.click(menuItems[0]) - expect(component.queryByRole('menu')).toBeNull() + const menuItems = page.getByRole('menuitem').all() + await userEvent.click(menuItems[0]) + await expect.element(page.getByRole('menu')).not.toBeInTheDocument() - expect(button).toEqual(document.activeElement) - await user.keyboard('{Enter}') - expect(component.queryByRole('menu')).toBeInTheDocument() + await expect.element(button).toHaveFocus() + await userEvent.keyboard('{Enter}') + await expect.element(page.getByRole('menu')).toBeInTheDocument() expect(mockOnKeyDown).toHaveBeenCalledTimes(1) }) @@ -1036,7 +1022,7 @@ describe('ActionMenu', () => { const mockOnClick = vi.fn() const mockOnKeyDown = vi.fn() - const component = HTMLRender( + HTMLRender( @@ -1052,21 +1038,20 @@ describe('ActionMenu', () => { , ) - const user = userEvent.setup() - const button = component.getByRole('button') - await user.click(button) + const button = page.getByRole('button') + await userEvent.click(button) - expect(component.getByRole('menu')).toBeInTheDocument() + await expect.element(page.getByRole('menu')).toBeInTheDocument() expect(mockOnClick).toHaveBeenCalledTimes(1) // select and close menu - const menuItems = component.getAllByRole('menuitem') - await user.click(menuItems[0]) - expect(component.queryByRole('menu')).toBeNull() + const menuItems = page.getByRole('menuitem').all() + await userEvent.click(menuItems[0]) + await expect.element(page.getByRole('menu')).not.toBeInTheDocument() - expect(button).toEqual(document.activeElement) - await user.keyboard('{Enter}') - expect(component.queryByRole('menu')).toBeInTheDocument() + await expect.element(button).toHaveFocus() + await userEvent.keyboard('{Enter}') + await expect.element(page.getByRole('menu')).toBeInTheDocument() expect(mockOnKeyDown).toHaveBeenCalledTimes(1) }) @@ -1074,7 +1059,7 @@ describe('ActionMenu', () => { const mockOnClick = vi.fn() const mockOnKeyDown = vi.fn() - const component = HTMLRender( + HTMLRender( @@ -1095,21 +1080,22 @@ describe('ActionMenu', () => { , ) - const user = userEvent.setup() - const button = component.getByRole('button') - await user.click(button) + const button = page.getByRole('button') + await userEvent.click(button) - expect(component.getByRole('menu')).toBeInTheDocument() + await expect.element(page.getByRole('menu')).toBeInTheDocument() expect(mockOnClick).toHaveBeenCalledTimes(1) + await userEvent.unhover(button) + // select and close menu - const menuItems = component.getAllByRole('menuitem') - await user.click(menuItems[0]) - expect(component.queryByRole('menu')).toBeNull() + const menuItems = page.getByRole('menuitem').all() + await userEvent.click(menuItems[0]) + await expect.element(page.getByRole('menu')).not.toBeInTheDocument() - expect(button).toEqual(document.activeElement) - await user.keyboard('{Enter}') - expect(component.queryByRole('menu')).toBeInTheDocument() + await expect.element(button).toHaveFocus() + await userEvent.keyboard('{Enter}') + await expect.element(page.getByRole('menu')).toBeInTheDocument() expect(mockOnKeyDown).toHaveBeenCalledTimes(1) }) }) @@ -1125,7 +1111,7 @@ describe('ActionMenu', () => { it('should enable displayInViewport when ActionMenu is inside a dialog', async () => { // When the ActionMenu is wrapped in a Dialog, it's inside a dialog context. // displayInViewport should be automatically enabled. - const component = HTMLRender( + HTMLRender( {}}> Toggle Menu @@ -1138,13 +1124,10 @@ describe('ActionMenu', () => { , ) - const user = userEvent.setup() - const button = component.getByRole('button', {name: 'Toggle Menu'}) - await user.click(button) + const button = page.getByRole('button', {name: 'Toggle Menu', exact: true}) + await userEvent.click(button) - await waitFor(() => { - expect(component.queryByRole('menu')).toBeInTheDocument() - }) + await expect.element(page.getByRole('menu')).toBeInTheDocument() // Verify getAnchoredPosition was called with displayInViewport: true await waitFor(() => { @@ -1159,7 +1142,7 @@ describe('ActionMenu', () => { it('should not enable displayInViewport when ActionMenu is NOT inside a dialog', async () => { // Without being wrapped in a Dialog, the ActionMenu is not in a dialog context. // displayInViewport should remain at its default (false/undefined). - const component = HTMLRender( + HTMLRender( Toggle Menu @@ -1170,13 +1153,10 @@ describe('ActionMenu', () => { , ) - const user = userEvent.setup() - const button = component.getByRole('button') - await user.click(button) + const button = page.getByRole('button') + await userEvent.click(button) - await waitFor(() => { - expect(component.queryByRole('menu')).toBeInTheDocument() - }) + await expect.element(page.getByRole('menu')).toBeInTheDocument() // Verify getAnchoredPosition was called without displayInViewport enabled await waitFor(() => { @@ -1191,7 +1171,7 @@ describe('ActionMenu', () => { it('should respect explicit displayInViewport prop over default logic', async () => { // Test that an explicit displayInViewport=false prop overrides the automatic // detection, even when the ActionMenu is inside a dialog. - const component = HTMLRender( + HTMLRender( {}}> Toggle Menu @@ -1204,13 +1184,10 @@ describe('ActionMenu', () => { , ) - const user = userEvent.setup() - const button = component.getByRole('button', {name: 'Toggle Menu'}) - await user.click(button) + const button = page.getByRole('button', {name: 'Toggle Menu', exact: true}) + await userEvent.click(button) - await waitFor(() => { - expect(component.queryByRole('menu')).toBeInTheDocument() - }) + await expect.element(page.getByRole('menu')).toBeInTheDocument() // Verify getAnchoredPosition was called with displayInViewport: false (explicit override) await waitFor(() => { @@ -1225,7 +1202,7 @@ describe('ActionMenu', () => { it('should respect explicit displayInViewport=true prop', async () => { // Test that an explicit displayInViewport=true prop works regardless of // the dialog context. - const component = HTMLRender( + HTMLRender( Toggle Menu @@ -1236,13 +1213,10 @@ describe('ActionMenu', () => { , ) - const user = userEvent.setup() - const button = component.getByRole('button') - await user.click(button) + const button = page.getByRole('button') + await userEvent.click(button) - await waitFor(() => { - expect(component.queryByRole('menu')).toBeInTheDocument() - }) + await expect.element(page.getByRole('menu')).toBeInTheDocument() // Verify getAnchoredPosition was called with displayInViewport: true (explicit override) await waitFor(() => { diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx index 557cc896ee6..2ac3942e87d 100644 --- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx +++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx @@ -1,7 +1,7 @@ +import {page, userEvent} from 'vitest/browser' import {createRef, useCallback, useRef, useState} from 'react' import {describe, expect, it, vi} from 'vitest' -import {act, fireEvent, render} from '@testing-library/react' -import userEvent from '@testing-library/user-event' +import {act, render} from '@testing-library/react' import {AnchoredOverlay} from '../AnchoredOverlay' import {Button} from '../Button' import BaseStyles from '../BaseStyles' @@ -97,14 +97,14 @@ describe.each([true, false])( it('should call onOpen when the anchor is clicked', async () => { const mockOpenCallback = vi.fn() const mockCloseCallback = vi.fn() - const anchoredOverlay = render( + render( , ) - const anchor = anchoredOverlay.baseElement.querySelector('[aria-haspopup="true"]')! + const anchor = page.getByRole('button', {name: 'Anchor Button', exact: true}) await userEvent.click(anchor) expect(mockOpenCallback).toHaveBeenCalledTimes(1) @@ -115,15 +115,18 @@ describe.each([true, false])( it('should call onOpen when the anchor activated by a key press', async () => { const mockOpenCallback = vi.fn() const mockCloseCallback = vi.fn() - const anchoredOverlay = render( + render( , ) - const anchor = anchoredOverlay.baseElement.querySelector('[aria-haspopup="true"]')! - fireEvent.keyDown(anchor, {key: ' '}) + const anchor = page.getByRole('button', {name: 'Anchor Button', exact: true}) + act(() => { + anchor.element().focus() + }) + await userEvent.keyboard(' ') expect(mockOpenCallback).toHaveBeenCalledTimes(1) expect(mockOpenCallback).toHaveBeenCalledWith('anchor-key-press') @@ -133,15 +136,20 @@ describe.each([true, false])( it('should call onClose when the user clicks off of the overlay', async () => { const mockOpenCallback = vi.fn() const mockCloseCallback = vi.fn() - const anchoredOverlay = render( - , + render( + <> + + + , ) - await userEvent.click(anchoredOverlay.baseElement) + await userEvent.click(page.getByRole('button', {name: 'Outside overlay', exact: true})) expect(mockOpenCallback).toHaveBeenCalledTimes(0) expect(mockCloseCallback).toHaveBeenCalledTimes(1) @@ -277,7 +285,7 @@ describe('AnchoredOverlay scroll/resize cascade', () => { ) // Open then close the overlay so position has a non-undefined last value. - await userEvent.click(document.body.querySelector('button')!) + await userEvent.click(page.getByRole('button', {name: 'Toggle', exact: true})) await userEvent.keyboard('{Escape}') // Let any pending close-time effects flush. @@ -706,7 +714,7 @@ describe('AnchoredOverlay CSS anchor positioning viewport handling', () => { }) describe('AnchoredOverlay anchor element replacement', () => { - it('should re-apply anchor-name to a new anchor DOM element when the overlay reopens', () => { + it('should re-apply anchor-name to a new anchor DOM element when the overlay reopens', async () => { function TestComponent() { const anchorRef = useRef(null) const [open, setOpen] = useState(true) @@ -736,31 +744,25 @@ describe('AnchoredOverlay anchor element replacement', () => { ) } - const {baseElement} = render() + render() // Verify anchor-name is set on the initial anchor element - const initialAnchor = baseElement.querySelector('[data-testid="anchor"]') as HTMLElement + const initialAnchor = page.getByTestId('anchor').element() expect(initialAnchor.style.getPropertyValue('anchor-name')).not.toBe('') const anchorName = initialAnchor.style.getPropertyValue('anchor-name') // Close the overlay - const toggleButton = baseElement.querySelector('[data-testid="toggle"]') as HTMLElement - act(() => { - toggleButton.click() - }) + const toggleButton = page.getByTestId('toggle') + await userEvent.click(toggleButton) // Replace the anchor DOM element by changing its key while overlay is closed - const switchButton = baseElement.querySelector('[data-testid="switch"]') as HTMLElement - act(() => { - switchButton.click() - }) + const switchButton = page.getByTestId('switch') + await userEvent.click(switchButton) // Reopen the overlay — the new anchor should get anchor-name before paint - act(() => { - toggleButton.click() - }) + await userEvent.click(toggleButton) - const newAnchor = baseElement.querySelector('[data-testid="anchor"]') as HTMLElement + const newAnchor = page.getByTestId('anchor').element() expect(newAnchor).not.toBe(initialAnchor) expect(newAnchor.style.getPropertyValue('anchor-name')).toBe(anchorName) })