From 11d852e292923b59dbdae2a0e792e92c02256880 Mon Sep 17 00:00:00 2001 From: Stephen Watkins Date: Fri, 12 Jun 2026 18:22:42 +0200 Subject: [PATCH 1/2] Expose test utilities as a published entry point Build src/utilities/test.ts into dist so consumers can import easy-ui's test helpers via @easypost/easy-ui/utilities/test: - vite.config.mts: include test.ts in the build glob (still excluding *.test.ts) and externalize vitest + @testing-library/* so they are resolved from the consumer rather than bundled. react-transition-group stays bundled so disable/enableReactTransitionGroup share easy-ui's config instance. - test.ts: drop the ambient `declare global` block (it would leak `var jest` / `IS_REACT_ACT_ENVIRONMENT` into consumers and collide with @types/jest); access the jest global via a local cast instead. - vitest.setup.ts: move the IS_REACT_ACT_ENVIRONMENT declaration here, where it is set; this file is outside the build so it does not ship. Also fix the "testing environment is not configured to support act(...)" warnings: user-event v14 wraps interactions in act() internally, so the manual act() wrappers in userClick/userTab/etc. and in the Tooltip test helpers caused nested-act flag thrashing under fake timers. Remove the redundant wrappers; wrap only the synchronous timer flush. --- easy-ui-react/src/Tooltip/Tooltip.test.tsx | 25 ++++++------ easy-ui-react/src/utilities/test.ts | 47 +++++++++------------- easy-ui-react/vite.config.mts | 12 ++++-- easy-ui-react/vitest.setup.ts | 5 +++ 4 files changed, 46 insertions(+), 43 deletions(-) diff --git a/easy-ui-react/src/Tooltip/Tooltip.test.tsx b/easy-ui-react/src/Tooltip/Tooltip.test.tsx index f140ac3fb..1dd12395b 100644 --- a/easy-ui-react/src/Tooltip/Tooltip.test.tsx +++ b/easy-ui-react/src/Tooltip/Tooltip.test.tsx @@ -88,8 +88,9 @@ describe("", () => { // Helpers for interacting with tooltip trigger // -// TODO: Look into why hover() and tab() has to be wrapped in act() since this -// shouldn't be necessary. Perhaps something being done in Aria +// user-event interactions wrap themselves in act(), so they must not be nested +// inside another act() call. Only the synchronous timer flush, which drives the +// tooltip's open/close delay state updates, needs to be wrapped. export async function hoverOverTooltipTrigger( user: UserEvent, @@ -97,19 +98,19 @@ export async function hoverOverTooltipTrigger( { runTimers = true } = {}, ) { fireEvent.mouseMove(document.body); - await act(async () => { - await userHover(user, el); - if (runTimers) { + await userHover(user, el); + if (runTimers) { + act(() => { vi.runAllTimers(); - } - }); + }); + } } async function tabToTooltipTrigger(user: UserEvent, { runTimers = true } = {}) { - await act(async () => { - await userTab(user); - if (runTimers) { + await userTab(user); + if (runTimers) { + act(() => { vi.runAllTimers(); - } - }); + }); + } } diff --git a/easy-ui-react/src/utilities/test.ts b/easy-ui-react/src/utilities/test.ts index 2ecd164f8..5190598ea 100644 --- a/easy-ui-react/src/utilities/test.ts +++ b/easy-ui-react/src/utilities/test.ts @@ -1,16 +1,9 @@ -import { act, render as renderWithTestingLib } from "@testing-library/react"; +import { render as renderWithTestingLib } from "@testing-library/react"; import userEvent, { UserEvent } from "@testing-library/user-event"; import { ReactElement } from "react"; import { config as reactTransitionGroupConfig } from "react-transition-group"; import { vi } from "vitest"; -declare global { - // eslint-disable-next-line no-var - var jest: object; - // eslint-disable-next-line no-var - var IS_REACT_ACT_ENVIRONMENT: boolean; -} - /** * Render a react element for testing. Passes in vitest's timers for user-event. */ @@ -28,16 +21,22 @@ export function render(jsx: ReactElement) { * user-event with `vi.useFakeTimers()` * * See https://github.com/testing-library/react-testing-library/issues/1195 + * + * Note: relies on the test runner's `beforeAll` global, so it must be called + * from a vitest/jest environment that exposes globals. */ export function installJestCompatibleFakeTimers() { beforeAll(() => { - const _jest = globalThis.jest; - globalThis.jest = { - ...globalThis.jest, + const globalWithJest = globalThis as typeof globalThis & { + jest?: Record; + }; + const _jest = globalWithJest.jest; + globalWithJest.jest = { + ...globalWithJest.jest, advanceTimersByTime: vi.advanceTimersByTime.bind(vi), }; return () => { - globalThis.jest = _jest; + globalWithJest.jest = _jest; }; }); } @@ -63,7 +62,11 @@ export function mockIntersectionObserver() { } export function installScrollToMock() { + const originalScrollTo = Element.prototype.scrollTo; Element.prototype.scrollTo = () => {}; + return () => { + Element.prototype.scrollTo = originalScrollTo; + }; } export function mockMatchMedia({ @@ -95,27 +98,19 @@ export function silenceConsoleError() { } export async function userClick(user: UserEvent, element: Element) { - await act(async () => { - await user.click(element); - }); + await user.click(element); } export async function userKeyboard(user: UserEvent, text: string) { - await act(async () => { - await user.keyboard(text); - }); + await user.keyboard(text); } export async function userHover(user: UserEvent, element: Element) { - await act(async () => { - await user.hover(element); - }); + await user.hover(element); } export async function userTab(user: UserEvent) { - await act(async () => { - await user.tab(); - }); + await user.tab(); } export async function userType( @@ -123,9 +118,7 @@ export async function userType( element: Element, text: string, ) { - await act(async () => { - await user.type(element, text); - }); + await user.type(element, text); } export async function selectCheckbox(user: UserEvent, el: HTMLElement) { diff --git a/easy-ui-react/vite.config.mts b/easy-ui-react/vite.config.mts index 36b058d8d..a66344864 100644 --- a/easy-ui-react/vite.config.mts +++ b/easy-ui-react/vite.config.mts @@ -45,16 +45,20 @@ export default defineConfig({ entry: buildEntryObject([ ...glob.sync("src/**/index.ts"), ...glob.sync("src/utilities/*.ts", { - ignore: ["**/test.ts", "**/*.test.ts"], + ignore: ["**/*.test.ts"], }), ]), cssFileName: "style", }, rollupOptions: { external: (id) => - ["react", "react-dom"].some( - (pkg) => id === pkg || id.startsWith(`${pkg}/`), - ), + [ + "react", + "react-dom", + "vitest", + "@testing-library/react", + "@testing-library/user-event", + ].some((pkg) => id === pkg || id.startsWith(`${pkg}/`)), output: [ { format: "cjs", diff --git a/easy-ui-react/vitest.setup.ts b/easy-ui-react/vitest.setup.ts index 810e3ec15..d59d58d85 100644 --- a/easy-ui-react/vitest.setup.ts +++ b/easy-ui-react/vitest.setup.ts @@ -4,6 +4,11 @@ import { installScrollToMock, } from "./src/utilities/test"; +declare global { + // eslint-disable-next-line no-var + var IS_REACT_ACT_ENVIRONMENT: boolean; +} + globalThis.IS_REACT_ACT_ENVIRONMENT = true; installJestCompatibleFakeTimers(); From 6bfc2f4100ebcac3954c07183cb00adb51cd416e Mon Sep 17 00:00:00 2001 From: Stephen Watkins Date: Fri, 12 Jun 2026 18:28:47 +0200 Subject: [PATCH 2/2] changeset --- .changeset/cold-nights-sit.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cold-nights-sit.md diff --git a/.changeset/cold-nights-sit.md b/.changeset/cold-nights-sit.md new file mode 100644 index 000000000..606543545 --- /dev/null +++ b/.changeset/cold-nights-sit.md @@ -0,0 +1,5 @@ +--- +"@easypost/easy-ui": patch +--- + +fix: expose test utilities as published entry point