diff --git a/package-lock.json b/package-lock.json index 352901f6ad7..695c5cc8f69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,8 +82,8 @@ "react-dom": "^18.3.1" }, "devDependencies": { - "@primer/react": "38.22.0", - "@primer/styled-react": "1.0.7", + "@primer/react": "38.23.0", + "@primer/styled-react": "1.0.8", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.3", @@ -96,8 +96,8 @@ "name": "example-nextjs", "version": "0.0.0", "dependencies": { - "@primer/react": "38.22.0", - "@primer/styled-react": "1.0.7", + "@primer/react": "38.23.0", + "@primer/styled-react": "1.0.8", "next": "^16.1.7", "react": "^19.2.0", "react-dom": "^19.2.0", @@ -139,8 +139,8 @@ "version": "0.0.0", "dependencies": { "@primer/octicons-react": "^19.21.0", - "@primer/react": "38.22.0", - "@primer/styled-react": "1.0.7", + "@primer/react": "38.23.0", + "@primer/styled-react": "1.0.8", "clsx": "^2.1.1", "next": "^16.1.7", "react": "^19.2.0", @@ -7072,6 +7072,10 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/@primer/vitest-config": { + "resolved": "packages/vitest-config", + "link": true + }, "node_modules/@publint/pack": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@publint/pack/-/pack-0.1.2.tgz", @@ -11666,7 +11670,6 @@ }, "node_modules/chalk": { "version": "5.4.1", - "dev": true, "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -26686,6 +26689,20 @@ } } }, + "node_modules/vitest-fail-on-console": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/vitest-fail-on-console/-/vitest-fail-on-console-0.10.1.tgz", + "integrity": "sha512-Xjy2SpgND547qSy0s0zYVnh1G/WyGtdjAbi4PFV8mkYRmTq+6NzRUJYdc08BHrw7HJLpO2kMxHFB8PWn7FOVsg==", + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1" + }, + "peerDependencies": { + "@vitest/utils": ">=0.26.2", + "vite": ">=4.5.2", + "vitest": ">=0.26.2" + } + }, "node_modules/vitest/node_modules/@vitest/expect": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz", @@ -27318,6 +27335,7 @@ "@babel/plugin-transform-runtime": "^7.29.0", "@babel/preset-env": "^7.29.0", "@babel/preset-typescript": "^7.28.5", + "@primer/vitest-config": "^0.0.0", "@rollup/plugin-babel": "^6.1.0", "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-json": "^6.1.0", @@ -27698,6 +27716,7 @@ "postcss-preset-env": "^10.1.3" }, "devDependencies": { + "@primer/vitest-config": "^0.0.0", "@types/postcss-mixins": "9.0.5", "cssnano": "^7.0.7", "postcss": "^8.4.41", @@ -27711,7 +27730,7 @@ }, "packages/react": { "name": "@primer/react", - "version": "38.22.0", + "version": "38.23.0", "license": "MIT", "dependencies": { "@github/mini-throttle": "^2.1.1", @@ -27748,6 +27767,7 @@ "@figma/code-connect": "1.3.2", "@primer/css": "^21.5.1", "@primer/doc-gen": "^0.0.1", + "@primer/vitest-config": "^0.0.0", "@rollup/plugin-babel": "6.1.0", "@rollup/plugin-commonjs": "29.0.0", "@rollup/plugin-json": "6.1.0", @@ -28084,7 +28104,7 @@ }, "packages/styled-react": { "name": "@primer/styled-react", - "version": "1.0.7", + "version": "1.0.8", "dependencies": { "@styled-system/css": "^5.1.5", "@styled-system/props": "^5.1.5", @@ -28101,7 +28121,8 @@ "@babel/preset-react": "^7.28.5", "@babel/preset-typescript": "^7.28.5", "@primer/primitives": "10.x || 11.x", - "@primer/react": "^38.22.0", + "@primer/react": "^38.23.0", + "@primer/vitest-config": "^0.0.0", "@rollup/plugin-babel": "^6.1.0", "@storybook/react-vite": "^10.3.3", "@types/react": "18.3.11", @@ -28250,6 +28271,13 @@ "funding": { "url": "https://github.com/sponsors/isaacs" } + }, + "packages/vitest-config": { + "name": "@primer/vitest-config", + "version": "0.0.0", + "dependencies": { + "vitest-fail-on-console": "^0.10.1" + } } } } diff --git a/packages/doc-gen/package.json b/packages/doc-gen/package.json index 548f56e0865..e2e261c1490 100644 --- a/packages/doc-gen/package.json +++ b/packages/doc-gen/package.json @@ -39,6 +39,7 @@ "@babel/plugin-transform-runtime": "^7.29.0", "@babel/preset-env": "^7.29.0", "@babel/preset-typescript": "^7.28.5", + "@primer/vitest-config": "^0.0.0", "@rollup/plugin-babel": "^6.1.0", "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-json": "^6.1.0", diff --git a/packages/doc-gen/vitest.config.mts b/packages/doc-gen/vitest.config.mts index 1f571e49350..5aed29b3f45 100644 --- a/packages/doc-gen/vitest.config.mts +++ b/packages/doc-gen/vitest.config.mts @@ -6,6 +6,7 @@ export default defineConfig({ }, test: { environment: 'node', + setupFiles: ['@primer/vitest-config/setup'], detectAsyncLeaks: true, }, }) diff --git a/packages/postcss-preset-primer/package.json b/packages/postcss-preset-primer/package.json index e4cd7dfec16..bc6ade9a93c 100644 --- a/packages/postcss-preset-primer/package.json +++ b/packages/postcss-preset-primer/package.json @@ -19,6 +19,7 @@ "postcss-preset-env": "^10.1.3" }, "devDependencies": { + "@primer/vitest-config": "^0.0.0", "@types/postcss-mixins": "9.0.5", "cssnano": "^7.0.7", "postcss": "^8.4.41", diff --git a/packages/postcss-preset-primer/vitest.config.ts b/packages/postcss-preset-primer/vitest.config.ts index 74213f595be..c699c977ba3 100644 --- a/packages/postcss-preset-primer/vitest.config.ts +++ b/packages/postcss-preset-primer/vitest.config.ts @@ -3,6 +3,7 @@ import {defineConfig} from 'vitest/config' export default defineConfig({ test: { environment: 'node', + setupFiles: ['@primer/vitest-config/setup'], detectAsyncLeaks: true, }, }) diff --git a/packages/react/package.json b/packages/react/package.json index 665179de202..23957c3569b 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -108,6 +108,7 @@ "@figma/code-connect": "1.3.2", "@primer/css": "^21.5.1", "@primer/doc-gen": "^0.0.1", + "@primer/vitest-config": "^0.0.0", "@rollup/plugin-babel": "6.1.0", "@rollup/plugin-commonjs": "29.0.0", "@rollup/plugin-json": "6.1.0", diff --git a/packages/react/src/ActionList/Group.test.tsx b/packages/react/src/ActionList/Group.test.tsx index 7d282da6296..2fc32e83df0 100644 --- a/packages/react/src/ActionList/Group.test.tsx +++ b/packages/react/src/ActionList/Group.test.tsx @@ -1,4 +1,4 @@ -import {describe, it, expect} from 'vitest' +import {describe, it, expect, vi} from 'vitest' import {render as HTMLRender} from '@testing-library/react' import {PlusIcon} from '@primer/octicons-react' import BaseStyles from '../BaseStyles' @@ -22,22 +22,23 @@ describe('ActionList.Group', () => { implementsClassName(ActionList.GroupHeading, classes.GroupHeading) it('should throw an error when ActionList.GroupHeading has an `as` prop when it is used within ActionMenu context', async () => { - expect(() => - HTMLRender( - - - Trigger - - - - Group Heading - - - - - , - ), - ).toThrow( + expect.hasAssertions() + expectRenderError( + () => + HTMLRender( + + + Trigger + + + + Group Heading + + + + + , + ), "Looks like you are trying to set a heading level to a menu role. Group headings for menu type action lists are for representational purposes, and rendered as divs. Therefore they don't need a heading level.", ) }) @@ -56,17 +57,18 @@ describe('ActionList.Group', () => { expect(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 () => { - expect(() => - HTMLRender( - - Heading - - Group Heading - Item - - , - ), - ).toThrow( + expect.hasAssertions() + expectRenderError( + () => + HTMLRender( + + Heading + + Group Heading + Item + + , + ), "You are setting a heading for a list, that requires a heading level. Please use 'as' prop to set a proper heading level.", ) }) @@ -191,44 +193,59 @@ describe('ActionList.Group', () => { }) it('throws when GroupHeading.TrailingAction is used inside an ActionMenu (menu role) and the feature flag is enabled', () => { - expect(() => - HTMLRender( - - - - Trigger - - - - - Group Heading - - - - - - - - , - ), - ).toThrow(/can not be used inside an ActionList with an ARIA role of "menu"/) + expect.hasAssertions() + expectRenderError( + () => + HTMLRender( + + + + Trigger + + + + + Group Heading + + + + + + + + , + ), + /can not be used inside an ActionList with an ARIA role of "menu"/, + ) }) it('throws when GroupHeading.TrailingAction is used inside a listbox role and the feature flag is enabled', () => { - expect(() => - HTMLRender( - - - - - Group Heading - - - - - , - ), - ).toThrow(/can not be used inside an ActionList with an ARIA role of "listbox"/) + expect.hasAssertions() + expectRenderError( + () => + HTMLRender( + + + + + Group Heading + + + + + , + ), + /can not be used inside an ActionList with an ARIA role of "listbox"/, + ) }) }) }) + +function expectRenderError(callback: () => void, error: string | RegExp) { + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) + try { + expect(callback).toThrow(error) + } finally { + consoleError.mockRestore() + } +} diff --git a/packages/react/src/ActionList/Heading.test.tsx b/packages/react/src/ActionList/Heading.test.tsx index 316d4bad06d..c41a366e17a 100644 --- a/packages/react/src/ActionList/Heading.test.tsx +++ b/packages/react/src/ActionList/Heading.test.tsx @@ -1,4 +1,4 @@ -import {describe, it, expect} from 'vitest' +import {describe, it, expect, vi} from 'vitest' import {render as HTMLRender} from '@testing-library/react' import BaseStyles from '../BaseStyles' import {ActionList} from '.' @@ -42,22 +42,32 @@ describe('ActionList.Heading', () => { }) it('should throw an error when ActionList.Heading is used within ActionMenu context', async () => { - expect(() => - HTMLRender( - - - Trigger - - - Heading - Item - - - - , - ), - ).toThrow( + expect.hasAssertions() + expectRenderError( + () => + HTMLRender( + + + Trigger + + + Heading + Item + + + + , + ), "ActionList.Heading shouldn't be used within an ActionMenu container. Menus are labelled by the menu button's name.", ) }) }) + +function expectRenderError(callback: () => void, error: string | RegExp) { + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) + try { + expect(callback).toThrow(error) + } finally { + consoleError.mockRestore() + } +} diff --git a/packages/react/src/ActionMenu/ActionMenu.test.tsx b/packages/react/src/ActionMenu/ActionMenu.test.tsx index 09303b7923b..2533711a6f4 100644 --- a/packages/react/src/ActionMenu/ActionMenu.test.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.test.tsx @@ -340,9 +340,7 @@ describe('ActionMenu', () => { const button = component.getByRole('button') const user = userEvent.setup() - await act(async () => { - await user.click(button) - }) + await user.click(button) expect(component.queryByRole('menu')).toBeInTheDocument() const menuItems = component.getAllByRole('menuitem') @@ -355,13 +353,11 @@ describe('ActionMenu', () => { await user.keyboard('{ArrowDown}') expect(menuItems[1]).toEqual(document.activeElement) - await act(async () => { - // 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}') - }) + // 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}') diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx index 255c771c6b3..b920cc5b0b7 100644 --- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx +++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx @@ -1,7 +1,7 @@ import {act, createRef, useCallback, useRef, useState} from 'react' import {describe, expect, it, vi} from 'vitest' -import {render} from '@testing-library/react' -import {userEvent} from 'vitest/browser' +import {fireEvent, render} from '@testing-library/react' +import userEvent from '@testing-library/user-event' import {AnchoredOverlay} from '../AnchoredOverlay' import {Button} from '../Button' import BaseStyles from '../BaseStyles' @@ -104,9 +104,7 @@ describe.each([true, false])( />, ) const anchor = anchoredOverlay.baseElement.querySelector('[aria-haspopup="true"]')! - await act(async () => { - await userEvent.click(anchor) - }) + await userEvent.click(anchor) expect(mockOpenCallback).toHaveBeenCalledTimes(1) expect(mockOpenCallback).toHaveBeenCalledWith('anchor-click') @@ -124,9 +122,7 @@ describe.each([true, false])( />, ) const anchor = anchoredOverlay.baseElement.querySelector('[aria-haspopup="true"]')! - await act(async () => { - await userEvent.type(anchor, '{Space}') - }) + fireEvent.keyDown(anchor, {key: ' ', code: 'Space'}) expect(mockOpenCallback).toHaveBeenCalledTimes(1) expect(mockOpenCallback).toHaveBeenCalledWith('anchor-key-press') @@ -144,9 +140,7 @@ describe.each([true, false])( withCSSAnchorPositioningFeatureFlag={withCSSAnchorPositioningFeatureFlag} />, ) - await act(async () => { - await userEvent.click(anchoredOverlay.baseElement) - }) + await userEvent.click(anchoredOverlay.baseElement) expect(mockOpenCallback).toHaveBeenCalledTimes(0) expect(mockCloseCallback).toHaveBeenCalledTimes(1) @@ -166,9 +160,7 @@ describe.each([true, false])( />, ) - await act(async () => { - await userEvent.keyboard('{Escape}') - }) + await userEvent.keyboard('{Escape}') expect(mockOpenCallback).toHaveBeenCalledTimes(0) expect(mockCloseCallback).toHaveBeenCalledTimes(1) @@ -185,9 +177,7 @@ describe.each([true, false])( />, ) - await act(async () => { - await userEvent.keyboard('{Escape}') - }) + await userEvent.keyboard('{Escape}') expect(mockPositionChangeCallback).toHaveBeenCalled() expect(mockPositionChangeCallback).toHaveBeenCalledWith({ @@ -309,6 +299,7 @@ describe('AnchoredOverlay feature flag specific behavior', () => { }) it('should set popover="manual" on overlay when renderAs is "popover"', () => { + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) const {baseElement} = render( @@ -317,9 +308,11 @@ describe('AnchoredOverlay feature flag specific behavior', () => { const overlay = baseElement.querySelector('[data-component="AnchoredOverlay"]') expect(overlay).toHaveAttribute('popover', 'manual') + consoleError.mockRestore() }) it('should set popovertarget on anchor when renderAs is "popover"', () => { + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) const {baseElement} = render( @@ -330,6 +323,7 @@ describe('AnchoredOverlay feature flag specific behavior', () => { const overlay = baseElement.querySelector('[data-component="AnchoredOverlay"]') expect(anchor).toHaveAttribute('popovertarget') expect(anchor!.getAttribute('popovertarget')).toBe(overlay!.getAttribute('id')) + consoleError.mockRestore() }) it('should not set popover attribute on overlay when renderAs is "portal"', () => { diff --git a/packages/react/src/Breadcrumbs/__tests__/Breadcrumbs.test.tsx b/packages/react/src/Breadcrumbs/__tests__/Breadcrumbs.test.tsx index 25dcbc5f8fc..76c221ff46f 100644 --- a/packages/react/src/Breadcrumbs/__tests__/Breadcrumbs.test.tsx +++ b/packages/react/src/Breadcrumbs/__tests__/Breadcrumbs.test.tsx @@ -1,5 +1,5 @@ import Breadcrumbs from '..' -import {render as HTMLRender, screen, waitFor, within} from '@testing-library/react' +import {act, render as HTMLRender, screen, waitFor, within} from '@testing-library/react' import {describe, expect, it, vi} from 'vitest' import userEvent from '@testing-library/user-event' import {FeatureFlags} from '../../FeatureFlags' @@ -243,30 +243,34 @@ describe('Breadcrumbs', () => { ) expect(resizeCallback).toBeDefined() + const callback = resizeCallback + if (!callback) { + throw new Error('ResizeObserver callback was not registered') + } // Initially should show overflow menu for >5 items expect(screen.getByRole('button', {name: /more breadcrumb items/i})).toBeInTheDocument() // Simulate a wide container resize - if (resizeCallback) { - resizeCallback([ + act(() => { + callback([ { contentRect: {width: 800, height: 40}, } as ResizeObserverEntry, ]) - } + }) // Should still have overflow menu for 6 items (>5 rule) expect(screen.getByRole('button', {name: /more breadcrumb items/i})).toBeInTheDocument() // Simulate a narrow container resize - if (resizeCallback) { - resizeCallback([ + act(() => { + callback([ { contentRect: {width: 250, height: 40}, } as ResizeObserverEntry, ]) - } + }) // Should maintain overflow menu for narrow container expect(screen.getByRole('button', {name: /more breadcrumb items/i})).toBeInTheDocument() @@ -303,6 +307,10 @@ describe('Breadcrumbs', () => { ) expect(resizeCallback).toBeDefined() + const callback = resizeCallback + if (!callback) { + throw new Error('ResizeObserver callback was not registered') + } // Initially should show overflow menu for >5 items const menuButton = screen.getByRole('button', {name: /more breadcrumb items/i}) @@ -327,29 +335,29 @@ describe('Breadcrumbs', () => { // Close menu by clicking outside await user.click(document.body) await waitFor(() => { - expect + expect(menuButton).toHaveAttribute('aria-expanded', 'false') }) // Simulate a very narrow container resize that would affect overflow calculation - if (resizeCallback) { - resizeCallback([ + act(() => { + callback([ { contentRect: {width: 200, height: 40}, } as ResizeObserverEntry, ]) - } + }) // Menu button should still be present expect(screen.getByRole('button', {name: /more breadcrumb items/i})).toBeInTheDocument() // Simulate a very wide container resize - if (resizeCallback) { - resizeCallback([ + act(() => { + callback([ { contentRect: {width: 1200, height: 40}, } as ResizeObserverEntry, ]) - } + }) // Menu button should still be present (7 items > 5) expect(screen.getByRole('button', {name: /more breadcrumb items/i})).toBeInTheDocument() @@ -506,7 +514,9 @@ describe('Breadcrumbs', () => { const menuButton = screen.getByRole('button', {name: /more breadcrumb items/i}) // Focus the menu button - menuButton.focus() + act(() => { + menuButton.focus() + }) expect(menuButton).toHaveFocus() // Open menu with Enter key @@ -521,7 +531,9 @@ describe('Breadcrumbs', () => { await user.keyboard('{Escape}') // Verify focus returns to button - expect(menuButton).toHaveFocus() + await waitFor(() => { + expect(menuButton).toHaveFocus() + }) }) }) diff --git a/packages/react/src/CheckboxGroup/CheckboxGroup.test.tsx b/packages/react/src/CheckboxGroup/CheckboxGroup.test.tsx index f752ae2d5c9..136b221e09f 100644 --- a/packages/react/src/CheckboxGroup/CheckboxGroup.test.tsx +++ b/packages/react/src/CheckboxGroup/CheckboxGroup.test.tsx @@ -6,7 +6,14 @@ import {implementsClassName} from '../utils/testing' import classes from '../internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroup.module.css' describe('CheckboxGroup', () => { - implementsClassName(CheckboxGroup, classes.GroupFieldset) + implementsClassName( + props => ( + + Choices + + ), + classes.GroupFieldset, + ) implementsClassName(CheckboxGroup.Caption, classes.CheckboxOrRadioGroupCaption) implementsClassName(CheckboxGroup.Label, classes.RadioGroupLabel) const mockWarningFn = vi.fn() diff --git a/packages/react/src/CircleBadge/CircleBadge.test.tsx b/packages/react/src/CircleBadge/CircleBadge.test.tsx index 32284a3d06d..c4b0eba81c3 100644 --- a/packages/react/src/CircleBadge/CircleBadge.test.tsx +++ b/packages/react/src/CircleBadge/CircleBadge.test.tsx @@ -1,7 +1,7 @@ import CircleBadge from './CircleBadge' import {CheckIcon} from '@primer/octicons-react' import {render as HTMLRender} from '@testing-library/react' -import {describe, expect, it} from 'vitest' +import {describe, expect, it, vi} from 'vitest' import {implementsClassName} from '../utils/testing' import classes from './CircleBadge.module.css' @@ -16,8 +16,17 @@ describe('CircleBadge', () => { }) it('respects the inline prop', () => { - const {container} = HTMLRender() - expect(container.firstChild).toMatchSnapshot() + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) + try { + const {container} = HTMLRender() + expect(container.firstChild).toMatchSnapshot() + const messages = consoleError.mock.calls.map(args => args.map(String).join(' ')) + expect(messages).toHaveLength(1) + expect(messages[0]).toContain('non-boolean attribute') + expect(messages[0]).toContain('inline') + } finally { + consoleError.mockRestore() + } }) it('respects the variant prop', () => { diff --git a/packages/react/src/ConfirmationDialog/ConfirmationDialog.test.tsx b/packages/react/src/ConfirmationDialog/ConfirmationDialog.test.tsx index eeb91d7e9fd..748014d6944 100644 --- a/packages/react/src/ConfirmationDialog/ConfirmationDialog.test.tsx +++ b/packages/react/src/ConfirmationDialog/ConfirmationDialog.test.tsx @@ -152,6 +152,7 @@ describe('ConfirmationDialog', () => { }) it('supports nested `focusTrap`s', async () => { + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) const {getByText, getByRole} = render() fireEvent.click(getByText('Show menu')) @@ -159,6 +160,7 @@ describe('ConfirmationDialog', () => { expect(getByRole('button', {name: 'Primary'})).toEqual(document.activeElement) expect(getByRole('button', {name: 'Secondary'})).not.toEqual(document.activeElement) + consoleError.mockRestore() }) it('accepts a className prop', async () => { diff --git a/packages/react/src/DataTable/__tests__/Pagination.test.tsx b/packages/react/src/DataTable/__tests__/Pagination.test.tsx index 90432a80063..6a70cbde0b0 100644 --- a/packages/react/src/DataTable/__tests__/Pagination.test.tsx +++ b/packages/react/src/DataTable/__tests__/Pagination.test.tsx @@ -1,7 +1,7 @@ import {page} from 'vitest/browser' import {beforeEach, describe, expect, it, vi} from 'vitest' import {Pagination} from '../Pagination' -import {render, screen} from '@testing-library/react' +import {render, screen, waitFor} from '@testing-library/react' import userEvent from '@testing-library/user-event' describe('Table.Pagination', () => { @@ -43,6 +43,7 @@ describe('Table.Pagination', () => { it('should warn if `defaultPageIndex` is not a valid `pageIndex`', () => { const spy = vi.spyOn(console, 'warn').mockImplementation(() => {}) render() + expect(spy).toHaveBeenCalledTimes(1) expect(spy).toHaveBeenCalledWith( 'Warning:', expect.stringMatching( @@ -50,6 +51,7 @@ describe('Table.Pagination', () => { ' expected `defaultPageIndex` to be less than the total number of pages. Instead, received a `defaultPageIndex` of 4 with 4 total pages.', ), ) + spy.mockRestore() }) it('should set the `id` prop on the rendered navigation landmark', () => { @@ -103,7 +105,9 @@ describe('Table.Pagination', () => { rerender( , ) - expect(getPageRange()).toEqual('11 through 15 of 300') + await waitFor(() => { + expect(getPageRange()).toEqual('11 through 15 of 300') + }) expect(getCurrentPage()).toEqual(getPage(2)) expect(getInvalidPages()).toHaveLength(0) expect(onChange).toHaveBeenCalledWith({ @@ -296,7 +300,9 @@ describe('Table.Pagination', () => { rerender( , ) - expect(getPageRange()).toEqual('1 through 5 of 300') + await waitFor(() => { + expect(getPageRange()).toEqual('1 through 5 of 300') + }) expect(getCurrentPage()).toEqual(getPage(0)) expect(getInvalidPages()).toHaveLength(0) expect(onChange).toHaveBeenCalledWith({ @@ -361,7 +367,9 @@ describe('Table.Pagination', () => { rerender( , ) - expect(getPageRange()).toEqual('1 through 5 of 300') + await waitFor(() => { + expect(getPageRange()).toEqual('1 through 5 of 300') + }) expect(getFirstPage()).toEqual(getCurrentPage()) expect(getInvalidPages()).toHaveLength(0) expect(onChange).toHaveBeenCalledWith({ diff --git a/packages/react/src/DataTable/__tests__/Table.test.tsx b/packages/react/src/DataTable/__tests__/Table.test.tsx index d7c5d126472..99e76bb43e2 100644 --- a/packages/react/src/DataTable/__tests__/Table.test.tsx +++ b/packages/react/src/DataTable/__tests__/Table.test.tsx @@ -219,7 +219,18 @@ describe('Table', () => { }) describe('Table.Cell', () => { - implementsClassName(Table.Cell, classes.TableCell) + implementsClassName( + props => ( + + + + Cell + + +
+ ), + classes.TableCell, + ) it('should set the element to a when `scope` is defined', () => { render( diff --git a/packages/react/src/Details/__tests__/Details.test.tsx b/packages/react/src/Details/__tests__/Details.test.tsx index 61afbccec04..964b18fff63 100644 --- a/packages/react/src/Details/__tests__/Details.test.tsx +++ b/packages/react/src/Details/__tests__/Details.test.tsx @@ -7,7 +7,14 @@ import {implementsClassName} from '../../utils/testing' import classes from '../Details.module.css' describe('Details', () => { - implementsClassName(Details, classes.Details) + implementsClassName( + props => ( +
+ Summary +
+ ), + classes.Details, + ) implementsClassName(Details.Summary) it('Toggles when you click outside', async () => { const Component = () => { diff --git a/packages/react/src/FormControl/__tests__/FormControl.test.tsx b/packages/react/src/FormControl/__tests__/FormControl.test.tsx index bd3141abdd6..d8324b0d8ad 100644 --- a/packages/react/src/FormControl/__tests__/FormControl.test.tsx +++ b/packages/react/src/FormControl/__tests__/FormControl.test.tsx @@ -59,8 +59,24 @@ const WrappedValidationComponent: FCWithSlotMarker = () => ( WrappedValidationComponent.__SLOT__ = FormControl.Validation.__SLOT__ describe('FormControl', () => { - implementsClassName(FormControl, classes.ControlVerticalLayout) - implementsClassName(props => , classes.ControlHorizontalLayout) + implementsClassName( + props => ( + + {LABEL_TEXT} + + + ), + classes.ControlVerticalLayout, + ) + implementsClassName( + props => ( + + {LABEL_TEXT} + + + ), + classes.ControlHorizontalLayout, + ) implementsClassName(FormControl.Caption, captionClasses.Caption) implementsClassName(FormControl.Label, inputClasses.Label) @@ -406,6 +422,7 @@ describe('FormControl', () => { ) expect(spy).toHaveBeenCalledTimes(1) + expect(spy.mock.calls[0][0]).toEqual(expect.stringContaining('MUST have a FormControl.Label child')) spy.mockRestore() }) @@ -423,6 +440,10 @@ describe('FormControl', () => { ) expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith( + 'Warning:', + 'A leading visual is only rendered for a checkbox or radio form control. If you want to render a leading visual inside of your input, check if your input supports a leading visual.', + ) spy.mockRestore() }) @@ -438,6 +459,10 @@ describe('FormControl', () => { ) expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith( + 'Warning:', + "instead of passing the 'id' prop directly to the input component, it should be passed to the parent component, ", + ) spy.mockRestore() }) @@ -453,6 +478,10 @@ describe('FormControl', () => { ) expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith( + 'Warning:', + "instead of passing the 'disabled' prop directly to the input component, it should be passed to the parent component, ", + ) spy.mockRestore() }) @@ -468,6 +497,10 @@ describe('FormControl', () => { ) expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith( + 'Warning:', + "instead of passing the 'required' prop directly to the input component, it should be passed to the parent component, ", + ) spy.mockRestore() }) }) @@ -547,6 +580,10 @@ describe('FormControl', () => { // The leading visual should be found as a slot because of the __SLOT__ property // This should trigger a warning since leading visuals are only for choice inputs expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith( + 'Warning:', + 'A leading visual is only rendered for a checkbox or radio form control. If you want to render a leading visual inside of your input, check if your input supports a leading visual.', + ) // The icon should be rendered in the DOM expect(container.querySelector('svg')).toBeDefined() @@ -603,7 +640,11 @@ describe('FormControl', () => { , ) - expect(consoleSpy).toHaveBeenCalled() + expect(consoleSpy).toHaveBeenCalledTimes(1) + expect(consoleSpy).toHaveBeenCalledWith( + 'Warning:', + 'Validation messages are not rendered for an individual checkbox or radio. The validation message should be shown for all options.', + ) consoleSpy.mockRestore() }) @@ -618,7 +659,12 @@ describe('FormControl', () => { , ) - expect(consoleSpy).toHaveBeenCalled() + expect(consoleSpy).toHaveBeenCalledTimes(2) + expect(consoleSpy).toHaveBeenCalledWith( + 'Warning:', + "instead of passing the 'required' prop directly to the input component, it should be passed to the parent component, ", + ) + expect(consoleSpy).toHaveBeenCalledWith('Warning:', 'An individual radio cannot be a required field.') consoleSpy.mockRestore() }) diff --git a/packages/react/src/PageLayout/usePaneWidth.test.ts b/packages/react/src/PageLayout/usePaneWidth.test.ts index dca2ac2adb8..bb245aa87d6 100644 --- a/packages/react/src/PageLayout/usePaneWidth.test.ts +++ b/packages/react/src/PageLayout/usePaneWidth.test.ts @@ -735,12 +735,11 @@ describe('usePaneWidth', () => { // Shrink viewport (crosses 1280 breakpoint, diff switches to 511) vi.stubGlobal('innerWidth', 1000) - // Fire resize - with throttle, first update happens immediately (if THROTTLE_MS passed) - window.dispatchEvent(new Event('resize')) - // Since Date.now() starts at 0 and lastUpdateTime is 0, first update should happen immediately // but it's in rAF, so we need to advance through rAF await act(async () => { + // Fire resize - with throttle, first update happens immediately (if THROTTLE_MS passed) + window.dispatchEvent(new Event('resize')) await vi.runAllTimersAsync() }) @@ -771,11 +770,10 @@ describe('usePaneWidth', () => { // Shrink viewport (crosses 1280 breakpoint, diff switches to 511) vi.stubGlobal('innerWidth', 900) - // Fire resize - with throttle, update happens via rAF - window.dispatchEvent(new Event('resize')) - // Wait for rAF to complete await act(async () => { + // Fire resize - with throttle, update happens via rAF + window.dispatchEvent(new Event('resize')) await vi.runAllTimersAsync() }) @@ -807,10 +805,10 @@ describe('usePaneWidth', () => { // Fire resize events rapidly vi.stubGlobal('innerWidth', 1100) - window.dispatchEvent(new Event('resize')) // With throttle, CSS should update immediately or via rAF await act(async () => { + window.dispatchEvent(new Event('resize')) await vi.runAllTimersAsync() }) @@ -821,13 +819,12 @@ describe('usePaneWidth', () => { setPropertySpy.mockClear() // Fire more resize events rapidly (within throttle window) - for (let i = 0; i < 3; i++) { - vi.stubGlobal('innerWidth', 1000 - i * 50) - window.dispatchEvent(new Event('resize')) - } - // Should schedule via rAF await act(async () => { + for (let i = 0; i < 3; i++) { + vi.stubGlobal('innerWidth', 1000 - i * 50) + window.dispatchEvent(new Event('resize')) + } await vi.runAllTimersAsync() }) @@ -858,10 +855,10 @@ describe('usePaneWidth', () => { // Shrink viewport (crosses 1280 breakpoint, diff switches to 511) vi.stubGlobal('innerWidth', 800) - window.dispatchEvent(new Event('resize')) // After throttle (via rAF), state updated via startTransition await act(async () => { + window.dispatchEvent(new Event('resize')) await vi.runAllTimersAsync() }) @@ -963,7 +960,9 @@ describe('usePaneWidth', () => { // Fire resize vi.stubGlobal('innerWidth', 1000) - window.dispatchEvent(new Event('resize')) + await act(async () => { + window.dispatchEvent(new Event('resize')) + }) // Attribute should be applied immediately on first resize expect(refs.paneRef.current?.hasAttribute('data-dragging')).toBe(true) @@ -971,7 +970,9 @@ describe('usePaneWidth', () => { // Fire another resize event immediately (simulating continuous resize) vi.stubGlobal('innerWidth', 900) - window.dispatchEvent(new Event('resize')) + await act(async () => { + window.dispatchEvent(new Event('resize')) + }) // Attribute should still be present (containment stays on during continuous resize) expect(refs.paneRef.current?.hasAttribute('data-dragging')).toBe(true) @@ -1006,7 +1007,9 @@ describe('usePaneWidth', () => { // Fire resize vi.stubGlobal('innerWidth', 1000) - window.dispatchEvent(new Event('resize')) + await act(async () => { + window.dispatchEvent(new Event('resize')) + }) // Attribute should be applied expect(refs.paneRef.current?.hasAttribute('data-dragging')).toBe(true) @@ -1044,9 +1047,9 @@ describe('usePaneWidth', () => { // Shrink viewport (crosses 1280 breakpoint, diff switches to 511) vi.stubGlobal('innerWidth', 800) - window.dispatchEvent(new Event('resize')) await act(async () => { + window.dispatchEvent(new Event('resize')) await vi.runAllTimersAsync() }) diff --git a/packages/react/src/Radio/Radio.test.tsx b/packages/react/src/Radio/Radio.test.tsx index 854eb350790..206dd761178 100644 --- a/packages/react/src/Radio/Radio.test.tsx +++ b/packages/react/src/Radio/Radio.test.tsx @@ -5,11 +5,11 @@ import {implementsClassName} from '../utils/testing' import classes from './Radio.module.css' describe('Radio', () => { - implementsClassName(Radio, classes.Radio) const defaultProps = { name: 'mock', value: 'mock value', } + implementsClassName(props => , classes.Radio) beforeEach(() => { vi.resetAllMocks() diff --git a/packages/react/src/RadioGroup/RadioGroup.test.tsx b/packages/react/src/RadioGroup/RadioGroup.test.tsx index 791e9e5188c..2851cd0633e 100644 --- a/packages/react/src/RadioGroup/RadioGroup.test.tsx +++ b/packages/react/src/RadioGroup/RadioGroup.test.tsx @@ -6,7 +6,14 @@ import {implementsClassName} from '../utils/testing' import classes from '../internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroup.module.css' describe('RadioGroup', () => { - implementsClassName(RadioGroup, classes.GroupFieldset) + implementsClassName( + props => ( + + Choices + + ), + classes.GroupFieldset, + ) implementsClassName(RadioGroup.Caption, classes.CheckboxOrRadioGroupCaption) implementsClassName(RadioGroup.Label, classes.RadioGroupLabel) const mockWarningFn = vi.fn() diff --git a/packages/react/src/SegmentedControl/SegmentedControl.test.tsx b/packages/react/src/SegmentedControl/SegmentedControl.test.tsx index 6a637d1ba30..5e3eeddf51d 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.test.tsx +++ b/packages/react/src/SegmentedControl/SegmentedControl.test.tsx @@ -32,7 +32,14 @@ const segmentData = [ ] describe('SegmentedControl', () => { - implementsClassName(SegmentedControl, classes.SegmentedControl) + implementsClassName( + props => ( + + Preview + + ), + classes.SegmentedControl, + ) it('renders with a selected segment - controlled', () => { const {getByText} = render( @@ -332,7 +339,10 @@ describe('SegmentedControl', () => { , ) - expect(spy).toHaveBeenCalled() + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith( + 'Use the `aria-label` or `aria-labelledby` prop to provide an accessible label for assistive technologies', + ) spy.mockRestore() }) diff --git a/packages/react/src/SelectPanel/SelectPanel.test.tsx b/packages/react/src/SelectPanel/SelectPanel.test.tsx index c1d71b154c7..f4f498267db 100644 --- a/packages/react/src/SelectPanel/SelectPanel.test.tsx +++ b/packages/react/src/SelectPanel/SelectPanel.test.tsx @@ -1671,11 +1671,10 @@ for (const usingRemoveActiveDescendant of [false, true]) { const input = screen.getByPlaceholderText('Filter items') const options = screen.getAllByRole('option') - // Wait a tick for the effect to run - await new Promise(resolve => setTimeout(resolve, 0)) - // aria-activedescendant should be set to the first item - expect(input.getAttribute('aria-activedescendant')).toBe(options[0].id) + await waitFor(() => { + expect(input.getAttribute('aria-activedescendant')).toBe(options[0].id) + }) }) it('should not set aria-activedescendant on mouse hover until after first interaction when setInitialFocus is true', async () => { diff --git a/packages/react/src/TooltipV2/__tests__/Tooltip.test.tsx b/packages/react/src/TooltipV2/__tests__/Tooltip.test.tsx index a2755af56f0..3b42e6e340f 100644 --- a/packages/react/src/TooltipV2/__tests__/Tooltip.test.tsx +++ b/packages/react/src/TooltipV2/__tests__/Tooltip.test.tsx @@ -1,5 +1,5 @@ import type React from 'react' -import {describe, expect, it} from 'vitest' +import {describe, expect, it, vi} from 'vitest' import type {TooltipProps} from '../Tooltip' import {Tooltip} from '../Tooltip' import {render as HTMLRender} from '@testing-library/react' @@ -125,15 +125,14 @@ describe('Tooltip', () => { expect(triggerEL.getAttribute('aria-describedby')).toContain('custom-tooltip-id') }) it('should throw an error if the trigger element is disabled', () => { - expect(() => { + expect.hasAssertions() + expectRenderError(() => { HTMLRender( , ) - }).toThrow( - 'The `Tooltip` component expects a single React element that contains interactive content. Consider using a `