web: disable Mantine transitions in tests to kill post-teardown timer flake (#1760) (#1761)
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled

* web: disable Mantine transitions in tests to kill post-teardown timer flake (#1760)

A Mantine `Transition`/`Modal` open/close `setTimeout` can fire after happy-dom
tears down `window` at the end of a run, throwing an uncaught
`ReferenceError: window is not defined` that fails the whole `coverage` job even
when every assertion passed (seen on PR #1759's CI, originating in
ServerRemoveConfirmModal.test.tsx).

Set `env="test"` on the shared `renderWithMantine` MantineProvider so Mantine
renders transitions synchronously (no timer). This removes the leak class for
every test that renders through the shared wrapper (Modals, Menus, Popovers, …).
Also apply it to ViewHeader's local forced-dark provider.

The handful of tests that assert mid-flight transition state (ViewHeader's
`data-anim="out"` crossfade checks, #1450) genuinely need real transitions, so
add an opt-in `renderWithMantineTransitions` wrapper (env="default") and route
those three tests through it — each already drives the transition to completion
(waitFor/findBy) so it doesn't reintroduce the leak. Tests that assert only the
entered state stay on the default (leak-free) wrapper.

Full `npm run ci` green; unit run reports zero unhandled errors.

Closes #1760

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* docs: note the renderWithMantine env=test invariant in AGENTS.md (#1760)

Per review feedback: document that tests must render through
`renderWithMantine` (env="test", transition-free) rather than a hand-rolled
`MantineProvider`, and reserve `renderWithMantineTransitions` for asserting
mid-flight animation state (driving the transition to completion). Keeps future
tests from quietly reintroducing the post-teardown timer leak class.

Refs #1760

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cliff Hall 2026-07-24 10:58:23 -04:00 • committed by GitHub
commit 2439ef9c3c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 44 additions and 9 deletions

View file

@ -3,6 +3,7 @@ import userEvent from "@testing-library/user-event";
import {
act,
renderWithMantine,
renderWithMantineTransitions,
screen,
waitFor,
} from "../../../test/renderWithMantine";
@ -12,11 +13,13 @@ import { theme } from "../../../theme/theme";
import { ViewHeader } from "./ViewHeader";
// Render under a forced-dark MantineProvider so `useComputedColorScheme`
// returns "dark" (the light/dark icon + logo branches).
// returns "dark" (the light/dark icon + logo branches). `env="test"` disables
// timer-driven transitions, matching the shared `renderWithMantine` wrapper and
// avoiding post-teardown `window is not defined` races (#1760).
function renderDark(ui: React.ReactElement) {
return render(ui, {
wrapper: ({ children }) => (
<MantineProvider theme={theme} defaultColorScheme="dark">
<MantineProvider theme={theme} defaultColorScheme="dark" env="test">
{children}
</MantineProvider>
),
@ -240,7 +243,9 @@ describe("ViewHeader", () => {
it("crossfades on disconnect: tab bar exits while the title enters, then the bar unmounts (#1450)", async () => {
mediaQueryMock.value = true;
const { rerender } = renderWithMantine(
// Asserts the mid-flight exit ("out") state, so it needs real transitions;
// it drives them to completion below (waitFor unmount).
const { rerender } = renderWithMantineTransitions(
<ViewHeader {...connectedProps} />,
);
expect(screen.getAllByRole("radio").length).toBeGreaterThan(0);
@ -309,7 +314,9 @@ describe("ViewHeader", () => {
it("animates the server name and disconnect controls in on connect, out on disconnect (#1450)", async () => {
mediaQueryMock.value = true;
const { rerender } = renderWithMantine(
// Asserts the mid-flight exit ("out") state, so it needs real transitions;
// it drives them to completion below (waitFor unmount).
const { rerender } = renderWithMantineTransitions(
<ViewHeader {...connectedProps} />,
);
// Connected: the server name and the Disconnect control are in their
@ -368,8 +375,10 @@ describe("ViewHeader", () => {
it("crossfades the title out and the connected header in on connect (#1450)", async () => {
mediaQueryMock.value = true;
// Asserts the mid-flight exit ("out") state, so it needs real transitions;
// it drives them to completion below (findByText for the entering header).
// Start disconnected: the title cell is the entering one.
const { rerender } = renderWithMantine(
const { rerender } = renderWithMantineTransitions(
<ViewHeader
connected={false}
onToggleTheme={vi.fn()}

View file

@ -3,9 +3,28 @@ import { render, type RenderOptions } from "@testing-library/react";
import { MantineProvider } from "@mantine/core";
import { theme } from "../theme/theme";
function MantineWrapper({ children }: { children: ReactNode }) {
// `env="test"` makes Mantine render transitions synchronously (no internal
// `setTimeout`). Without it, a `Transition`/`Modal` open/close timer can fire
// after happy-dom tears down `window` at the end of the run, throwing an
// uncaught `ReferenceError: window is not defined` that fails the whole run
// even when every assertion passed (#1760). This is the right default for the
// vast majority of tests, which don't assert on mid-transition state.
function TestEnvWrapper({ children }: { children: ReactNode }) {
return (
<MantineProvider theme={theme} defaultColorScheme="light">
<MantineProvider theme={theme} defaultColorScheme="light" env="test">
{children}
</MantineProvider>
);
}
// Opt-in wrapper that keeps Mantine's timer-driven transitions enabled, for the
// few tests that assert on transition/animation state that only exists mid-flight
// (e.g. a `data-anim="out"` cell during an exit crossfade). Such a test MUST
// drive the transition to completion (`await waitFor`/`findBy`) so its timer
// resolves before teardown; otherwise it reintroduces the #1760 leak.
function TransitionsWrapper({ children }: { children: ReactNode }) {
return (
<MantineProvider theme={theme} defaultColorScheme="light" env="default">
{children}
</MantineProvider>
);
@ -15,7 +34,14 @@ export function renderWithMantine(
ui: ReactElement,
options?: Omit<RenderOptions, "wrapper">,
) {
return render(ui, { wrapper: MantineWrapper, ...options });
return render(ui, { wrapper: TestEnvWrapper, ...options });
}
export function renderWithMantineTransitions(
ui: ReactElement,
options?: Omit<RenderOptions, "wrapper">,
) {
return render(ui, { wrapper: TransitionsWrapper, ...options });
}
export * from "@testing-library/react";