From 1057c2d94bedd3750811c77840c06f4c36582d28 Mon Sep 17 00:00:00 2001 From: Adam Dierkens Date: Fri, 22 May 2026 19:43:42 +0000 Subject: [PATCH] fix(test-helpers): make @primer/react/test-helpers runtime-agnostic Previously the published helper hard-referenced the `jest` global to create mock functions for its JSDOM polyfills, so importing it from a Vitest test threw 'ReferenceError: jest is not defined'. The polyfills now detect `globalThis.jest?.fn` or `globalThis.vi?.fn` at runtime and fall back to a plain no-op function if neither is present. Mock-style introspection still works for both Jest and Vitest consumers; consumers without a test runtime get a silent no-op which is sufficient for the polyfill use case. Also removes a stale '// jest function' comment from SelectPanel.test.tsx (the test actually uses vi.fn()). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .changeset/test-helpers-runtime-agnostic.md | 11 +++++ .../src/SelectPanel/SelectPanel.test.tsx | 1 - packages/react/src/utils/test-helpers.tsx | 44 ++++++++++++------- 3 files changed, 39 insertions(+), 17 deletions(-) create mode 100644 .changeset/test-helpers-runtime-agnostic.md diff --git a/.changeset/test-helpers-runtime-agnostic.md b/.changeset/test-helpers-runtime-agnostic.md new file mode 100644 index 00000000000..7cb37cf417b --- /dev/null +++ b/.changeset/test-helpers-runtime-agnostic.md @@ -0,0 +1,11 @@ +--- +'@primer/react': patch +--- + +Make the published `@primer/react/test-helpers` entry runtime-agnostic so it works with both Jest and Vitest consumers. + +Previously the file hard-referenced the `jest` global to create mock functions for its JSDOM polyfills (`ResizeObserver`, `HTMLDialogElement.showModal/close`, `HTMLCanvasElement.getContext`, `Element.scrollIntoView`, `matchMedia`, `CSS.escape/supports`). Importing the helper from a Vitest test threw `ReferenceError: jest is not defined`. + +The polyfills now detect `globalThis.jest?.fn` or `globalThis.vi?.fn` at runtime and fall back to a plain no-op function if neither is present. Mock-style introspection (`toHaveBeenCalled` etc.) still works for both Jest and Vitest consumers; consumers without a test runtime get a silent no-op which is sufficient for the polyfill use case. + +Also removes a stale `// jest function` comment from `SelectPanel.test.tsx` (the test actually uses `vi.fn()`). \ No newline at end of file diff --git a/packages/react/src/SelectPanel/SelectPanel.test.tsx b/packages/react/src/SelectPanel/SelectPanel.test.tsx index c1d71b154c7..025982cb8e5 100644 --- a/packages/react/src/SelectPanel/SelectPanel.test.tsx +++ b/packages/react/src/SelectPanel/SelectPanel.test.tsx @@ -113,7 +113,6 @@ for (const usingRemoveActiveDescendant of [false, true]) { it('should call onActiveDescendantChanged when using keyboard while focusing on an item', async () => { const user = userEvent.setup() - // jest function const onActiveDescendantChanged = vi.fn() render() diff --git a/packages/react/src/utils/test-helpers.tsx b/packages/react/src/utils/test-helpers.tsx index 93d37d01c01..0014727522d 100644 --- a/packages/react/src/utils/test-helpers.tsx +++ b/packages/react/src/utils/test-helpers.tsx @@ -2,19 +2,31 @@ // @ts-nocheck import {TextEncoder} from 'node:util' +// Runtime-agnostic mock helper: works with Jest, Vitest, or any other test runtime. +// Falls back to a plain function when no runtime is detected, so polyfills still apply +// even outside a test runner (e.g. an SSR-only import). +const mockFn = (impl?: (...args: unknown[]) => unknown) => { + const runtimeMockFactory = + (globalThis as {jest?: {fn: typeof Function}}).jest?.fn ?? (globalThis as {vi?: {fn: typeof Function}}).vi?.fn + if (runtimeMockFactory) { + return impl ? runtimeMockFactory(impl) : runtimeMockFactory() + } + return impl ?? (() => {}) +} + // JSDOM doesn't mock ResizeObserver -global.ResizeObserver = jest.fn().mockImplementation(() => { +global.ResizeObserver = mockFn(() => { return { - observe: jest.fn(), - disconnect: jest.fn(), - unobserve: jest.fn(), + observe: mockFn(), + disconnect: mockFn(), + unobserve: mockFn(), } }) // @ts-expect-error only declare properties used internally global.CSS = { - escape: jest.fn(), - supports: jest.fn().mockImplementation(() => { + escape: mockFn(), + supports: mockFn(() => { return false }), } @@ -29,23 +41,23 @@ global.TextEncoder = TextEncoder * bonus: we only want to mock browser globals in DOM (or js-dom) environments – not in SSR / node */ if (typeof document !== 'undefined') { - global.HTMLDialogElement.prototype.showModal = jest.fn(function mock(this: HTMLDialogElement) { + global.HTMLDialogElement.prototype.showModal = mockFn(function mock(this: HTMLDialogElement) { this.open = true }) - global.HTMLDialogElement.prototype.close = jest.fn(function mock(this: HTMLDialogElement) { + global.HTMLDialogElement.prototype.close = mockFn(function mock(this: HTMLDialogElement) { this.open = false }) // Add a fallback for getContext if it does not exist in the test, used for axe - global.HTMLCanvasElement.prototype.getContext = jest.fn() + global.HTMLCanvasElement.prototype.getContext = mockFn() } // Add a fallback for scrollIntoView if it does not exist in the test // environment. // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (global.Element.prototype.scrollIntoView === undefined) { - global.Element.prototype.scrollIntoView = jest.fn() + global.Element.prototype.scrollIntoView = mockFn() } // setup match media for tests that use useResponsiveValue or use a compone that uses useResponsiveValue @@ -55,15 +67,15 @@ export const setupMatchMedia = () => { // https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom Object.defineProperty(window, 'matchMedia', { writable: true, - value: jest.fn().mockImplementation(query => ({ + value: mockFn(query => ({ matches: false, media: query, onchange: null, - addListener: jest.fn(), // deprecated - removeListener: jest.fn(), // deprecated - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), + addListener: mockFn(), // deprecated + removeListener: mockFn(), // deprecated + addEventListener: mockFn(), + removeEventListener: mockFn(), + dispatchEvent: mockFn(), })), }) }