# Files changed · PR \#22187 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/22187/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[test\(web\): disposable render helper and lint rule for browser tests](https://git.cafe/pingdotgg/t3code/pull/22187.md)

Version: 30

Head commit: 7b47897f81695d01a3108f14face541c07a5c0e6

Base commit: 2229efcbe22c5616b8186e2b018e7a996cd4693b

Comparison base: 2229efcbe22c5616b8186e2b018e7a996cd4693b

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## AGENTS.md

Status: modified; +12; −0

````diff
@@ -100,6 +100,18 @@
 - **Do not run repo-wide checks.** No `vp check`, no `vp run -r test`, no `vp run -r typecheck` unless I ask. CI owns the full suite.
 - Backend behavior changes ship with focused tests for that behavior.
 - The server is event-sourced, and side effects run after the command commits. In tests, drain the effect worker (`OrchestrationEffectWorkerV2.drain`) or await the specific persisted event or `Deferred` that marks the milestone. Never wait on sleeps or polling. A test that needs a timeout to pass is wrong.
+- Web component tests are `*.browser.test.tsx` and render through `apps/web/test/render.tsx`. Acquire everything a test changes with `using`/`await using` so disposal undoes it even when the test fails; no `beforeEach`/`afterEach`, no module-level screens. Shared mock state that `vi.mock` factories read gets reset by a `setup()` call at the top of the test. `t3code/require-disposable-render` flags the old patterns.
+
+  ```tsx
+  it("saves on Enter", async () => {
+    const onSave = vi.fn();
+    using _now = vi.spyOn(Date, "now").mockReturnValue(0);
+    await using screen = await render(<TitleEditor onSave={onSave} />, { router: "/threads/1" });
+    await userEvent.type(screen.getByRole("textbox"), "Title{Enter}");
+    expect(onSave).toHaveBeenCalledWith("Title");
+  });
+  ```
+
 - Upon request, user-visible frontend changes should get one integrated pass in a real client: `test-t3-app` for web, `test-t3-mobile` for mobile. The primary agent does this once after integrating. Subagents do not launch their own dev servers. Ask permission before doing computer use or spinning up browsers.
 
 For authorized mobile verification, a missing or outdated native client is a build step, not a blocker. Run `node scripts/mobile-native-client.ts ensure <ios|android> <device-id>` on the simulator host before starting Metro. It checks the local Expo fingerprint and builds/installs when needed. See `test-t3-mobile` for the full workflow.
````

## apps/web/src/browser/ElectronBrowserHost.browser.test.tsx

Status: modified; +33; −33

```diff
@@ -7,9 +7,9 @@
   type DesktopPreviewBridge,
   type PreviewSessionSnapshot,
 } from "@t3tools/contracts";
+import { describe, expect, it, vi } from "vite-plus/test";
+
+import { act, fakeTimers, render } from "../../test/render";
-import { act } from "react";
-import { createRoot, type Root } from "react-dom/client";
-import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
 
 const mocks = vi.hoisted(() => ({
   getClientSettings: vi.fn<() => Promise<ClientSettings | null>>(),
@@ -73,10 +73,7 @@
   updatedAt,
 });
 
-let root: Root | undefined;
-let container: HTMLDivElement;
+function setup(): Disposable {
-
-beforeEach(() => {
   __resetClientSettingsPersistenceForTests();
   mocks.getClientSettings.mockReset().mockResolvedValue(DEFAULT_CLIENT_SETTINGS);
   mocks.setClientSettings.mockReset().mockResolvedValue(undefined);
@@ -88,26 +85,37 @@
     webPreferences: "contextIsolation=yes",
     preloadUrl: null,
   });
+  return {
+    [Symbol.dispose]() {
+      resetPreviewStateForTests();
+      __resetClientSettingsPersistenceForTests();
+    },
+  };
-  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
-  container = document.createElement("div");
-  document.body.append(container);
-});
+}
 
-afterEach(async () => {
-  vi.useFakeTimers();
-  await act(async () => root?.unmount());
-  root = undefined;
-  container.remove();
-  await vi.advanceTimersByTimeAsync(0);
-  vi.useRealTimers();
-  resetPreviewStateForTests();
-  __resetClientSettingsPersistenceForTests();
-  vi.restoreAllMocks();
-  vi.unstubAllGlobals();
+async function renderHost() {
+  await using stack = new AsyncDisposableStack();
+  const screen = stack.use(
+    await render(
+      <AppAtomRegistryProvider>
+        <ElectronBrowserHost />
+      </AppAtomRegistryProvider>,
+    ),
+  );
+  // Unmounting releases each tab on a close timer; run it now so it never fires in a later test.
+  stack.defer(async () => {
+    using _timers = fakeTimers();
+    await screen.unmount();
+    await vi.advanceTimersByTimeAsync(0);
+  });
+  await act(() => ensureClientSettingsHydrated());
+  const environment = stack.move();
-});
+  return { container: screen.container, [Symbol.asyncDispose]: () => environment.disposeAsync() };
+}
 
 describe("ElectronBrowserHost", () => {
   it("never moves a tab's webview when the server reorders tabs by last update", async () => {
+    using _state = setup();
     reconcilePreviewServerSessions(threadRef, {
       sessions: [
         snapshot("tab_7", "2026-01-01T00:00:01.000Z"),
@@ -116,17 +124,9 @@
       serverEpoch: "server-a",
       revision: 1,
     });
-    root = createRoot(container);
-    await act(async () => {
-      root!.render(
-        <AppAtomRegistryProvider>
-          <ElectronBrowserHost />
-        </AppAtomRegistryProvider>,
-      );
-      await ensureClientSettingsHydrated();
-    });
+    await using screen = await renderHost();
 
-    const host = container.querySelector("[data-electron-browser-host]")!;
+    const host = screen.container.querySelector("[data-electron-browser-host]")!;
     const webviews = () => [...host.querySelectorAll("webview")];
     const before = webviews();
     expect(before.map((node) => node.getAttribute("data-preview-server-tab")).toSorted()).toEqual([
```

## apps/web/src/browser/HostedBrowserWebview.browser.test.tsx

Status: modified; +80; −68

```diff
@@ -7,13 +7,14 @@
   type DesktopPreviewBridge,
   type PreviewViewportSetting,
 } from "@t3tools/contracts";
-import { act } from "react";
+import type { ReactNode } from "react";
 
 import "~/index.css";
+import { describe, expect, it, vi } from "vite-plus/test";
-import { createRoot, type Root } from "react-dom/client";
-import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
 import type { previewBridge } from "~/components/preview/previewBridge";
 
+import { act, fakeTimers, render, stubGlobal } from "../../test/render";
+
 const mocks = vi.hoisted(() => ({
   getClientSettings: vi.fn<() => Promise<ClientSettings | null>>(),
   setClientSettings: vi.fn<(settings: ClientSettings) => Promise<void>>(),
@@ -58,9 +59,6 @@
 import * as desktopTabLifetime from "./desktopTabLifetime";
 import { HostedBrowserWebview } from "./HostedBrowserWebview";
 
-let root: Root | undefined;
-let container: HTMLDivElement;
-
 function deferred<A>() {
   let resolve!: (value: A) => void;
   let reject!: (error: unknown) => void;
@@ -71,9 +69,16 @@
   return { promise, resolve, reject };
 }
 
-beforeEach(() => {
+function resetStores() {
   __resetClientSettingsPersistenceForTests();
   useBrowserSurfaceStore.setState({ activityByTabId: {}, byTabId: {} });
+}
+
+function setup() {
+  using stack = new DisposableStack();
+  resetStores();
+  stack.defer(resetStores);
+  vi.mocked(desktopTabLifetime.acquireDesktopTab).mockClear();
   mocks.getClientSettings.mockReset();
   mocks.setClientSettings.mockReset().mockResolvedValue(undefined);
   mocks.createTab.mockReset().mockResolvedValue(undefined);
@@ -84,27 +89,27 @@
     webPreferences: "contextIsolation=yes",
     preloadUrl: null,
   });
+  stack.use(vi.spyOn(console, "error").mockImplementation(() => undefined));
+  return stack.move();
+}
-  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
-  vi.spyOn(console, "error").mockImplementation(() => undefined);
-  container = document.createElement("div");
-  document.body.append(container);
-});
 
-afterEach(async () => {
-  vi.useFakeTimers();
-  await act(async () => root?.unmount());
-  root = undefined;
-  container.remove();
-  await vi.advanceTimersByTimeAsync(0);
-  vi.useRealTimers();
-  __resetClientSettingsPersistenceForTests();
-  useBrowserSurfaceStore.setState({ activityByTabId: {}, byTabId: {} });
-  vi.restoreAllMocks();
+// The webview releases its desktop tab on a timer after unmount; run it before the test ends
+// so it cannot land in the next one.
+async function renderHost(ui: ReactNode) {
+  await using stack = new AsyncDisposableStack();
+  const screen = stack.use(await render(ui));
+  stack.defer(async () => {
+    using _timers = fakeTimers();
+    await screen.unmount();
+    await vi.advanceTimersByTimeAsync(0);
+  });
+  const environment = stack.move();
+  return Object.assign(screen, { [Symbol.asyncDispose]: () => environment.disposeAsync() });
+}
-  vi.unstubAllGlobals();
-});
 
 describe("HostedBrowserWebview settings hydration", () => {
   it("starts a retained background tab only after a settings read succeeds on retry", async () => {
+    using _setup = setup();
     const firstRead = deferred<ClientSettings | null>();
     const retryRead = deferred<ClientSettings | null>();
     const tabCreation = deferred<void>();
@@ -116,7 +121,7 @@
     // Chromium treats <webview> as an unknown element; give it the one Electron method the
     // host calls once the guest attaches.
     const createElement = document.createElement.bind(document);
-    const createGuest = vi.spyOn(document, "createElement").mockImplementation(((
+    using createGuest = vi.spyOn(document, "createElement").mockImplementation(((
       tagName: string,
       options?: ElementCreationOptions,
     ) => {
@@ -126,7 +131,7 @@
         : element;
     }) as typeof document.createElement);
     const guests = () => createGuest.mock.calls.filter(([tagName]) => tagName === "webview");
+    const guest = () => screen.container.querySelector("webview");
-    const guest = () => container.querySelector("webview");
     const threadRef = {
       environmentId: EnvironmentId.make("host-settings-retry"),
       threadId: ThreadId.make("thread-settings-retry"),
@@ -134,20 +139,17 @@
     const runtimeTabId = "retained-background-tab";
     useBrowserSurfaceStore.getState().acquireActivity(runtimeTabId);
 
-    root = createRoot(container);
-    await act(async () =>
-      root!.render(
-        <HostedBrowserWebview
-          threadRef={threadRef}
-          tabId="server-tab"
-          runtimeTabId={runtimeTabId}
-          initialUrl="https://example.com"
+    await using screen = await renderHost(
+      <HostedBrowserWebview
+        threadRef={threadRef}
-          viewport={FILL_PREVIEW_VIEWPORT}
-          pictureInPicture={false}
-          profileId="work"
-          zoomFactor={1.25}
-        />,
-      ),
+        tabId="server-tab"
+        runtimeTabId={runtimeTabId}
+        initialUrl="https://example.com"
+        viewport={FILL_PREVIEW_VIEWPORT}
+        pictureInPicture={false}
+        profileId="work"
+        zoomFactor={1.25}
+      />,
     );
 
     expect(mocks.getClientSettings).toHaveBeenCalledOnce();
@@ -211,41 +213,48 @@
   const owner = Symbol("panel");
   const panel = { x: 0, y: 0, width: 2000, height: 1600 };
 
+  const webview = (viewport: PreviewViewportSetting) => (
+    <HostedBrowserWebview
+      threadRef={{
+        environmentId: EnvironmentId.make("zoom-layout"),
+        threadId: ThreadId.make("zoom-layout"),
+      }}
+      tabId="zoom-layout"
+      runtimeTabId={runtimeTabId}
+      initialUrl="about:blank"
+      viewport={viewport}
+      pictureInPicture={false}
+      profileId={undefined}
+      zoomFactor={1.5}
+    />
+  );
+
   async function renderViewport(viewport: PreviewViewportSetting) {
     mocks.getClientSettings.mockResolvedValue({
       ...DEFAULT_CLIENT_SETTINGS,
       browserDefaultZoomFactor: 1.5,
     });
-    root ??= createRoot(container);
-    await act(async () => {
-      await ensureClientSettingsHydrated();
-      root!.render(
-        <HostedBrowserWebview
-          threadRef={{
-            environmentId: EnvironmentId.make("zoom-layout"),
-            threadId: ThreadId.make("zoom-layout"),
-          }}
-          tabId="zoom-layout"
-          runtimeTabId={runtimeTabId}
-          initialUrl="about:blank"
-          viewport={viewport}
+    await act(() => ensureClientSettingsHydrated());
+    await using stack = new AsyncDisposableStack();
+    const screen = stack.use(await renderHost(webview(viewport)));
+    const guest = screen.container.querySelector("webview");
+    expect(guest).not.toBeNull();
+    const environment = stack.move();
+    return Object.assign(screen, {
+      guest: guest!,
+      setViewport: (next: PreviewViewportSetting) => screen.rerender(webview(next)),
+      [Symbol.asyncDispose]: () => environment.disposeAsync(),
-          pictureInPicture={false}
-          profileId={undefined}
-          zoomFactor={1.5}
-        />,
-      );
     });
-    const guest = container.querySelector("webview");
-    expect(guest).not.toBeNull();
-    return guest!;
   }
 
   it("keeps fixed viewports sized in guest pixels across host zoom and fitted surfaces", async () => {
+    using _setup = setup();
     let windowZoom = 1.25;
-    vi.stubGlobal("desktopBridge", { getWindowZoomFactor: () => windowZoom });
+    using _bridge = stubGlobal("desktopBridge", { getWindowZoomFactor: () => windowZoom });
     useBrowserSurfaceStore.getState().claim(runtimeTabId, owner, false);
     useBrowserSurfaceStore.getState().present(runtimeTabId, owner, panel, true, 0, 30);
+    await using screen = await renderViewport({ _tag: "freeform", width: 800, height: 600 });
-    const guest = await renderViewport({ _tag: "freeform", width: 800, height: 600 });
+    const { guest } = screen;
     expect(guest.getBoundingClientRect()).toMatchObject({ width: 960, height: 720 });
 
     windowZoom = 0.75;
@@ -258,7 +267,7 @@
 
     windowZoom = 1.25;
     await act(() => window.dispatchEvent(new Event("resize")));
-    await renderViewport({ _tag: "preset", presetId: "iphone-se", width: 375, height: 667 });
+    await screen.setViewport({ _tag: "preset", presetId: "iphone-se", width: 375, height: 667 });
     expect(guest.getBoundingClientRect().width).toBe(450);
     expect(guest.getBoundingClientRect().height).toBeCloseTo(800.4, 1);
 
@@ -278,7 +287,7 @@
       useBrowserSurfaceStore.getState().claim(runtimeTabId, owner, false);
       useBrowserSurfaceStore.getState().present(runtimeTabId, owner, panel, true, 0, 30);
     });
-    await renderViewport(FILL_PREVIEW_VIEWPORT);
+    await screen.setViewport(FILL_PREVIEW_VIEWPORT);
     expect(guest.getBoundingClientRect()).toMatchObject({ width: 2000, height: 1600 });
     expect(mocks.createTab).toHaveBeenCalledExactlyOnceWith(runtimeTabId, {
       zoomFactor: 1.5,
@@ -287,11 +296,14 @@
   });
 
   it("preserves preview scaling with an older bridge or no desktop bridge", async () => {
+    using _setup = setup();
+    using _bridge = stubGlobal("desktopBridge", {});
+    await using screen = await renderViewport({ _tag: "freeform", width: 800, height: 600 });
-    vi.stubGlobal("desktopBridge", {});
+    const { guest } = screen;
-    const guest = await renderViewport({ _tag: "freeform", width: 800, height: 600 });
     expect(guest.getBoundingClientRect()).toMatchObject({ width: 1200, height: 900 });
 
-    vi.stubGlobal("desktopBridge", undefined);
+    // `_bridge` puts the original back after the unmount, so the unmount still sees no bridge.
+    Reflect.set(globalThis, "desktopBridge", undefined);
     await act(() => window.dispatchEvent(new Event("resize")));
     expect(guest.getBoundingClientRect()).toMatchObject({ width: 1200, height: 900 });
   });
```

## apps/web/src/cloud/useCloudLinkController.browser.test.tsx

Status: modified; +68; −56

```diff
@@ -6,10 +6,11 @@
 } from "@t3tools/contracts";
 import type { RelayManagedEndpointProviderKind } from "@t3tools/contracts/relay";
 import { AsyncResult } from "effect/reactivity";
-import { act, useLayoutEffect } from "react";
-import { createRoot, type Root } from "react-dom/client";
-import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
+import { useLayoutEffect } from "react";
+import { describe, expect, it, vi } from "vite-plus/test";
 
+import { act, render } from "../../test/render";
+
 const testState = vi.hoisted(() => ({
   scopes: new Set<string>(),
   snapshot: null as EnvironmentCloudLinkStateResult | null,
@@ -105,29 +106,38 @@
   publishAgentActivity: false,
 };
 
-let root: Root | null = null;
-let controller: ReturnType<typeof useCloudLinkController> | null = null;
+type Controller = ReturnType<typeof useCloudLinkController>;
 
-function ControllerProbe() {
+function ControllerProbe({ onRender }: { onRender: (controller: Controller) => void }) {
   const value = useCloudLinkController();
   useLayoutEffect(() => {
-    controller = value;
+    onRender(value);
   });
   return null;
 }
 
 async function mountController() {
+  let controller: Controller | null = null;
+  const screen = await render(
-  root = createRoot(document.createElement("div"));
-  await act(async () => root!.render(<ControllerProbe />));
+    <ControllerProbe
+      onRender={(value) => {
+        controller = value;
+      }}
+    />,
+  );
-}
-
-async function reconcile(desired: CloudLinkDesiredState) {
-  let succeeded = false;
-  await act(async () => {
-    if (controller === null) throw new Error("Controller is not mounted.");
-    succeeded = await controller.reconcileCloudState(desired);
+  return Object.assign(screen, {
+    get controller() {
+      return controller;
+    },
+    async reconcile(desired: CloudLinkDesiredState) {
+      let succeeded = false;
+      await act(async () => {
+        if (controller === null) throw new Error("Controller is not mounted.");
+        succeeded = await controller.reconcileCloudState(desired);
+      });
+      return succeeded;
+    },
   });
-  return succeeded;
 }
 
 function expectNoMutations() {
@@ -136,9 +146,7 @@
   expect(testState.preferences).not.toHaveBeenCalled();
 }
 
-beforeEach(() => {
-  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
+function setup() {
-  vi.spyOn(console, "error").mockImplementation(() => undefined);
   testState.scopes = new Set([AuthRelayReadScope, AuthRelayWriteScope]);
   testState.snapshot = linkedState;
   testState.renderedSnapshot = linkedState;
@@ -154,34 +162,29 @@
   }
   testState.refreshLink.mockReset();
   testState.toast.mockReset();
-});
+  return vi.spyOn(console, "error").mockImplementation(() => undefined);
+}
 
-afterEach(async () => {
-  await act(async () => root?.unmount());
-  root = null;
-  controller = null;
-  vi.restoreAllMocks();
-  vi.unstubAllGlobals();
-});
-
 describe("useCloudLinkController", () => {
   it.each([AuthRelayReadScope, AuthRelayWriteScope])(
     "does not mutate without %s, even with a cached link snapshot",
     async (scope) => {
+      using _setup = setup();
+      await using screen = await mountController();
-      await mountController();
       testState.scopes.delete(scope);
 
-      expect(await reconcile({ managedTunnel: true, publish: true })).toBe(false);
+      expect(await screen.reconcile({ managedTunnel: true, publish: true })).toBe(false);
       expect(testState.getToken).not.toHaveBeenCalled();
       expectNoMutations();
     },
   );
 
   it("does not interpret unavailable link state as an unlinked environment", async () => {
+    using _setup = setup();
     testState.snapshot = null;
-    await mountController();
+    await using screen = await mountController();
 
-    expect(await reconcile({ managedTunnel: false, publish: true })).toBe(false);
+    expect(await screen.reconcile({ managedTunnel: false, publish: true })).toBe(false);
     expect(testState.getToken).not.toHaveBeenCalled();
     expectNoMutations();
   });
@@ -189,38 +192,41 @@
   it.each([AuthRelayReadScope, AuthRelayWriteScope])(
     "rechecks %s after requesting the cloud token",
     async (scope) => {
-      await mountController();
+      using _setup = setup();
+      await using screen = await mountController();
       testState.getToken.mockImplementationOnce(async () => {
         testState.scopes.delete(scope);
         return "clerk-token";
       });
 
-      expect(await reconcile({ managedTunnel: false, publish: false })).toBe(false);
+      expect(await screen.reconcile({ managedTunnel: false, publish: false })).toBe(false);
       expectNoMutations();
     },
   );
 
   it("stops if the link-state read fails while requesting the cloud token", async () => {
-    await mountController();
+    using _setup = setup();
+    await using screen = await mountController();
     testState.getToken.mockImplementationOnce(async () => {
       testState.snapshot = null;
       return "clerk-token";
     });
 
+    expect(await screen.reconcile({ managedTunnel: true, publish: true })).toBe(false);
-    expect(await reconcile({ managedTunnel: true, publish: true })).toBe(false);
     expectNoMutations();
   });
 
   it("uses the latest linked mode to change publishing without replacing the tunnel", async () => {
+    using _setup = setup();
     testState.snapshot = { ...linkedState, linked: false, managedTunnelActive: false };
     testState.renderedSnapshot = testState.snapshot;
-    await mountController();
+    await using screen = await mountController();
     testState.getToken.mockImplementationOnce(async () => {
       testState.snapshot = linkedState;
       return "clerk-token";
     });
 
-    expect(await reconcile({ managedTunnel: true, publish: true })).toBe(true);
+    expect(await screen.reconcile({ managedTunnel: true, publish: true })).toBe(true);
     expect(testState.link).not.toHaveBeenCalled();
     expect(testState.unlink).not.toHaveBeenCalled();
     expect(testState.preferences).toHaveBeenCalledExactlyOnceWith({
@@ -230,12 +236,13 @@
   });
 
   it("shows a link the relay holds as publish-only as off and relinks it as managed", async () => {
+    using _setup = setup();
     testState.relayProviderKind = "manual";
+    await using screen = await mountController();
-    await mountController();
 
+    expect(screen.controller?.managedTunnelActive).toBe(false);
+    expect(screen.controller?.managedTunnelOutOfSync).toBe(true);
+    expect(await screen.reconcile({ managedTunnel: true, publish: false })).toBe(true);
-    expect(controller?.managedTunnelActive).toBe(false);
-    expect(controller?.managedTunnelOutOfSync).toBe(true);
-    expect(await reconcile({ managedTunnel: true, publish: false })).toBe(true);
     expect(testState.link).toHaveBeenCalledExactlyOnceWith({
       target,
       clerkToken: "clerk-token",
@@ -244,17 +251,18 @@
   });
 
   it("shows a revoked link as off and links it again when turned on", async () => {
+    using _setup = setup();
     testState.snapshot = { ...linkedState, linkRevoked: true };
     testState.renderedSnapshot = testState.snapshot;
-    await mountController();
-    expect(controller).toMatchObject({
+    await using screen = await mountController();
+    expect(screen.controller).toMatchObject({
       linked: false,
       linkRevoked: true,
       managedTunnelActive: false,
       managedTunnelOutOfSync: false,
     });
 
-    expect(await reconcile({ managedTunnel: true, publish: false })).toBe(true);
+    expect(await screen.reconcile({ managedTunnel: true, publish: false })).toBe(true);
     expect(testState.link).toHaveBeenCalledExactlyOnceWith({
       target,
       clerkToken: "clerk-token",
@@ -263,46 +271,50 @@
   });
 
   it("keeps a managed link that the relay also holds as managed", async () => {
+    using _setup = setup();
     testState.relayProviderKind = "cloudflare_tunnel";
-    await mountController();
+    await using screen = await mountController();
 
-    expect(controller?.managedTunnelActive).toBe(true);
-    expect(controller?.managedTunnelOutOfSync).toBe(false);
-    expect(await reconcile({ managedTunnel: true, publish: false })).toBe(true);
+    expect(screen.controller?.managedTunnelActive).toBe(true);
+    expect(screen.controller?.managedTunnelOutOfSync).toBe(false);
+    expect(await screen.reconcile({ managedTunnel: true, publish: false })).toBe(true);
     expect(testState.link).not.toHaveBeenCalled();
   });
 
   it("stops the preference write if management permission is lost after linking", async () => {
-    await mountController();
+    using _setup = setup();
+    await using screen = await mountController();
     testState.link.mockImplementationOnce(async () => {
       testState.scopes.delete(AuthRelayWriteScope);
       return AsyncResult.success({});
     });
 
-    expect(await reconcile({ managedTunnel: false, publish: true })).toBe(false);
+    expect(await screen.reconcile({ managedTunnel: false, publish: true })).toBe(false);
     expect(testState.link).toHaveBeenCalledOnce();
     expect(testState.preferences).not.toHaveBeenCalled();
     expect(testState.refreshLink).toHaveBeenCalledOnce();
   });
 
   it("still lets an authorized user unlink when the cloud token is unavailable", async () => {
-    await mountController();
+    using _setup = setup();
+    await using screen = await mountController();
     testState.getToken.mockRejectedValueOnce(new Error("Cloud sign-in unavailable"));
 
-    expect(await reconcile({ managedTunnel: false, publish: false })).toBe(true);
+    expect(await screen.reconcile({ managedTunnel: false, publish: false })).toBe(true);
     expect(testState.unlink).toHaveBeenCalledExactlyOnceWith({ target, clerkToken: null });
     expect(testState.link).not.toHaveBeenCalled();
     expect(testState.preferences).not.toHaveBeenCalled();
   });
 
   it("unlinks if the link-state read fails while requesting the cloud token", async () => {
+    using _setup = setup();
+    await using screen = await mountController();
-    await mountController();
     testState.getToken.mockImplementationOnce(async () => {
       testState.snapshot = null;
       throw new Error("Cloud sign-in unavailable");
     });
 
+    expect(await screen.reconcile({ managedTunnel: false, publish: false })).toBe(true);
-    expect(await reconcile({ managedTunnel: false, publish: false })).toBe(true);
     expect(testState.unlink).toHaveBeenCalledExactlyOnceWith({ target, clerkToken: null });
     expect(testState.link).not.toHaveBeenCalled();
     expect(testState.preferences).not.toHaveBeenCalled();
```

## apps/web/src/components/BranchToolbar.panel.browser.test.tsx

Status: modified; +33; −49

```diff
@@ -1,10 +1,10 @@
 import { EnvironmentId, ProjectId, ThreadId } from "@t3tools/contracts";
-import { act, type ComponentProps } from "react";
-import { createRoot, type Root } from "react-dom/client";
-import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
+import type { ComponentProps } from "react";
+import { expect, it, vi } from "vite-plus/test";
 import { page, userEvent } from "vite-plus/test/browser";
 
 import "~/index.css";
+import { act, render as renderUi } from "../../test/render";
 
 const state = vi.hoisted(() => ({
   worktreePath: "/tmp/worktree" as string | null,
@@ -32,51 +32,36 @@
 
 import { BranchToolbar } from "./BranchToolbar";
 
+function render(
+  props: Partial<ComponentProps<typeof BranchToolbar>>,
+  { worktreePath = "/tmp/worktree" }: { worktreePath?: string | null } = {},
+) {
+  state.worktreePath = worktreePath;
-let root: Root;
-let container: HTMLDivElement;
-
-beforeEach(() => {
-  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
-  state.worktreePath = "/tmp/worktree";
   state.showContextMenu.mockReset().mockResolvedValue("copy-path");
   state.writeTextToClipboard.mockReset().mockResolvedValue(true);
-  container = document.createElement("div");
-  document.body.append(container);
-  root = createRoot(container);
-});
-
-afterEach(async () => {
-  await act(async () => root.unmount());
-  container.remove();
-  vi.unstubAllGlobals();
-});
-
-function render(props: Partial<ComponentProps<typeof BranchToolbar>>) {
-  return act(async () =>
-    root.render(
-      <BranchToolbar
-        layout="panel"
-        panelSection="workspace"
-        environmentId={EnvironmentId.make("local")}
-        threadId={ThreadId.make("thread")}
-        projectId={ProjectId.make("project")}
-        worktreePath={state.worktreePath}
-        isServerThread
-        showGitControls
-        envMode="local"
+  return renderUi(
+    <BranchToolbar
+      layout="panel"
+      panelSection="workspace"
+      environmentId={EnvironmentId.make("local")}
+      threadId={ThreadId.make("thread")}
+      projectId={ProjectId.make("project")}
+      worktreePath={state.worktreePath}
+      isServerThread
+      showGitControls
+      envMode="local"
-        envLocked={false}
+      envLocked={false}
-        startFromOrigin={false}
-        onStartFromOriginChange={vi.fn()}
-        onEnvModeChange={vi.fn()}
+      startFromOrigin={false}
+      onStartFromOriginChange={vi.fn()}
+      onEnvModeChange={vi.fn()}
+      {...props}
+    />,
-        {...props}
-      />,
-    ),
   );
 }
 
 it("keeps machine choices usable when the combined row's workspace is locked", async () => {
   const onEnvironmentChange = vi.fn();
-  await render({
+  await using screen = await render({
     onEnvironmentChange,
     availableEnvironments: ["local", "remote"].map((id) => ({
       environmentId: EnvironmentId.make(id),
@@ -86,9 +71,9 @@
       machine: "server",
     })),
   });
-  const trigger = container.querySelector("button")!;
+  const trigger = screen.container.querySelector("button")!;
   expect(trigger.textContent).toBe("local");
+  expect(screen.container.querySelectorAll("button")).toHaveLength(1);
-  expect(container.querySelectorAll("button")).toHaveLength(1);
 
   // A ctrl-click is a macOS secondary click; it must not open the menu.
   await act(async () => {
@@ -105,7 +90,7 @@
 });
 
 it("shows a started thread's primary machine as a static label in the composer", async () => {
-  await render({
+  await using screen = await render({
     layout: "composer",
     showGitControls: false,
     availableEnvironments: ["local", "remote"].map((id) => ({
@@ -116,8 +101,8 @@
       machine: "server",
     })),
   });
+  expect(screen.container.textContent).toBe("local");
+  expect(screen.container.querySelector("button")).toBeNull();
-  expect(container.textContent).toBe("local");
-  expect(container.querySelector("button")).toBeNull();
 });
 
 it.each([
@@ -128,11 +113,10 @@
 ] as const)(
   "copies only an existing workspace path with mode %s and worktree %s",
   async (envMode, worktreePath, copiedPath) => {
-    state.worktreePath = worktreePath;
+    await using screen = await render({ envMode }, { worktreePath });
-    await render({ envMode });
     const event = new MouseEvent("contextmenu", { bubbles: true, cancelable: true });
     await act(async () => {
-      container.querySelector('[aria-label="Run context"]')!.dispatchEvent(event);
+      screen.container.querySelector('[aria-label="Run context"]')!.dispatchEvent(event);
     });
     expect(event.defaultPrevented).toBe(copiedPath !== null);
     if (copiedPath === null) {
```

## apps/web/src/components/ChatMarkdown.assets.browser.test.tsx

Status: modified; +45; −59

```diff
@@ -12,9 +12,10 @@
 import * as Cause from "effect/Cause";
 import * as Schema from "effect/Schema";
 import { AtomRegistry } from "effect/reactivity";
+import type { ReactNode } from "react";
+import { expect, it, vi } from "vite-plus/test";
+
+import { act, render } from "../../test/render";
-import { act, type ReactNode } from "react";
-import { createRoot, type Root } from "react-dom/client";
-import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
 
 const mint = vi.hoisted(() => vi.fn<() => Promise<AssetCreateUrlResult>>());
 const remoteOpen = vi.hoisted(() => ({
@@ -87,9 +88,6 @@
 import ChatMarkdown from "./ChatMarkdown";
 import { toastManager } from "./ui/toast";
 
-const roots: Root[] = [];
-let registry: AtomRegistry.AtomRegistry;
-
 const imageUrl = (version: string) =>
   `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>#${version}`;
 const assetResult = (version: string): AssetCreateUrlResult => ({
@@ -97,22 +95,15 @@
   expiresAt: 1,
 });
 
-beforeEach(() => {
-  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
-  registry = AtomRegistry.make();
+function setup() {
   mint.mockReset().mockResolvedValue(assetResult("updated"));
-});
-afterEach(async () => {
-  for (const root of roots.splice(0)) await act(async () => root.unmount());
-  registry.dispose();
-  document.body.replaceChildren();
   remoteOpen.isResolved = true;
   remoteOpen.state.mode = "local-exec";
   remoteOpen.label = "media-host";
   remoteOpen.httpBaseUrl = "https://host.test";
+  const registry = AtomRegistry.make();
-  vi.restoreAllMocks();
-  vi.unstubAllGlobals();
-});
+  return { registry, [Symbol.dispose]: () => registry.dispose() };
+}
 
 function deferred<T>() {
   let resolve!: (value: T) => void;
@@ -124,27 +115,21 @@
   return { promise, resolve, reject };
 }
 
-function createTestRoot() {
-  const container = document.body.appendChild(document.createElement("div"));
-  const root = createRoot(container);
-  roots.push(root);
+async function renderInRegistry(registry: AtomRegistry.AtomRegistry, ui: ReactNode) {
-  return {
-    container,
-    render: (children: ReactNode) =>
-      act(async () => {
-        root.render(
-          <RegistryContext.Provider value={registry}>{children}</RegistryContext.Provider>,
-        );
-      }),
-  };
+  const wrap = (node: ReactNode) => (
+    <RegistryContext.Provider value={registry}>{node}</RegistryContext.Provider>
+  );
+  const screen = await render(wrap(ui));
+  return Object.assign(screen, { update: (node: ReactNode) => screen.rerender(wrap(node)) });
 }
 
 it("opens host media through server authorization before the client grant loads", async () => {
+  using env = setup();
   mint.mockResolvedValue({ relativeUrl: "/api/assets/image.png", expiresAt: 1 });
   const expanded = deferred<unknown>();
   const onImageExpand = vi.fn(expanded.resolve);
+  await using screen = await renderInRegistry(
-  const { container, render } = createTestRoot();
+    env.registry,
-  await render(
     <ChatMarkdown
       cwd="/repo"
       threadRef={{
@@ -155,7 +140,7 @@
       onImageExpand={onImageExpand}
     />,
   );
-  const link = container.querySelector<HTMLAnchorElement>('a[href="/tmp/image.png"]');
+  const link = screen.container.querySelector<HTMLAnchorElement>('a[href="/tmp/image.png"]');
   expect(link).not.toBeNull();
   await act(async () => link!.click());
   expect(await expanded.promise).toMatchObject({
@@ -190,6 +175,7 @@
     description: "Only images, videos, audio, HTML, and PDF files can be previewed.",
   },
 ])("reports the media failure for $name", async (scenario) => {
+  using env = setup();
   const lookup = deferred<AssetCreateUrlResult>();
   const started = deferred<void>();
   const reported = deferred<Parameters<typeof toastManager.add>[0]>();
@@ -197,17 +183,16 @@
     started.resolve();
     return lookup.promise;
   });
-  vi.spyOn(toastManager, "add").mockImplementation((toast) => {
+  using _toast = vi.spyOn(toastManager, "add").mockImplementation((toast) => {
     reported.resolve(toast);
     return "media-toast";
   });
   remoteOpen.isResolved = false;
-  const { container, render } = createTestRoot();
   const content = (
     <ChatMarkdown cwd="/repo" threadRef={threadRef} text="[Open image](~/Downloads/image.png)" />
   );
+  await using screen = await renderInRegistry(env.registry, content);
-  await render(content);
+  const link = screen.container.querySelector<HTMLAnchorElement>('a[href="~/Downloads/image.png"]');
-  const link = container.querySelector<HTMLAnchorElement>('a[href="~/Downloads/image.png"]');
   expect(link).not.toBeNull();
   await act(async () => {
     link!.click();
@@ -218,7 +203,7 @@
   remoteOpen.state.mode = scenario.local ? "local-exec" : "remote-unavailable";
   remoteOpen.httpBaseUrl = scenario.local ? "http://localhost:3773" : "https://host.test";
   remoteOpen.label = "settled-host";
-  await render(content);
+  await screen.update(content);
   const resource = {
     _tag: "media-file" as const,
     path: "~/Downloads/image.png",
@@ -258,33 +243,33 @@
 }
 
 it("ordinary chat keeps its embedded asset URL across renders and reopening", async () => {
-  const { container, render } = createTestRoot();
-  await render(markdown());
+  using env = setup();
-  expect(renderedImage(container)).toBe(imageUrl("updated"));
+  await using screen = await renderInRegistry(env.registry, markdown());
+  expect(renderedImage(screen.container)).toBe(imageUrl("updated"));
   mint.mockResolvedValue(assetResult("changed"));
 
+  await screen.update(markdown());
+  await screen.update(null);
-  await render(markdown());
+  await screen.update(markdown());
-  await render(null);
-  await render(markdown());
 
+  expect(renderedImage(screen.container)).toBe(imageUrl("updated"));
-  expect(renderedImage(container)).toBe(imageUrl("updated"));
   expect(mint).toHaveBeenCalledTimes(1);
 });
 
 it.each(["URL", "failure"] as const)(
   "reopening a file preview with no workspace mutation replaces its cached %s",
   async (cached) => {
+    using env = setup();
     if (cached === "failure") mint.mockRejectedValueOnce(new Error("Image missing"));
     else mint.mockResolvedValueOnce(assetResult("stale"));
-    const { container, render } = createTestRoot();
-    await render(markdown(null));
-    expect(renderedImage(container)).toBe(cached === "URL" ? imageUrl("stale") : undefined);
+    await using screen = await renderInRegistry(env.registry, markdown(null));
+    expect(renderedImage(screen.container)).toBe(cached === "URL" ? imageUrl("stale") : undefined);
+    await screen.update(null);
-    await render(null);
 
     mint.mockResolvedValue(assetResult("updated"));
+    await screen.update(markdown(null));
-    await render(markdown(null));
 
-    expect(renderedImage(container)).toBe(imageUrl("updated"));
+    expect(renderedImage(screen.container)).toBe(imageUrl("updated"));
     expect(mint).toHaveBeenCalledTimes(2);
   },
 );
@@ -292,21 +277,22 @@
 it.each(["initial", "refresh"] as const)(
   "shows the latest workspace edit after a pending %s asset request settles",
   async (request) => {
+    using env = setup();
     let resolvePending!: (value: AssetCreateUrlResult) => void;
     const pending = new Promise<AssetCreateUrlResult>((resolve) => {
       resolvePending = resolve;
     });
-    const { container, render } = createTestRoot();
+    await using screen = await renderInRegistry(env.registry, null);
     if (request === "refresh") {
-      await render(markdown("m0"));
-      expect(renderedImage(container)).toBe(imageUrl("updated"));
+      await screen.update(markdown("m0"));
+      expect(renderedImage(screen.container)).toBe(imageUrl("updated"));
     }
 
     mint.mockReturnValueOnce(pending).mockResolvedValue(assetResult("latest"));
+    await screen.update(markdown("m1"));
-    await render(markdown("m1"));
     const callsBeforeEdits = mint.mock.calls.length;
+    await screen.update(markdown("m2"));
+    await screen.update(markdown("m3"));
-    await render(markdown("m2"));
-    await render(markdown("m3"));
     expect(mint).toHaveBeenCalledTimes(callsBeforeEdits);
 
     await act(async () => {
@@ -314,9 +300,9 @@
       await pending;
     });
 
-    expect(renderedImage(container)).toBe(imageUrl("latest"));
+    expect(renderedImage(screen.container)).toBe(imageUrl("latest"));
     expect(mint).toHaveBeenCalledTimes(callsBeforeEdits + 1);
+    await screen.update(markdown("m3"));
-    await render(markdown("m3"));
     expect(mint).toHaveBeenCalledTimes(callsBeforeEdits + 1);
   },
 );
```

## apps/web/src/components/ChatMarkdown.browser.test.tsx

Status: modified; +267; −188

````diff
@@ -2,11 +2,11 @@
 import { searchableMessageSegments } from "@t3tools/shared/threadFindText";
 import { countThreadSearchOccurrences } from "@t3tools/shared/threadSearch";
 import { AsyncResult } from "effect/reactivity";
-import { act, useEffect, type ReactElement } from "react";
-import { createRoot, type Root } from "react-dom/client";
-import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
+import { useEffect, type ReactElement } from "react";
+import { describe, expect, it, vi } from "vite-plus/test";
 import { page, userEvent } from "vite-plus/test/browser";
 
+import { act, render, stubGlobal } from "../../test/render";
 import "../index.css";
 import { useTheme } from "../hooks/useTheme";
 import { applyThemeColorPreview, getStandardThemeColors } from "../themePalette";
@@ -86,37 +86,15 @@
 import { serializeTableElementToCsv, chatMarkdownClipboardPayload } from "../markdown-clipboard";
 import { captureAssistantTextSelection } from "../lib/assistantTextSelection";
 
-const roots: Root[] = [];
+function setup() {
-
-beforeEach(() => {
   settingsOverrides.mathRenderingEnabled = false;
   renderDiagram.mockReset();
   workspaceSearch.mockReset().mockResolvedValue(AsyncResult.success({ entries: [] }));
   filesystemMetadata.mockReset().mockResolvedValue(AsyncResult.success({ entries: [null] }));
-  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
-});
-
-afterEach(async () => {
-  for (const root of roots.splice(0)) await act(async () => root.unmount());
-  document.body.replaceChildren();
-  CSS.highlights.clear();
-  vi.restoreAllMocks();
-  vi.unstubAllGlobals();
-});
-
-async function render(element: ReactElement) {
-  const container = document.body.appendChild(document.createElement("div"));
-  const root = createRoot(container);
-  roots.push(root);
-  await act(async () => root.render(element));
-  return {
-    container,
-    rerender: (next: ReactElement) => act(async () => root.render(next)),
-  };
 }
 
-async function renderText(text: string, props: Partial<Parameters<typeof ChatMarkdown>[0]> = {}) {
+function renderText(text: string, props: Partial<Parameters<typeof ChatMarkdown>[0]> = {}) {
+  return render(<ChatMarkdown cwd="/tmp/project" text={text} {...props} />);
-  return (await render(<ChatMarkdown cwd="/tmp/project" text={text} {...props} />)).container;
 }
 
 function linkTexts(container: HTMLElement) {
@@ -136,16 +114,10 @@
 }
 
 describe("ChatMarkdown theme drafts", () => {
-  afterEach(() => {
-    act(() => {
-      localStorage.clear();
-      window.dispatchEvent(new StorageEvent("storage", { key: null }));
-    });
-  });
-
   it.each(["dark", "light"] as const)(
     "previews opposite syntax colors and restores saved %s colors on cancel",
     async (savedAppearance) => {
+      setup();
       let selection: ReturnType<typeof useTheme>;
       function Preview() {
         const theme = useTheme();
@@ -155,7 +127,15 @@
         return <ChatMarkdown cwd={undefined} text={"```typescript\nconst answer = 42;\n```"} />;
       }
       await getSyntaxHighlighterPromise("typescript");
-      const { container } = await render(<Preview />);
+      await using screen = await render(<Preview />);
+      // Disposed before the unmount, while useTheme still listens for the storage event.
+      using _storage = {
+        [Symbol.dispose]() {
+          localStorage.clear();
+          window.dispatchEvent(new StorageEvent("storage", { key: null }));
+        },
+      };
+      const { container } = screen;
       await act(async () => {
         selection.setTheme(savedAppearance);
         selection.setAppearanceMode(savedAppearance);
@@ -199,7 +179,8 @@
   ])(
     "wraps expanded cells with fit=$fits in a $width rem container (timeline=$timeline)",
     async ({ width, timeline, fits }) => {
+      setup();
+      await using screen = await render(
-      const { container } = await render(
         <div style={{ width: `${width}rem` }}>
           <div className={timeline ? "@container/chat-lane chat-content-lane overflow-x-clip" : ""}>
             <ChatMarkdown
@@ -213,6 +194,7 @@
           </div>
         </div>,
       );
+      const { container } = screen;
       const table = container.querySelector("table")!;
       const viewport = table.closest<HTMLElement>('[data-slot="scroll-area-viewport"]')!;
       const cell = table.tBodies[0]!.rows[0]!.cells[0]!;
@@ -252,9 +234,11 @@
   it.each(["<A>", "<a>", "<a >", "<a/>", "<A/>", "<a />"])(
     "preserves unmatched %s without linking later blocks",
     async (token) => {
-      const container = await renderText(
+      setup();
+      await using screen = await renderText(
         `- **"From ${token}"** appears in the header.\n\n- **Tests:** cover inheritance.\n\nThe deferred move continues on B.\n\nSee <a href="https://example.com">the link</a>.`,
       );
+      const { container } = screen;
       expect(container.querySelector("strong")?.textContent).toBe(`"From ${token}"`);
       expect(linkTexts(container)).toEqual(["the link"]);
       expect(container.querySelectorAll("li")).toHaveLength(2);
@@ -267,29 +251,37 @@
   it.each(["</a>  ", "<div>more</div>\n</a>"])(
     "preserves a paired anchor closing in the raw block %s",
     async (closing) => {
-      const container = await renderText(`See <a>label\n\n${closing}\n\nfinish`);
+      setup();
+      await using screen = await renderText(`See <a>label\n\n${closing}\n\nfinish`);
+      const { container } = screen;
       expect(container.querySelector("p")?.textContent).toBe("See label");
     },
   );
 
   it("preserves a paired anchor after comment-looking raw text", async () => {
+    setup();
-    const container = await renderText("See <a>label<script><!-- </script> --></a>");
+    await using screen = await renderText("See <a>label<script><!-- </script> --></a>");
+    const { container } = screen;
     expect(container.querySelector("p")?.textContent).toBe("See label -->");
   });
 
   it.each(["<!-- </a> -->", '<div title="</a>">more</div>', '<script>"</a>"</script>'])(
     "ignores apparent closing anchors inside %s",
     async (html) => {
+      setup();
+      await using screen = await renderText(`Before <A>.\n\n${html}\n\nAfter.`);
+      const { container } = screen;
-      const container = await renderText(`Before <A>.\n\n${html}\n\nAfter.`);
       expect(container.querySelector("p")?.textContent).toBe("Before <A>.");
       expect(container.querySelectorAll("a")).toHaveLength(0);
     },
   );
 
   it("preserves paired HTML anchors, details, markdown links, and inline code", async () => {
-    const container = await renderText(
+    setup();
+    await using screen = await renderText(
       'Bare <a>label</a>, <a id="section"></a>, `<A>`, and [docs](https://example.com).\n\n<details><summary>More</summary>Details</details>',
     );
+    const { container } = screen;
     expect(linkTexts(container)).toEqual(["label", "", "docs"]);
     expect(container.querySelector("code")?.textContent).toBe("<A>");
     expect(container.querySelector("[data-markdown-details]")?.textContent).toContain("More");
@@ -298,6 +290,7 @@
 
 describe("ChatMarkdown Mermaid streaming", () => {
   it("reveals a completed fence while streaming and preserves it through later prose and stream end", async () => {
+    setup();
     let finishRender!: (label: string) => void;
     renderDiagram.mockReturnValue(
       new Promise<string>((resolve) => {
@@ -310,7 +303,8 @@
     const message = (text: string, isStreaming = true) => (
       <ChatMarkdown cwd={undefined} text={text} isStreaming={isStreaming} />
     );
+    await using screen = await render(message(open));
-    const { container, rerender } = await render(message(open));
+    const { container, rerender } = screen;
     expect(container.querySelector("pre")?.textContent).toContain(source);
     expect(renderDiagram).not.toHaveBeenCalled();
 
@@ -353,8 +347,10 @@
       "\n  ```",
     ],
   ])("keeps a %s in source until a real closer arrives", async (_, open, closing) => {
+    setup();
     const message = (text: string) => <ChatMarkdown cwd={undefined} text={text} isStreaming />;
+    await using screen = await render(message(open));
+    const { container, rerender } = screen;
-    const { container, rerender } = await render(message(open));
     expect(renderDiagram).not.toHaveBeenCalled();
     expect(container.querySelector("pre")?.textContent).toContain("A --> B");
 
@@ -363,9 +359,11 @@
   });
 
   it("does not mistake a CRLF stream cut after CR for a closed fence", async () => {
+    setup();
     const open = "```mermaid\r\nflowchart LR\r\nA --> B\r\n\t```";
     const message = (text: string) => <ChatMarkdown cwd={undefined} text={text} isStreaming />;
+    await using screen = await render(message(open));
-    const { container, rerender } = await render(message(open));
+    const { container, rerender } = screen;
     expect(renderDiagram).not.toHaveBeenCalled();
     await rerender(message(`${open}\r`));
     expect(renderDiagram).not.toHaveBeenCalled();
@@ -376,12 +374,16 @@
   });
 
   it("falls back to source when a completed diagram fails during streaming", async () => {
+    setup();
     await getSyntaxHighlighterPromise("mermaid");
     renderDiagram.mockImplementation(() => {
       throw new Error("Invalid Mermaid diagram");
     });
+    using _consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
-    vi.spyOn(console, "error").mockImplementation(() => {});
+    await using screen = await renderText("```mermaid\ninvalid diagram\n```", {
-    const container = await renderText("```mermaid\ninvalid diagram\n```", { isStreaming: true });
+      isStreaming: true,
+    });
+    const { container } = screen;
     expect(renderDiagram).toHaveBeenCalledWith(expect.stringContaining("invalid diagram"));
     expect(container.querySelector('svg[role="img"]')).toBeNull();
     expect(container.querySelector("pre")?.textContent).toContain("invalid diagram");
@@ -391,9 +393,10 @@
 
 describe("ChatMarkdown context references", () => {
   it("renders text and image references through the chip renderer, with readable fallback", async () => {
+    setup();
     const text =
       "See [Terminal output](t3-context://v1/terminal/term-1) and ![Error image](t3-context://v1/image/img-1).";
-    const { container, rerender } = await render(
+    await using screen = await render(
       <ChatMarkdown
         cwd={undefined}
         text={text}
@@ -404,6 +407,7 @@
         )}
       />,
     );
+    const { container, rerender } = screen;
     expect([...container.querySelectorAll("button")].map((button) => button.textContent)).toEqual([
       "terminal: Terminal output",
       "image: Error image",
@@ -415,8 +419,9 @@
   });
 
   it("reads formatted context labels through nested markup instead of the context id", async () => {
+    setup();
     const seen: Array<string> = [];
-    await render(
+    await using _screen = await render(
       <ChatMarkdown
         cwd={undefined}
         text="See [**Bold** `code`](t3-context://v1/terminal/term-1)."
@@ -432,8 +437,10 @@
 
 describe("ChatMarkdown favicon privacy", () => {
   it("suppresses private link images while preserving public links across updates", async () => {
+    setup();
     const markdown = (url: string) => <ChatMarkdown cwd="/tmp/project" text={`[Link](${url})`} />;
+    await using screen = await render(markdown("https://example.com"));
+    const { container, rerender } = screen;
-    const { container, rerender } = await render(markdown("https://example.com"));
     expect([...container.querySelectorAll("img")].map((image) => image.src)).toEqual([
       "https://www.google.com/s2/favicons?domain=example.com&sz=32",
     ]);
@@ -451,6 +458,7 @@
 
 describe("ChatMarkdown streaming", () => {
   it("runs only a complete single-line shell block after a click", async () => {
+    setup();
     const onRunShellCommand = vi.fn();
     const message = (text: string, isStreaming = false) => (
       <ChatMarkdown
@@ -460,7 +468,8 @@
         onRunShellCommand={onRunShellCommand}
       />
     );
-    const { container, rerender } = await render(message("```bash\necho hello\n```", true));
+    await using screen = await render(message("```bash\necho hello\n```", true));
+    const { container, rerender } = screen;
     const runButtons = () => buttonsLabelled(container, "Run in terminal");
     expect(runButtons()).toHaveLength(0);
 
@@ -497,10 +506,12 @@
   });
 
   it("does not retokenize completed lines when streaming finishes", async () => {
+    setup();
     const highlighter = await getSyntaxHighlighterPromise("typescript");
+    using highlight = vi.spyOn(highlighter, "codeToHast");
-    const highlight = vi.spyOn(highlighter, "codeToHast");
     const text = "```typescript\nconst completed = 1;\nconst current = 2;";
-    const { rerender } = await render(<ChatMarkdown cwd="/tmp/project" text={text} isStreaming />);
+    await using screen = await render(<ChatMarkdown cwd="/tmp/project" text={text} isStreaming />);
+    const { rerender } = screen;
     await vi.waitFor(() => expect(highlight).toHaveBeenCalled());
     highlight.mockClear();
     await rerender(<ChatMarkdown cwd="/tmp/project" text={text + "\n```"} />);
@@ -508,19 +519,21 @@
   });
 
   it("recovers highlighting after a failed fence changes without resetting its controls", async () => {
+    setup();
     const highlighter = await getSyntaxHighlighterPromise("text");
     const codeToHast = highlighter.codeToHast.bind(highlighter);
     let fail = true;
-    vi.spyOn(highlighter, "codeToHast").mockImplementation((...args) => {
+    using _codeToHast = vi.spyOn(highlighter, "codeToHast").mockImplementation((...args) => {
       if (fail) throw new Error("Temporary highlighter failure");
       return codeToHast(...args);
     });
+    using _consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
+    using _consoleWarn = vi.spyOn(console, "warn").mockImplementation(() => {});
-    vi.spyOn(console, "error").mockImplementation(() => {});
-    vi.spyOn(console, "warn").mockImplementation(() => {});
 
-    const { container, rerender } = await render(
+    await using screen = await render(
       <ChatMarkdown cwd="/tmp/project" text={"```text\ninitial\n```"} isStreaming />,
     );
+    const { container, rerender } = screen;
     const codeBlock = container.querySelector<HTMLElement>('[data-language="text"]')!;
     const initialWrap = codeBlock.dataset.wrap === "true";
     await act(async () =>
@@ -540,9 +553,10 @@
   });
 
   it("preserves code controls and details without highlighting an unchanged fence again", async () => {
+    setup();
     const highlighter = await getSyntaxHighlighterPromise("text");
+    using highlight = vi.spyOn(highlighter, "codeToHast");
-    const highlight = vi.spyOn(highlighter, "codeToHast");
-    const writeText = vi
+    using writeText = vi
       .spyOn(navigator.clipboard, "writeText")
       .mockImplementation(async () => undefined);
     const text = [
@@ -559,9 +573,8 @@
       "Streaming reply",
     ].join("\n");
 
+    await using screen = await render(<ChatMarkdown cwd="/tmp/project" text={text} isStreaming />);
-    const { container, rerender } = await render(
-      <ChatMarkdown cwd="/tmp/project" text={text} isStreaming />,
-    );
+    const { container, rerender } = screen;
     const codeBlock = container.querySelector<HTMLElement>('[data-language="text"]')!;
     const initialWrap = codeBlock.dataset.wrap === "true";
     await act(async () => {
@@ -603,6 +616,7 @@
   });
 
   it("edits the current task text and marker after reusing a renderer", async () => {
+    setup();
     let editedText: string | undefined;
     let rerender: (next: ReactElement) => Promise<void> = async () => {};
     const message = (text: string): ReactElement => (
@@ -615,7 +629,7 @@
         }}
       />
     );
-    const rendered = await render(message("- [ ] First\n- [ ] Second"));
+    await using rendered = await render(message("- [ ] First\n- [ ] Second"));
     rerender = rendered.rerender;
     const { container } = rendered;
     const checkboxes = () => container.querySelectorAll<HTMLInputElement>('input[type="checkbox"]');
@@ -631,8 +645,10 @@
 
 describe("ChatMarkdown skill chips", () => {
   it("updates digit-leading skill labels when discovered skills change", async () => {
+    setup();
     const text = "Use $2spec with a $20k budget.";
+    await using screen = await render(<ChatMarkdown cwd="/tmp/project" text={text} />);
+    const { container, rerender } = screen;
-    const { container, rerender } = await render(<ChatMarkdown cwd="/tmp/project" text={text} />);
     expect(container.textContent).not.toContain("2Spec");
 
     await rerender(
@@ -674,6 +690,7 @@
   ])(
     "opens a repo-relative chip at its indexed path in $cwd",
     async ({ cwd, path, indexedPath }) => {
+      setup();
       workspaceSearch.mockResolvedValue(
         AsyncResult.success({
           entries: [
@@ -683,10 +700,11 @@
           truncated: false,
         }),
       );
-      const openFile = vi
+      using openFile = vi
         .spyOn(useRightPanelStore.getState(), "openFile")
         .mockImplementation(() => {});
+      await using screen = await renderText(`[Notes](${path}:7)`, { cwd, threadRef });
+      const { container } = screen;
-      const container = await renderText(`[Notes](${path}:7)`, { cwd, threadRef });
 
       await act(async () => container.querySelector("a")!.click());
 
@@ -695,6 +713,7 @@
   );
 
   it("prefers an existing literal nested file over the stripped Windows match", async () => {
+    setup();
     filesystemMetadata.mockResolvedValue(
       AsyncResult.success({ entries: [{ kind: "file", byteLength: 10 }] }),
     );
@@ -707,13 +726,14 @@
         truncated: false,
       }),
     );
-    const openFile = vi
+    using openFile = vi
       .spyOn(useRightPanelStore.getState(), "openFile")
       .mockImplementation(() => {});
+    await using screen = await renderText("[Notes](physics/notes/a.md)", {
-    const container = await renderText("[Notes](physics/notes/a.md)", {
       cwd: "C:/school/Physics",
       threadRef,
     });
+    const { container } = screen;
 
     await act(async () => container.querySelector("a")!.click());
 
@@ -721,6 +741,7 @@
   });
 
   it("keeps a literal file omitted by a truncated search", async () => {
+    setup();
     workspaceSearch.mockResolvedValue(
       AsyncResult.success({
         entries: [{ path: "notes/a.md", kind: "file" }],
@@ -730,13 +751,14 @@
     filesystemMetadata.mockResolvedValue(
       AsyncResult.success({ entries: [{ kind: "file", byteLength: 10 }] }),
     );
-    const openFile = vi
+    using openFile = vi
       .spyOn(useRightPanelStore.getState(), "openFile")
       .mockImplementation(() => {});
-    const container = await renderText("[Notes](physics/notes/a.md:7)", {
+    await using screen = await renderText("[Notes](physics/notes/a.md:7)", {
       cwd: "/school/physics",
       threadRef,
     });
+    const { container } = screen;
 
     await act(async () => container.querySelector("a")!.click());
 
@@ -757,20 +779,22 @@
   ])(
     "keeps the literal target for a $kind when only its stripped twin exists",
     async ({ href, imageBaseDir }) => {
+      setup();
       workspaceSearch.mockResolvedValue(
         AsyncResult.success({
           entries: [{ path: "notes/a.md", kind: "file" }],
           truncated: false,
         }),
       );
-      const openFile = vi
+      using openFile = vi
         .spyOn(useRightPanelStore.getState(), "openFile")
         .mockImplementation(() => {});
-      const container = await renderText(`[Notes](${href})`, {
+      await using screen = await renderText(`[Notes](${href})`, {
         cwd: "/school/physics",
         imageBaseDir,
         threadRef,
       });
+      const { container } = screen;
 
       await act(async () => container.querySelector("a")!.click());
 
@@ -781,6 +805,7 @@
   );
 
   it("keeps a newer absolute file open when an earlier repo-relative lookup finishes", async () => {
+    setup();
     const result = AsyncResult.success({
       entries: [{ path: "notes/a.md", kind: "file" }],
       truncated: false,
@@ -790,16 +815,17 @@
       resolvePending = resolve;
     });
     workspaceSearch.mockReturnValueOnce(pending);
+    using openFile = vi
-    const openFile = vi
       .spyOn(useRightPanelStore.getState(), "openFile")
       .mockImplementation(() => {});
-    const container = await renderText(
+    await using screen = await renderText(
       "[Notes](physics/notes/a.md) and [Other](/elsewhere/b.md:9)",
       {
         cwd: "/school/physics",
         threadRef,
       },
     );
+    const { container } = screen;
     const [notes, other] = container.querySelectorAll("a");
     await act(async () => notes!.click());
     expect(workspaceSearch).toHaveBeenCalledOnce();
@@ -817,20 +843,24 @@
   it.each([true, false])(
     "renders Codex file citations as file chips with parseRawHtml=%s",
     async (parseRawHtml) => {
-      const container = await renderText(
+      setup();
+      await using screen = await renderText(
         'Created :codex-file-citation{path="/tmp/project/outputs/report.xlsx" purpose="output"}.',
         { lineBreaks: !parseRawHtml, parseRawHtml },
       );
+      const { container } = screen;
       expect(container.textContent).not.toContain("codex-file-citation");
       expect(fileOptions(container)).toEqual(["File options for report.xlsx"]);
     },
   );
 
   it("leaves an unfinished streaming citation visible until it is complete", async () => {
+    setup();
-    const container = await renderText(
+    await using screen = await renderText(
       'Created :codex-file-citation{path="/tmp/project/outputs/report.xlsx"',
       { isStreaming: true },
     );
+    const { container } = screen;
     expect(container.textContent).toContain(":codex-file-citation");
     expect(fileOptions(container)).toEqual([]);
   });
@@ -840,55 +870,66 @@
     ':codex-file-citation-extra{path="/tmp/project/outputs/report.xlsx"}',
     'Example: \\:codex-file-citation{path="/tmp/project/outputs/report.xlsx"}',
   ])("leaves %s literal", async (text) => {
+    setup();
+    await using screen = await renderText(text);
+    const { container } = screen;
-    const container = await renderText(text);
     expect(container.textContent).toContain(text.replace("\\", ""));
     expect(fileOptions(container)).toEqual([]);
   });
 
   it("preserves Codex file citation examples inside code", async () => {
+    setup();
     const directive = ':codex-file-citation{path="/tmp/project/outputs/report.xlsx"}';
-    const container = await renderText(
+    await using screen = await renderText(
       `Example: \`${directive}\`\n\n\`\`\`text\n${directive}\n\`\`\``,
     );
+    const { container } = screen;
     expect(container.textContent?.match(/:codex-file-citation/g)).toHaveLength(2);
     expect(fileOptions(container)).toEqual([]);
   });
 
   it("does not create a nested link for citations inside link text", async () => {
+    setup();
     const directive = ':codex-file-citation{path="/tmp/project/outputs/report.xlsx"}';
+    await using screen = await renderText(`[See ${directive}](https://example.com)`);
-    const container = await renderText(`[See ${directive}](https://example.com)`);
+    const { container } = screen;
     expect(container.textContent).toContain("codex-file-citation");
     expect(fileOptions(container)).toEqual([]);
   });
 
   it("renders file citations created by over-indented list recovery", async () => {
-    const container = await renderText(
+    setup();
+    await using screen = await renderText(
       '-       Created :codex-file-citation{path="/tmp/project/outputs/report.xlsx"}',
     );
+    const { container } = screen;
     expect(container.querySelector("pre")).toBeNull();
     expect(container.querySelector("li")?.textContent).toContain("Created ");
     expect(fileOptions(container)).toEqual(["File options for report.xlsx"]);
   });
 
   it("disambiguates Codex citations with the same basename", async () => {
+    setup();
+    await using screen = await renderText(
-    const container = await renderText(
       'Changed :codex-file-citation{path="/tmp/project/src/index.ts"} and :codex-file-citation{path="/tmp/project/test/index.ts"}.',
     );
+    const { container } = screen;
     expect(container.textContent).toContain("index.ts · project/src");
     expect(container.textContent).toContain("index.ts · project/test");
   });
 
   it("preserves rejected citations created by over-indented list recovery", async () => {
+    setup();
+    await using malformed = await renderText(
-    const malformed = await renderText(
       'Leading text before list.\n\n-       Bad :codex-file-citation{purpose="output"}',
     );
+    expect(malformed.container.querySelector("li")?.textContent).toBe(
-    expect(malformed.querySelector("li")?.textContent).toBe(
       'Bad :codex-file-citation{purpose="output"}',
     );
-    const nestedLink = await renderText(
+    await using nestedLink = await renderText(
       'Leading text before list.\n\n-       [Bad :codex-file-citation{path="/tmp/project/report.xlsx"}](https://example.com)',
     );
-    expect(nestedLink.textContent).toContain(
+    expect(nestedLink.container.textContent).toContain(
       'Bad :codex-file-citation{path="/tmp/project/report.xlsx"}',
     );
   });
@@ -896,17 +937,21 @@
 
 describe("ChatMarkdown Codex follow-up suggestions", () => {
   it.each([true, false])("renders the label with parseRawHtml=%s", async (parseRawHtml) => {
+    setup();
+    await using screen = await renderText(
-    const container = await renderText(
       'Done.\n\n- :codex-followup[Prepare print version]{prompt="Prepare the document for printing."}',
       { lineBreaks: !parseRawHtml, parseRawHtml },
     );
+    const { container } = screen;
     expect(container.textContent).toContain("Prepare print version");
     expect(container.textContent).not.toContain("codex-followup");
     expect(container.textContent).not.toContain("Prepare the document for printing.");
   });
 
   it("keeps the label of a malformed follow-up created by over-indented list recovery", async () => {
+    setup();
+    await using screen = await renderText("-       Next :codex-followup[Prepare print version]");
+    const { container } = screen;
-    const container = await renderText("-       Next :codex-followup[Prepare print version]");
     expect(container.querySelector("li")?.textContent).toBe(
       "Next :codex-followup[Prepare print version]",
     );
@@ -920,11 +965,13 @@
   it.each([true, false])(
     "offers the Codex template from its card with parseRawHtml=%s",
     async (parseRawHtml) => {
+      setup();
       const onUseArtifactTemplate = vi.fn();
-      const container = await renderText(ARTIFACT_TEMPLATE_DIRECTIVE, {
+      await using screen = await renderText(ARTIFACT_TEMPLATE_DIRECTIVE, {
         parseRawHtml,
         onUseArtifactTemplate,
       });
+      const { container } = screen;
       expect(container.textContent).not.toContain("::artifact-template");
       expect(container.textContent).toContain("Hello World");
       expect(container.textContent).toContain("Document template");
@@ -943,7 +990,9 @@
   );
 
   it("renders a passive card outside a composer-backed timeline", async () => {
+    setup();
+    await using screen = await renderText(ARTIFACT_TEMPLATE_DIRECTIVE);
-    const container = await renderText(ARTIFACT_TEMPLATE_DIRECTIVE);
+    const { container } = screen;
     expect(container.textContent).toContain("Hello World");
     expect(container.textContent).not.toContain("Use template");
   });
@@ -954,15 +1003,19 @@
     `\\${ARTIFACT_TEMPLATE_DIRECTIVE}`,
     ARTIFACT_TEMPLATE_DIRECTIVE.replace("::artifact-template", "::artifact-template-extra"),
   ])("leaves %s literal", async (text) => {
+    setup();
-    const container = await renderText(text);
+    await using screen = await renderText(text);
+    const { container } = screen;
     expect(container.textContent).toContain("::artifact-template");
     expect(container.textContent).not.toContain("Document template");
   });
 
   it("preserves artifact-template examples inside code", async () => {
-    const container = await renderText(
+    setup();
+    await using screen = await renderText(
       `\`${ARTIFACT_TEMPLATE_DIRECTIVE}\`\n\n\`\`\`text\n${ARTIFACT_TEMPLATE_DIRECTIVE}\n\`\`\``,
     );
+    const { container } = screen;
     expect(container.textContent?.match(/::artifact-template/g)).toHaveLength(2);
     expect(container.textContent).not.toContain("Document template");
   });
@@ -970,9 +1023,11 @@
 
 describe("ChatMarkdown headings", () => {
   it("exposes headings below the host heading without changing their tags", async () => {
+    setup();
-    const container = await renderText("# Top\n\n## Section\n\n###### Fine print", {
+    await using screen = await renderText("# Top\n\n## Section\n\n###### Fine print", {
       headingLevelOffset: 3,
     });
+    const { container } = screen;
     expect(
       [...container.querySelectorAll("h1, h2, h6")].map((heading) => [
         heading.tagName,
@@ -983,15 +1038,17 @@
       ["H2", "5"],
       ["H6", "6"],
     ]);
+    await using flat = await renderText("# Top");
-    const flat = await renderText("# Top");
+    expect(flat.container.querySelector("h1")?.hasAttribute("aria-level")).toBe(false);
-    expect(flat.querySelector("h1")?.hasAttribute("aria-level")).toBe(false);
   });
 
   it("never gives two headings the same id, even when a suffix matches another heading", async () => {
-    const container = await renderText(
+    setup();
+    await using screen = await renderText(
       '## Setup\n\n## Setup\n\n## Setup-1\n\n<h2 id="install-1">Pinned</h2>\n\n## Install\n\n## Install',
       { parseRawHtml: true },
     );
+    const { container } = screen;
     expect([...container.querySelectorAll("h2")].map((heading) => heading.id)).toEqual([
       "user-content-setup",
       "user-content-setup-1",
@@ -1022,43 +1079,50 @@
     ["backslashes", String.raw`[Open](C:\Users\shawn\project\src\main.ts)`],
     ["reference links", "[Open][source]\n\n[source]: C:/Users/shawn/project/src/main.ts"],
   ])("resolves %s to the workspace file", async (_, text) => {
+    setup();
     for (const parseRawHtml of [true, false]) {
+      await using screen = await renderWindows(text, parseRawHtml);
-      const container = await renderWindows(text, parseRawHtml);
+      const { container } = screen;
       expect(fileHrefs(container)).toEqual(["C:/Users/shawn/project/src/main.ts"]);
     }
   });
 
   it("keeps backslashes CommonMark would read as escapes", async () => {
+    setup();
     for (const parseRawHtml of [true, false]) {
-      const container = await renderWindows(
+      await using screen = await renderWindows(
         String.raw`[settings](C:\Users\shawn\.claude\settings.json)`,
         parseRawHtml,
       );
+      const { container } = screen;
       expect(fileHrefs(container)).toEqual(["C:/Users/shawn/.claude/settings.json"]);
     }
   });
 
   it("distinguishes same-named backslash paths but not the same file twice", async () => {
+    setup();
     for (const parseRawHtml of [true, false]) {
+      await using distinct = await renderWindows(
-      const distinct = await renderWindows(
         String.raw`[Source](C:\Users\shawn\project\src\index.ts) and [Test](C:\Users\shawn\project\test\index.ts)`,
         parseRawHtml,
       );
-      expect(distinct.textContent).toContain("index.ts · project/src");
+      expect(distinct.container.textContent).toContain("index.ts · project/src");
+      expect(distinct.container.textContent).toContain("index.ts · project/test");
-      expect(distinct.textContent).toContain("index.ts · project/test");
       const path = String.raw`C:\Users\shawn\project\src\main.ts`;
-      const same = await renderWindows(`[Source](${path}) and \`${path}\``, parseRawHtml);
-      expect(fileHrefs(same)).toHaveLength(2);
-      expect(same.textContent).not.toContain("main.ts ·");
+      await using same = await renderWindows(`[Source](${path}) and \`${path}\``, parseRawHtml);
+      expect(fileHrefs(same.container)).toHaveLength(2);
+      expect(same.container.textContent).not.toContain("main.ts ·");
     }
   });
 
   it("still rejects unsafe schemes", async () => {
+    setup();
     for (const parseRawHtml of [true, false]) {
+      await using screen = await renderWindows(
-      const container = await renderWindows(
         "[unsafe](javascript:alert(1)) and [unknown](d:alert(1))",
         parseRawHtml,
       );
+      const { container } = screen;
       expect(container.innerHTML).not.toContain("javascript:");
       expect(container.innerHTML).not.toContain("d:alert");
       expect(fileHrefs(container)).toEqual([]);
@@ -1070,14 +1134,16 @@
   it.each([true, false])(
     "scrolls a table-of-contents link to its heading without touching the URL (parseRawHtml=%s)",
     async (parseRawHtml) => {
-      const scrollIntoView = vi
+      setup();
+      using scrollIntoView = vi
         .spyOn(HTMLElement.prototype, "scrollIntoView")
         .mockImplementation(() => {});
       const href = window.location.href;
-      const container = await renderText(
+      await using screen = await renderText(
         "- [Operating model](#1-operating-model)\n- [Missing](#nowhere)\n\n## 1. Operating model\n\n## 1. Operating model",
         { parseRawHtml },
       );
+      const { container } = screen;
       const headings = [...container.querySelectorAll("h2")];
       expect(headings.map((heading) => heading.id)).toEqual([
         "user-content-1-operating-model",
@@ -1117,15 +1183,17 @@
   }
   const frame = () =>
     act(() => new Promise<void>((resolve) => requestAnimationFrame(() => resolve())));
+  // Highlights live in a document-wide registry the rendered tree does not own.
+  const clearHighlights = () => ({ [Symbol.dispose]: () => CSS.highlights.clear() });
   const activeHighlights = () =>
     [...(CSS.highlights.get("t3-thread-find-active") ?? [])].map((range) =>
       (range as Range).toString(),
     );
 
   it("opens a disclosure only when find selects a match inside it", async () => {
+    setup();
+    using _highlights = clearHighlights();
-    const container = document.body.appendChild(document.createElement("div"));
-    const root = createRoot(container);
-    roots.push(root);
+    const container = document.createElement("div");
     const markdown = (
       <ChatMarkdown
         cwd={undefined}
@@ -1136,20 +1204,19 @@
         ].join("\n\n")}
       />
     );
+    const probe = (query: string, activeOccurrence: number) => (
+      <FindProbe container={container} query={query} activeOccurrence={activeOccurrence}>
+        {markdown}
+      </FindProbe>
+    );
+    await using screen = await render(probe("", 0), { container });
     const show = (query: string, activeOccurrence: number) =>
+      screen.rerender(probe(query, activeOccurrence));
-      act(() =>
-        root.render(
-          <FindProbe container={container} query={query} activeOccurrence={activeOccurrence}>
-            {markdown}
-          </FindProbe>,
-        ),
-      );
     const openStates = () =>
       [...container.querySelectorAll<HTMLElement>("[data-markdown-details-open]")].map(
         (node) => node.dataset.markdownDetailsOpen,
       );
 
-    await show("", 0);
     expect(container.textContent).not.toContain("nothing here");
     await show("needle", 0);
     await frame();
@@ -1166,24 +1233,21 @@
   });
 
   it("keeps Mermaid diagrams rendered until find selects a match in their source", async () => {
+    setup();
+    using _highlights = clearHighlights();
+    const container = document.createElement("div");
-    const container = document.body.appendChild(document.createElement("div"));
-    const root = createRoot(container);
-    roots.push(root);
     const diagram = "```mermaid\ngraph TD; Alpha-->Beta\n```";
+    const probe = (query: string) => (
+      <FindProbe container={container} query={query}>
+        <ChatMarkdown cwd={undefined} text={`Needle first.\n\n${diagram}\n\n${diagram}`} />
+      </FindProbe>
-    const show = (query: string) =>
+    );
-      act(() =>
-        root.render(
-          <FindProbe container={container} query={query}>
-            <ChatMarkdown cwd={undefined} text={`Needle first.\n\n${diagram}\n\n${diagram}`} />
-          </FindProbe>,
-        ),
-      );
     const diagrams = () => container.querySelectorAll('svg[aria-label="Diagram"]').length;
 
-    await show("Needle");
+    await using screen = await render(probe("Needle"), { container });
     await frame();
     expect(diagrams()).toBe(2);
-    await show("Alpha");
+    await screen.rerender(probe("Alpha"));
     await frame();
     await frame();
     expect(diagrams()).toBe(1);
@@ -1224,23 +1288,22 @@
   ])(
     "highlights the indexed occurrences of $query in $text",
     async ({ text, query, count, lineBreaks, user, withTemplate }) => {
+      setup();
       const skills = [{ name: "test-t3-app", displayName: "T3 App Testing" }];
-      const container = document.body.appendChild(document.createElement("div"));
-      const root = createRoot(container);
-      roots.push(root);
+      using _highlights = clearHighlights();
+      const container = document.createElement("div");
+      await using _screen = await render(
+        <FindProbe container={container} query={query}>
+          <ChatMarkdown
+            text={text}
+            cwd={undefined}
+            skills={skills}
+            lineBreaks={lineBreaks ?? false}
+            parseRawHtml={!user}
+            onUseArtifactTemplate={withTemplate ? () => undefined : undefined}
+          />
+        </FindProbe>,
+        { container },
-      await act(() =>
-        root.render(
-          <FindProbe container={container} query={query}>
-            <ChatMarkdown
-              text={text}
-              cwd={undefined}
-              skills={skills}
-              lineBreaks={lineBreaks ?? false}
-              parseRawHtml={!user}
-              onUseArtifactTemplate={withTemplate ? () => undefined : undefined}
-            />
-          </FindProbe>,
-        ),
       );
       await frame();
       // Lazy blocks such as Mermaid can swap their source text after the first frame,
@@ -1290,17 +1353,19 @@
     relative: "reports/report.csv",
   },
 ])("copies $name and shows the copied text", async ({ platform, cwd, href, full, relative }) => {
+  setup();
+  using _platform = vi.spyOn(navigator, "platform", "get").mockReturnValue(platform);
-  vi.spyOn(navigator, "platform", "get").mockReturnValue(platform);
+  using _clipboardItem = stubGlobal("ClipboardItem", undefined);
+  using writeText = vi.spyOn(navigator.clipboard, "writeText").mockResolvedValue(undefined);
+  using toast = vi.spyOn(toastManager, "add");
-  vi.stubGlobal("ClipboardItem", undefined);
-  const writeText = vi.spyOn(navigator.clipboard, "writeText").mockResolvedValue(undefined);
-  const toast = vi.spyOn(toastManager, "add");
   const api = readLocalApi();
   if (!api) throw new Error("Local API unavailable");
-  const choose = vi.spyOn(api.contextMenu, "show");
-  const container = await renderText(`[File](<${href}>)`, {
+  using choose = vi.spyOn(api.contextMenu, "show");
+  await using screen = await renderText(`[File](<${href}>)`, {
     cwd,
     environmentId: EnvironmentId.make("env-file-copy"),
   });
+  const { container } = screen;
   const link = container.querySelector(".chat-markdown-file-link");
   if (!link) throw new Error("File link unavailable");
   for (const [action, expected] of [
@@ -1324,21 +1389,24 @@
     options: Partial<Parameters<typeof ChatMarkdown>[0]> = {},
   ) => {
     settingsOverrides.mathRenderingEnabled = true;
+    await using stack = new AsyncDisposableStack();
+    const screen = stack.use(
-    const { container: host } = await render(
-      <ChatMarkdown cwd="/tmp/project" text={text} {...options} />,
+      await render(<ChatMarkdown cwd="/tmp/project" text={text} {...options} />),
     );
+    screen.container.setAttribute("data-assistant-citation-source", "assistant");
-    host.setAttribute("data-assistant-citation-source", "assistant");
     await act(async () => {
       await import("./chat/MathTypeset");
     });
+    stack.move();
-    return { host };
+    return screen;
   };
 
   it.each(["$x$", "$$x$$", "```math\nx\n```", "$\\notACommand{x}$"])(
     "keeps keyboard focus on the parent control in compact previews of %s",
     async (formula) => {
+      setup();
       settingsOverrides.mathRenderingEnabled = true;
-      const { container } = await render(
+      await using screen = await render(
         <>
           <button type="button">
             Preview <ChatMarkdown inline text={formula} cwd={undefined} />
@@ -1346,6 +1414,7 @@
           <button type="button">After preview</button>
         </>,
       );
+      const { container } = screen;
       await act(async () => {
         await import("./chat/MathTypeset");
       });
@@ -1360,13 +1429,15 @@
   );
 
   it("keeps message formulas reachable by keyboard", async () => {
+    setup();
     settingsOverrides.mathRenderingEnabled = true;
-    const { container } = await render(
+    await using screen = await render(
       <>
         <button type="button">Before equation</button>
         <ChatMarkdown text="$x$" cwd={undefined} />
       </>,
     );
+    const { container } = screen;
     container.querySelector("button")!.focus();
     await act(() => userEvent.tab());
     expect(document.activeElement).toBe(
@@ -1375,11 +1446,12 @@
   });
 
   it("renders table formulas and exports their TeX instead of duplicate glyph text", async () => {
+    setup();
+    await using rendered = await renderMath(
-    const rendered = await renderMath(
       "| Name | Formula |\n| --- | --- |\n| Norm | $|x|$ and \\(\\|y\\|\\) |\n| Price | $5 and $10 |",
     );
 
+    const table = rendered.container.querySelector("table")!;
-    const table = rendered.host.querySelector("table")!;
     expect([...table.rows].map((row) => row.cells.length)).toEqual([2, 2, 2]);
     expect(table.querySelectorAll(".katex")).toHaveLength(2);
     const norm = table.querySelectorAll(".katex")[1]!;
@@ -1402,34 +1474,36 @@
     expect(payload.text).toBe("\\(|x|\\)");
     expect(payload.html).toContain("\\(|x|\\)");
     expect(payload.html).not.toContain("katex");
-    expect(captureAssistantTextSelection(rendered.host, selection)?.selector.text).toBe(
+    expect(captureAssistantTextSelection(rendered.container, selection)?.selector.text).toBe(
       "\\(|x|\\)",
     );
   });
 
   it("renders explicit math fences and keeps unfinished streamed fences readable", async () => {
+    setup();
+    await using rendered = await renderMath("```math\nx_i^2\n```\n\n`$literal$`\n\n$unfinished");
-    const rendered = await renderMath("```math\nx_i^2\n```\n\n`$literal$`\n\n$unfinished");
 
+    expect(rendered.container.querySelectorAll(".katex")).toHaveLength(1);
+    expect(rendered.container.textContent).toContain("$literal$");
-    expect(rendered.host.querySelectorAll(".katex")).toHaveLength(1);
-    expect(rendered.host.textContent).toContain("$literal$");
-    expect(rendered.host.textContent).toContain("$unfinished");
+    expect(rendered.container.textContent).toContain("$unfinished");
 
-    const streamed = await renderMath("```math\nx_i^2", { isStreaming: true });
+    await using streamed = await renderMath("```math\nx_i^2", { isStreaming: true });
 
-    expect(streamed.host.querySelector(".katex")).toBeNull();
-    expect(streamed.host.textContent).toContain("x_i^2");
+    expect(streamed.container.querySelector(".katex")).toBeNull();
+    expect(streamed.container.textContent).toContain("x_i^2");
   });
 
   it.each(["$$\nx^2\n", "\\[\nx^2\n"])(
     "keeps unfinished display %s and following prose readable while streaming",
     async (source) => {
+      setup();
       for (const text of [source, `${source}\nFollowing prose.`]) {
-        const { host } = await renderMath(text, { isStreaming: true });
-        expect(host.querySelector(".katex")).toBeNull();
-        expect(host.querySelector("[data-markdown-math]")).toBeNull();
+        await using math = await renderMath(text, { isStreaming: true });
+        expect(math.container.querySelector(".katex")).toBeNull();
+        expect(math.container.querySelector("[data-markdown-math]")).toBeNull();
+        expect(math.container.textContent).toContain("x^2");
-        expect(host.textContent).toContain("x^2");
         if (text.includes("Following prose."))
+          expect(math.container.textContent).toContain("Following prose.");
-          expect(host.textContent).toContain("Following prose.");
       }
     },
   );
@@ -1437,46 +1511,51 @@
   it.each(["$$x^2$$", "$$\nx^2\n$$", "\\[\nx^2\n\\]", "```math\nx^2\n```"])(
     "renders completed %s while the response is streaming",
     async (source) => {
+      setup();
+      await using math = await renderMath(source, { isStreaming: true });
+      expect(math.container.querySelectorAll(".katex")).toHaveLength(1);
+      expect(math.container.querySelector('[data-markdown-math="display"]')).not.toBeNull();
-      const { host } = await renderMath(source, { isStreaming: true });
-      expect(host.querySelectorAll(".katex")).toHaveLength(1);
-      expect(host.querySelector('[data-markdown-math="display"]')).not.toBeNull();
     },
   );
 
   it("shows unsupported TeX as source and keeps active TeX commands inert", async () => {
-    const rendered = await renderMath(
+    setup();
+    await using rendered = await renderMath(
       "\\(\\notACommand{x}\\) and \\(\\href{https://example.com}{x}\\)",
     );
 
+    expect(rendered.container.textContent).toContain("\\(\\notACommand{x}\\)");
-    expect(rendered.host.textContent).toContain("\\(\\notACommand{x}\\)");
+    expect(rendered.container.querySelector('a[href="https://example.com"]')).toBeNull();
-    expect(rendered.host.querySelector('a[href="https://example.com"]')).toBeNull();
   });
 
   it("typesets only when enabled, keeping prices and task offsets intact", async () => {
+    setup();
     const text = "Area $\\pi r^2$ costs $5 and $10.\n\n\\[\nE = mc^2\n\\]\n\n- [ ] check \\(x\\)";
-    const off = await renderText(text);
+    await using off = await renderText(text);
+    expect(off.container.querySelector("[data-markdown-math]")).toBeNull();
-    expect(off.querySelector("[data-markdown-math]")).toBeNull();
-    const html = await renderText('<code class="language-math math-inline">x</code>');
-    expect(html.querySelector("[data-markdown-math]")).toBeNull();
+    await using html = await renderText('<code class="language-math math-inline">x</code>');
+    expect(html.container.querySelector("[data-markdown-math]")).toBeNull();
     for (const parseRawHtml of [true, false]) {
       let editedText = text;
-      const { host } = await renderMath(text, {
+      await using math = await renderMath(text, {
         parseRawHtml,
         onTaskListChange: ({ markerOffset, checked }) => {
           editedText = setMarkdownTaskChecked(text, markerOffset, checked);
         },
       });
       expect(
-        [...host.querySelectorAll("[data-markdown-math]")].map((node) =>
+        [...math.container.querySelectorAll("[data-markdown-math]")].map((node) =>
           node.getAttribute("data-markdown-copy"),
         ),
       ).toEqual(["\\(\\pi r^2\\)", "\\[\nE = mc^2\n\\]\n\n", "\\(x\\)"]);
-      await act(() => host.querySelector<HTMLInputElement>('input[type="checkbox"]')!.click());
+      await act(() =>
+        math.container.querySelector<HTMLInputElement>('input[type="checkbox"]')!.click(),
+      );
       expect(editedText).toBe(text.replace("- [ ]", "- [x]"));
     }
+    await using math = await renderMath("-       intro\n\n        $$\n        x^2\n        $$");
-    const { host } = await renderMath("-       intro\n\n        $$\n        x^2\n        $$");
     expect(
+      [...math.container.querySelectorAll("[data-markdown-math]")].map((node) =>
-      [...host.querySelectorAll("[data-markdown-math]")].map((node) =>
         node.getAttribute("data-markdown-copy"),
       ),
     ).toEqual(["\\[\nx^2\n\\]\n\n"]);
````

## apps/web/src/components/ChatMarkdown.permissions.browser.test.tsx

Status: modified; +50; −51

```diff
@@ -8,10 +8,9 @@
   type ThreadLinkedPullRequest,
 } from "@t3tools/contracts";
 import { AsyncResult } from "effect/reactivity";
-import { act } from "react";
-import { createRoot, type Root } from "react-dom/client";
-import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
+import { expect, it, vi } from "vite-plus/test";
 
+import { act, render } from "../../test/render";
 import { useRightPanelStore } from "../rightPanelStore";
 
 const state = vi.hoisted(() => ({
@@ -130,10 +129,9 @@
   shellRevealInFileManagerKind: "xdg-open",
   environment: { platform: { os: "linux" }, capabilities: { threadPullRequestLinking: true } },
 };
-const roots: Root[] = [];
-let container: HTMLDivElement;
 
+function setup() {
+  using stack = new DisposableStack();
-beforeEach(() => {
   state.allowed = true;
   state.listeners.clear();
   state.linkedPullRequest = null;
@@ -144,40 +142,35 @@
   state.openFile.mockReset();
   state.openExternal.mockReset().mockResolvedValue(undefined);
   state.copy.mockReset().mockResolvedValue(undefined);
+  const { openFile } = useRightPanelStore.getState();
   useRightPanelStore.setState({ openFile: state.openFile });
-  vi.spyOn(navigator.clipboard, "writeText").mockImplementation(state.copy);
-  vi.spyOn(navigator.clipboard, "write").mockImplementation(async (items) => {
+  stack.defer(() => useRightPanelStore.setState({ openFile }));
+  stack.use(vi.spyOn(navigator.clipboard, "writeText").mockImplementation(state.copy));
+  stack.use(
+    vi.spyOn(navigator.clipboard, "write").mockImplementation(async (items) => {
+      const item = items[0]!;
+      await state.copy(await (await item.getType("text/plain")).text());
+    }),
-    const item = items[0]!;
-    await state.copy(await (await item.getType("text/plain")).text());
-  });
-  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
-});
-
-afterEach(async () => {
-  for (const root of roots.splice(0)) await act(async () => root.unmount());
-  document.body.replaceChildren();
-  vi.restoreAllMocks();
-  vi.unstubAllGlobals();
-});
-
-async function renderMarkdown(text: string) {
-  container = document.body.appendChild(document.createElement("div"));
-  const root = createRoot(container);
-  roots.push(root);
-  await act(async () =>
-    root.render(<ChatMarkdown cwd="/work" threadRef={threadRef} text={text} />),
   );
+  return stack.move();
 }
 
+async function renderMarkdown(text: string) {
+  const screen = await render(<ChatMarkdown cwd="/work" threadRef={threadRef} text={text} />);
+  const anchor = () => screen.container.querySelector("a")!;
+  return Object.assign(screen, {
+    anchor,
+    openContextMenu: () =>
+      act(async () => {
+        anchor().dispatchEvent(
+          new MouseEvent("contextmenu", {
+            bubbles: true,
+            cancelable: true,
+            clientX: 10,
+            clientY: 10,
+          }),
+        );
-function anchor() {
+      }),
-  return container.querySelector("a")!;
-}
-
-async function openContextMenu() {
-  await act(async () => {
-    anchor().dispatchEvent(
-      new MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: 10, clientY: 10 }),
-    );
   });
 }
 
@@ -193,20 +186,22 @@
 }
 
 it("removes host actions after revocation while keeping file preview and copying", async () => {
-  await renderMarkdown("[Readme](docs/readme.md)");
-  await openContextMenu();
+  using _setup = setup();
+  await using screen = await renderMarkdown("[Readme](docs/readme.md)");
+  await screen.openContextMenu();
   expect(offeredActions()).toEqual(["open", "reveal", "copy-relative", "copy-full"]);
   await setAllowed(false);
   state.choose.mockResolvedValue("copy-full");
-  await openContextMenu();
+  await screen.openContextMenu();
   expect(offeredActions()).toEqual(["copy-relative", "copy-full"]);
   await vi.waitFor(() => expect(state.copy).toHaveBeenCalledWith("/work/docs/readme.md"));
-  await act(async () => anchor().click());
+  await act(async () => screen.anchor().click());
   expect(state.openFile).toHaveBeenCalledWith(threadRef, "docs/readme.md", undefined);
   expect(state.openEditor).not.toHaveBeenCalled();
 });
 
 it("starts copying before the nested-file lookup finishes", async () => {
+  using _setup = setup();
   let finishLookup: () => void = () => {
     throw new Error("Lookup not started");
   };
@@ -218,8 +213,8 @@
       }),
   );
   state.choose.mockResolvedValue("copy-full");
-  await renderMarkdown("`readme.md:10:2`");
-  await openContextMenu();
+  await using screen = await renderMarkdown("`readme.md:10:2`");
+  await screen.openContextMenu();
   expect(navigator.clipboard.write).toHaveBeenCalledOnce();
   expect(state.copy).not.toHaveBeenCalled();
   await act(async () => finishLookup());
@@ -227,6 +222,7 @@
 });
 
 it("does not launch an editor after revocation during a native context menu", async () => {
+  using _setup = setup();
   let choose: (action: string) => void = () => {
     throw new Error("Menu not opened");
   };
@@ -236,8 +232,8 @@
         choose = resolve;
       }),
   );
+  await using screen = await renderMarkdown("[Readme](docs/readme.md)");
+  await screen.openContextMenu();
-  await renderMarkdown("[Readme](docs/readme.md)");
-  await openContextMenu();
   expect(offeredActions()).toContain("open");
   await act(async () => {
     state.allowed = false;
@@ -247,6 +243,7 @@
 });
 
 it("does not reveal a file after revocation during its workspace lookup", async () => {
+  using _setup = setup();
   let finishLookup: () => void = () => {
     throw new Error("Lookup not started");
   };
@@ -257,8 +254,8 @@
       }),
   );
   state.choose.mockResolvedValue("reveal");
-  await renderMarkdown("[Readme](readme.md)");
+  await using screen = await renderMarkdown("[Readme](readme.md)");
+  await screen.openContextMenu();
-  await openContextMenu();
   expect(state.search).toHaveBeenCalledOnce();
   await act(async () => {
     state.allowed = false;
@@ -270,11 +267,12 @@
 it.each([false, true])(
   "keeps PR links usable without offering metadata edits (linked=%s)",
   async (linked) => {
+    using _setup = setup();
     state.allowed = false;
     state.linkedPullRequest = linked ? linkedPullRequest : null;
     state.choose.mockResolvedValue("open-external");
-    await renderMarkdown(`[Review](${linkedPullRequest.url})`);
-    await openContextMenu();
+    await using screen = await renderMarkdown(`[Review](${linkedPullRequest.url})`);
+    await screen.openContextMenu();
     expect(offeredActions()).toEqual(["open-external", "copy-link"]);
     await vi.waitFor(() => expect(state.openExternal).toHaveBeenCalledWith(linkedPullRequest.url));
     expect(state.updateMetadata).not.toHaveBeenCalled();
@@ -284,6 +282,7 @@
 it.each(["link-to-thread", "unlink-from-thread"])(
   "rechecks %s after a native context menu returns",
   async (action) => {
+    using _setup = setup();
     state.linkedPullRequest = action === "unlink-from-thread" ? linkedPullRequest : null;
     let choose: (action: string) => void = () => {
       throw new Error("Menu not opened");
@@ -294,8 +293,8 @@
           choose = resolve;
         }),
     );
-    await renderMarkdown(`[Review](${linkedPullRequest.url})`);
+    await using screen = await renderMarkdown(`[Review](${linkedPullRequest.url})`);
-    await openContextMenu();
+    await screen.openContextMenu();
     expect(offeredActions()).toContain(action);
     await act(async () => {
       state.allowed = false;
@@ -305,7 +304,7 @@
 
     await setAllowed(true);
     state.choose.mockResolvedValue(action);
+    await screen.openContextMenu();
-    await openContextMenu();
     await vi.waitFor(() =>
       expect(state.updateMetadata).toHaveBeenCalledExactlyOnceWith({
         environmentId: threadRef.environmentId,
```

## apps/web/src/components/CommandPalette.focus.browser.test.tsx

Status: modified; +29; −48

```diff
@@ -1,13 +1,14 @@
+import { useImperativeHandle, useRef, type ReactNode } from "react";
+import { describe, expect, it, vi } from "vite-plus/test";
-import { act, useImperativeHandle, useRef } from "react";
-import { createRoot, type Root } from "react-dom/client";
-import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
+import { act, fakeTimers, render } from "../../test/render";
 import { DEFAULT_RESOLVED_KEYBINDINGS } from "@t3tools/shared/keybindings";
 import { CommandPalette } from "./CommandPalette";
 import { useComposerHandleContext } from "../composerHandleContext";
 import { openCommandPalette } from "../commandPaletteBus";
 import { primaryServerKeybindingsAtom } from "../state/server";
 
+vi.mock("@tanstack/react-router", async (importOriginal) => ({
+  ...(await importOriginal<typeof import("@tanstack/react-router")>()),
-vi.mock("@tanstack/react-router", () => ({
   useNavigate: () => vi.fn(),
   useParams: () => null,
   useLocation: () => "/settings",
@@ -66,26 +67,20 @@
   useAvailableSettingsSearchItems: () => [],
 }));
 
-let root: Root;
+// Navigation hooks are mocked above, and the test router would load on the fake clock too.
+async function renderPalette(children: ReactNode) {
-let container: HTMLDivElement;
-
-beforeEach(() => {
+  await using stack = new AsyncDisposableStack();
+  stack.use(fakeTimers());
+  // Runs after unmount, so timers the palette leaves behind fire on the fake clock.
+  stack.defer(async () => {
+    await act(() => vi.runOnlyPendingTimersAsync());
-  vi.useFakeTimers();
-  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
-  container = document.createElement("div");
-  document.body.append(container);
-  root = createRoot(container);
-});
-
-afterEach(async () => {
-  await act(async () => {
-    root.unmount();
-    await vi.runOnlyPendingTimersAsync();
   });
+  const screen = stack.use(
+    await render(<CommandPalette>{children}</CommandPalette>, { router: false }),
+  );
+  const environment = stack.move();
-  container.remove();
-  vi.unstubAllGlobals();
+  return Object.assign(screen, { [Symbol.asyncDispose]: () => environment.disposeAsync() });
+}
-  vi.useRealTimers();
-});
 
 async function flushFocus() {
   await act(() => vi.advanceTimersByTimeAsync(0));
@@ -120,14 +115,8 @@
 
 describe("command palette focus restoration", () => {
   it("returns to the previously focused Settings field when no composer is mounted", async () => {
-    await act(() =>
-      root.render(
-        <CommandPalette>
+    await using screen = await renderPalette(<input aria-label="Settings field" />);
+    const field = screen.container.querySelector("input")!;
-          <input aria-label="Settings field" />
-        </CommandPalette>,
-      ),
-    );
-    const field = container.querySelector("input")!;
     await act(() => field.focus());
     await act(() => openCommandPalette());
     await flushFocus();
@@ -137,14 +126,8 @@
   });
 
   it("focuses the replacement command input after leaving the file picker", async () => {
-    await act(() =>
-      root.render(
-        <CommandPalette>
-          <input aria-label="Settings field" />
-        </CommandPalette>,
-      ),
-    );
+    await using screen = await renderPalette(<input aria-label="Settings field" />);
+    const field = screen.container.querySelector("input")!;
-    const field = container.querySelector("input")!;
     await act(() => field.focus());
     await act(() =>
       field.dispatchEvent(
@@ -175,20 +158,18 @@
   });
 
   it("focuses the mounted composer at the end instead of the previous field", async () => {
-    await act(() =>
-      root.render(
+    await using screen = await renderPalette(
+      <>
+        <input aria-label="Previous field" />
+        <Composer />
+      </>,
-        <CommandPalette>
-          <input aria-label="Previous field" />
-          <Composer />
-        </CommandPalette>,
-      ),
     );
-    await act(() => container.querySelector("input")!.focus());
+    await act(() => screen.container.querySelector("input")!.focus());
     await act(() => openCommandPalette());
     await flushFocus();
     expect(document.activeElement?.closest("[data-command-palette]")).not.toBeNull();
     await dismissPalette();
-    const composer = container.querySelector("textarea")!;
+    const composer = screen.container.querySelector("textarea")!;
     expect(document.activeElement).toBe(composer);
     expect(composer.selectionStart).toBe(composer.value.length);
   });
```

## apps/web/src/components/ComposerPromptEditorTiptap.citationDraft.browser.test.tsx

Status: modified; +7; −29

```diff
@@ -3,21 +3,13 @@
   serializeAssistantCitation,
   withAssistantCitationComment,
 } from "@t3tools/shared/assistantCitations";
+import { createRef, useState } from "react";
+import { expect, it } from "vite-plus/test";
-import { act, createRef, useState, type ReactNode } from "react";
-import { createRoot, type Root } from "react-dom/client";
-import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
 
 import { page, userEvent } from "vite-plus/test/browser";
 
 import { findAssistantCitationSourceAnchor } from "~/lib/assistantTextSelection";
-
-// No router is mounted; the citation chip only needs navigation to exist.
-vi.mock(import("@tanstack/react-router"), async (original) => ({
-  ...(await original()),
-  useNavigate: () => async () => {},
+import { act, render } from "../../test/render";
-  Link: (({ children }: { children: ReactNode }) => <a>{children}</a>) as never,
-}));
-
 import { ComposerPromptEditor, type ComposerPromptEditorHandle } from "./ComposerPromptEditor";
 
 const citation = {
@@ -33,20 +25,6 @@
 };
 const promptBefore = `See ${serializeAssistantCitation(citation)} `;
 
-let root: Root;
-let container: HTMLDivElement;
-
-beforeEach(() => {
-  container = document.createElement("div");
-  document.body.append(container);
-  root = createRoot(container);
-});
-
-afterEach(async () => {
-  await act(async () => root.unmount());
-  container.remove();
-});
-
 function Composer({
   question,
   editorRef,
@@ -88,9 +66,9 @@
 
 it("keeps an unsaved citation comment when a question takes the composer", async () => {
   const editorRef = createRef<ComposerPromptEditorHandle>();
+  await using screen = await render(<Composer question={false} editorRef={editorRef} />);
+  const drafts = () => screen.container.querySelector("output")?.dataset;
-  const drafts = () => container.querySelector("output")?.dataset;
 
-  await act(async () => root.render(<Composer question={false} editorRef={editorRef} />));
   const sourceAnchor = findAssistantCitationSourceAnchor(document, citation);
   if (!sourceAnchor) throw new Error("Citation source did not resolve");
   await act(async () =>
@@ -107,9 +85,9 @@
   await comment.click();
   await userEvent.keyboard("check this");
 
-  await act(async () => root.render(<Composer question editorRef={editorRef} />));
+  await screen.rerender(<Composer question editorRef={editorRef} />);
   expect(drafts()?.answer).toBe("");
-  await act(async () => root.render(<Composer question={false} editorRef={editorRef} />));
+  await screen.rerender(<Composer question={false} editorRef={editorRef} />);
 
   expect(drafts()?.prompt).toBe(
     `See ${serializeAssistantCitation(withAssistantCitationComment(citation, "check this"))} `,
```

## apps/web/src/components/ComposerPromptEditorTiptap.input.browser.test.tsx

Status: modified; +84; −76

```diff
@@ -10,10 +10,11 @@
 import { userEvent } from "vite-plus/test/browser";
 import type { ServerProviderSkill } from "@t3tools/contracts";
 import type { Editor } from "@tiptap/core";
+import { createElement, createRef, useEffect, useState } from "react";
+import { describe, expect, it, vi } from "vite-plus/test";
-import { act, createElement, createRef, useEffect, useState } from "react";
-import { createRoot, type Root } from "react-dom/client";
-import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
 
+import { act, render } from "../../test/render";
+
 import { detectComposerTrigger } from "~/composer-logic";
 import { serializeEditorDoc } from "~/composer-rich-text-doc";
 import {
@@ -21,23 +22,7 @@
   type ComposerPromptEditorHandle,
   type ComposerPromptEditorProps,
 } from "./ComposerPromptEditor";
-
-let root: Root;
-let container: HTMLDivElement;
 
-beforeEach(() => {
-  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
-  container = document.createElement("div");
-  document.body.append(container);
-  root = createRoot(container);
-});
-
-afterEach(async () => {
-  await act(async () => root.unmount());
-  container.remove();
-  vi.unstubAllGlobals();
-});
-
 const babysit: ServerProviderSkill = {
   name: "babysit",
   path: "/skills/babysit/SKILL.md",
@@ -75,27 +60,39 @@
   );
 }
 
-async function mountEditor(
+function composer(
   value: string,
   richTextEnabled = true,
   skills: ReadonlyArray<ServerProviderSkill> = [],
   onChange?: ComposerPromptEditorProps["onChange"],
 ) {
-  await act(async () =>
-    root.render(
-      <Composer
-        value={value}
-        richTextEnabled={richTextEnabled}
-        skills={skills}
+  return (
-        onChange={onChange}
-      />,
-    ),
+    <Composer value={value} richTextEnabled={richTextEnabled} skills={skills} onChange={onChange} />
   );
+}
+
+function editorIn(container: HTMLElement) {
   const element = container.querySelector<HTMLElement & { editor: Editor }>(".tiptap");
   if (!element) throw new Error("Composer did not mount");
   return element.editor;
 }
 
+async function mountEditor(...props: Parameters<typeof composer>) {
+  await using stack = new AsyncDisposableStack();
+  const screen = stack.use(await render(composer(...props)));
+  const editor = editorIn(screen.container);
+  const environment = stack.move();
+  return Object.assign(screen, {
+    [Symbol.asyncDispose]: () => environment.disposeAsync(),
+    editor,
+    /** Rerenders the same composer with new props, as a parent would. */
+    update: async (...next: Parameters<typeof composer>) => {
+      await screen.rerender(composer(...next));
+      return editorIn(screen.container);
+    },
+  });
+}
+
 async function replaceDomText(editor: Editor, text: string, isComposing = false) {
   await act(async () => {
     const node = editor.view.dom.querySelector("p")?.firstChild;
@@ -140,7 +137,8 @@
   { value: "clean _build and node", code: [] },
   { value: "clean `_build` and `node", code: ["_build"] },
 ])("keeps a typed underscore literal after $value", async ({ value, code }) => {
+  await using screen = await mountEditor(value);
+  const { editor } = screen;
-  const editor = await mountEditor(value);
   await act(async () => {
     editor.commands.focus("end");
     const { from, to } = editor.state.selection;
@@ -152,7 +150,8 @@
 });
 
 it("still italicizes text when typing a closing asterisk", async () => {
-  const editor = await mountEditor("clean *build");
+  await using screen = await mountEditor("clean *build");
+  const { editor } = screen;
   await act(async () => {
     editor.commands.focus("end");
     const { from, to } = editor.state.selection;
@@ -166,7 +165,8 @@
   "composes an umlaut without wrapping the preedit text (rich text: %s)",
   async (richTextEnabled) => {
     // The middle dot stands in for the provisional text shown by the Compose key.
+    await using screen = await mountEditor("·", richTextEnabled);
+    const { editor } = screen;
-    const editor = await mountEditor("·", richTextEnabled);
     await act(async () => {
       editor.view.focus();
       editor.view.dom.dispatchEvent(new CompositionEvent("compositionstart"));
@@ -182,7 +182,8 @@
 );
 
 it("commits a composed quote without surrounding the preedit text", async () => {
-  const editor = await mountEditor("·");
+  await using screen = await mountEditor("·");
+  const { editor } = screen;
   await act(async () => {
     editor.view.focus();
     editor.view.dom.dispatchEvent(new CompositionEvent("compositionstart"));
@@ -193,7 +194,8 @@
 });
 
 it("lets composition replace selected text without surrounding it", async () => {
+  await using screen = await mountEditor("word");
-  const editor = await mountEditor("word");
+  const { editor } = screen;
   await act(async () => {
     editor.commands.setTextSelection({ from: 1, to: 5 });
     editor.view.dom.dispatchEvent(new CompositionEvent("compositionstart"));
@@ -203,7 +205,8 @@
 });
 
 it("replaces text outside the selected range without surrounding it", async () => {
+  await using screen = await mountEditor("word");
+  const { editor } = screen;
-  const editor = await mountEditor("word");
   await act(async () => {
     editor.commands.setTextSelection({ from: 1, to: 5 });
     typeText(editor, 2, 3, '"');
@@ -214,7 +217,8 @@
 it.each([false, true])(
   "still surrounds selected text with quotes (rich text: %s)",
   async (richTextEnabled) => {
-    const editor = await mountEditor("word", richTextEnabled);
+    await using screen = await mountEditor("word", richTextEnabled);
+    const { editor } = screen;
     await act(async () => {
       editor.commands.setTextSelection({ from: 1, to: 5 });
       editor.view.focus();
@@ -226,7 +230,8 @@
 
 it("keeps unknown dollar names editable when restoring and pasting a draft", async () => {
   const value = "echo $HOME $PATH $anything $notaskill $20 $20k $babysit ";
+  await using screen = await mountEditor(value, true, [babysit]);
+  const { editor } = screen;
-  const editor = await mountEditor(value, true, [babysit]);
   expect(editor.state.doc.textContent).toBe(value.replace("$babysit", ""));
   expect(serializeEditorDoc(editor.state.doc).value).toBe(value);
   await act(async () => {
@@ -246,7 +251,13 @@
 
 it("keeps skill queries active before whitespace and line breaks, suppressing only real chips", async () => {
   const onChange = vi.fn<ComposerPromptEditorProps["onChange"]>();
-  const editor = await mountEditor("use  now\nfirst \n$babysit tail", true, [babysit], onChange);
+  await using screen = await mountEditor(
+    "use  now\nfirst \n$babysit tail",
+    true,
+    [babysit],
+    onChange,
+  );
+  const { editor } = screen;
   const trigger = () => {
     const [value, , expandedCursor, suppressTrigger] = onChange.mock.lastCall!;
     return suppressTrigger ? null : detectComposerTrigger(value, expandedCursor);
@@ -284,16 +295,17 @@
 
 it("keeps the caret inside unknown dollar text across rich and plain remounts", async () => {
   const value = "echo $HOME now";
+  await using screen = await mountEditor(value);
-  let editor = await mountEditor(value);
+  let { editor } = screen;
   await act(async () => {
     editor.commands.setTextSelection("echo $HO".length + 1);
   });
-  editor = await mountEditor(value, false);
+  editor = await screen.update(value, false);
   await act(async () => {
     editor.commands.insertContent("X");
   });
   expect(serializeEditorDoc(editor.state.doc).value).toBe("echo $HOXME now");
+  editor = await screen.update(value, true);
-  editor = await mountEditor(value, true);
   await act(async () => {
     editor.commands.insertContent("Y");
   });
@@ -304,18 +316,19 @@
   const value = "run **use $babysit now** then $HOME";
   const disabled = { ...babysit, enabled: false };
   const agentOnly = { ...babysit, userInvocable: false };
-  const editor = await mountEditor(value, true, [disabled]);
+  await using screen = await mountEditor(value, true, [disabled]);
+  const { editor } = screen;
   expect.soft(editor.state.doc.textContent).toBe("run use $babysit now then $HOME");
+  await screen.update(value, true, [agentOnly]);
-  await mountEditor(value, true, [agentOnly]);
   expect.soft(editor.state.doc.textContent).toBe("run use $babysit now then $HOME");
   await act(async () => {
     editor.commands.setTextSelection("run use $baby".length + 1);
   });
-  await mountEditor(value, true, [disabled, babysit]);
+  await screen.update(value, true, [disabled, babysit]);
   expect(editor.state.doc.textContent).toBe("run use  now then $HOME");
   const chip = serializeEditorDoc(editor.state.doc).runs.find((run) => run.kind === "token")!;
   expect(editor.state.selection.from).toBe(chip.pmPos + 1);
+  await screen.update(value, true, [disabled]);
-  await mountEditor(value, true, [disabled]);
   expect.soft(editor.state.doc.textContent).toBe("run use $babysit now then $HOME");
   expect(serializeEditorDoc(editor.state.doc).value).toBe(value);
   await act(async () => {
@@ -328,25 +341,25 @@
   expect(serializeEditorDoc(editor.state.doc).value).toBe(value);
   expect(editor.state.doc.textContent).toBe("run use $babysit now then $HOME");
 
-  await mountEditor(value, true, [babysit]);
+  await screen.update(value, true, [babysit]);
   await act(async () => {
     editor.commands.deleteRange({ from: chip.pmPos, to: chip.pmPos + 1 });
   });
-  await mountEditor(value, true, [agentOnly]);
+  await screen.update(value, true, [agentOnly]);
   await act(async () => {
     editor.commands.undo();
   });
   expect(editor.state.doc.textContent).toBe("run use $babysit now then $HOME");
   expect(serializeEditorDoc(editor.state.doc).value).toBe(value);
 
+  await screen.update(value, true, [{ ...babysit, description: "Previous provider description" }]);
-  await mountEditor(value, true, [{ ...babysit, description: "Previous provider description" }]);
   await act(async () => {
-    container
+    screen.container
       .querySelector<HTMLButtonElement>('[aria-label="Skill Babysit. Show details"]')!
       .click();
   });
   expect(document.body.textContent).toContain("Previous provider description");
+  await screen.update(value, true, [
-  await mountEditor(value, true, [
     { ...disabled, displayName: "Disabled Babysit" },
     {
       ...babysit,
@@ -354,11 +367,11 @@
       shortDescription: "  Current short description  ",
     },
   ]);
-  expect(container.textContent).toContain("Current Babysit");
+  expect(screen.container.textContent).toContain("Current Babysit");
   expect(document.body.textContent).toContain("Current short description");
   expect(document.body.textContent).not.toContain("Previous provider description");
+  await screen.update(value, true, [{ ...babysit, displayName: "Current Babysit" }]);
+  expect(screen.container.textContent).toContain("Current Babysit");
-  await mountEditor(value, true, [{ ...babysit, displayName: "Current Babysit" }]);
-  expect(container.textContent).toContain("Current Babysit");
   expect(document.body.textContent).not.toContain("Current short description");
   expect(document.body.textContent).toContain("No description is available for this skill.");
   expect(document.body.textContent).not.toContain("Previous provider description");
@@ -431,18 +444,14 @@
       return createElement(ComposerPromptEditor, editorProps);
     }
 
-    const render = async (targetKey: string) => {
-      await act(async () => root.render(createElement(Composer, { targetKey })));
-    };
-    const editor = () => {
-      const element = container.querySelector<HTMLElement & { editor: Editor }>(".tiptap");
-      if (!element) throw new Error("Composer did not mount");
-      return element.editor;
+    await using stack = new AsyncDisposableStack();
-    };
-    await render("a");
-    return {
+    const screen = stack.use(await render(createElement(Composer, { targetKey: "a" })));
+    const editor = () => editorIn(screen.container);
+    const environment = stack.move();
+    return Object.assign(screen, {
+      [Symbol.asyncDispose]: () => environment.disposeAsync(),
       initialValue,
-      render,
+      show: (targetKey: string) => screen.rerender(createElement(Composer, { targetKey })),
       editor,
       draft: () => current,
       retained: () => retained,
@@ -459,11 +468,11 @@
           editor().commands.undo();
         });
       },
+    });
-    };
   }
 
   it("preserves typing focus across draft switches without taking focus from another control", async () => {
-    const composer = await mountComposer("Draft B");
+    await using composer = await mountComposer("Draft B");
     const previousEditor = composer.editor();
     await act(async () => {
       previousEditor.commands.setTextSelection(previousEditor.state.doc.content.size - 1);
@@ -471,7 +480,7 @@
     });
     expect(document.activeElement).toBe(previousEditor.view.dom);
 
+    await composer.show("b");
-    await composer.render("b");
     const nextEditor = composer.editor();
     expect(nextEditor).not.toBe(previousEditor);
     expect(document.activeElement).toBe(nextEditor.view.dom);
@@ -479,48 +488,47 @@
     expect(composer.draft().value).toBe("xDraft B");
 
     const button = document.createElement("button");
-    container.append(button);
+    composer.container.append(button);
     button.focus();
-    await composer.render("a");
+    await composer.show("a");
     expect(document.activeElement).toBe(button);
     expect(composer.draft().value).toBe(composer.initialValue);
   });
 
   it("restores a deleted chip and its file bytes on same-draft undo, then releases them on unmount", async () => {
+    await using composer = await mountComposer();
-    const composer = await mountComposer();
     await composer.deleteDraft();
     expect(composer.draft()).toEqual({ value: "", files: [] });
     const retained = composer.retained();
     expect(retained.files.get(fileContextReference(file).contextId)?.file).toBe(file.file);
 
-    await composer.render("a");
+    await composer.show("a");
     await composer.undo();
     expect(serializeEditorDoc(composer.editor().state.doc).value).toBe(composer.initialValue);
     expect(composer.draft()).toEqual({ value: composer.initialValue, files: [file] });
     expect(composer.draft().files[0]?.file).toBe(file.file);
 
     await composer.deleteDraft();
+    await composer.unmount();
-    await act(async () => root.unmount());
     expect(retained.files.size).toBe(0);
-    root = createRoot(container);
   });
 
   it.each(["", "Draft B"])(
     "releases deleted payloads and prevents undo into the previous draft when switching to %j",
     async (destinationValue) => {
-      const composer = await mountComposer(destinationValue);
+      await using composer = await mountComposer(destinationValue);
       await composer.deleteDraft();
       expect(composer.draft()).toEqual({ value: "", files: [] });
       const retained = composer.retained();
       expect(retained.files.get(fileContextReference(file).contextId)?.file).toBe(file.file);
 
+      await composer.show("b");
-      await composer.render("b");
       expect(retained.files.size).toBe(0);
       await composer.undo();
       expect(serializeEditorDoc(composer.editor().state.doc).value).toBe(destinationValue);
       expect(composer.draft()).toEqual({ value: destinationValue, files: [] });
 
-      await composer.render("a");
+      await composer.show("a");
       await composer.undo();
       expect(serializeEditorDoc(composer.editor().state.doc).value).toBe("");
       expect(composer.draft()).toEqual({ value: "", files: [] });
```

## apps/web/src/components/ComposerPromptEditorTiptap.navigation.browser.test.tsx

Status: modified; +53; −53

```diff
@@ -1,52 +1,37 @@
 import type { Editor } from "@tiptap/core";
 import { TextSelection } from "@tiptap/pm/state";
-import { act, createRef } from "react";
-import { createRoot, type Root } from "react-dom/client";
+import { createRef } from "react";
+import { describe, expect, it, vi } from "vite-plus/test";
-import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
 
+import { act, render } from "../../test/render";
 import { serializeEditorDoc } from "~/composer-rich-text-doc";
 import { ComposerPromptEditor, type ComposerPromptEditorHandle } from "./ComposerPromptEditor";
 
 const draft =
   "> **Assistant and reasoning text still save intermediate full snapshots**, using the existing 50 ms batching. Compaction still preserves their `turn-item.updated` events. [Code](/Users/yashsingh/p/projects/t3code/apps/server/src/orchestration-v2/ProjectionMaintenance.ts:235)";
 
+async function mountEditor(value: string, richTextEnabled = true, container?: HTMLElement) {
+  const screen = await render(
+    <ComposerPromptEditor
+      editorRef={createRef<ComposerPromptEditorHandle>()}
+      value={value}
+      cursor={0}
+      richTextEnabled={richTextEnabled}
+      contextRecords={new Map()}
+      skills={[]}
+      disabled={false}
+      placeholder=""
+      onChange={() => {}}
+      onPaste={() => {}}
+    />,
-let root: Root;
-let container: HTMLDivElement;
-
-beforeEach(() => {
-  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
-  container = document.createElement("div");
-  document.body.append(container);
-  root = createRoot(container);
-});
-
-afterEach(async () => {
-  await act(async () => root.unmount());
-  container.remove();
-  vi.restoreAllMocks();
-  vi.unstubAllGlobals();
-});
-
-async function mountEditor(value: string, richTextEnabled = true) {
-  await act(async () =>
-    root.render(
-      <ComposerPromptEditor
-        editorRef={createRef<ComposerPromptEditorHandle>()}
-        value={value}
-        cursor={0}
-        richTextEnabled={richTextEnabled}
-        contextRecords={new Map()}
-        skills={[]}
-        disabled={false}
-        placeholder=""
-        onChange={() => {}}
-        onPaste={() => {}}
-      />,
-    ),
+    container ? { container } : {},
   );
+  const element = screen.container.querySelector<HTMLElement & { editor: Editor }>(".tiptap");
+  if (!element) {
+    await screen[Symbol.asyncDispose]();
-  const element = container.querySelector<HTMLElement & { editor: Editor }>(".tiptap");
-  if (!element) throw new Error("Composer did not mount");
-  return element.editor;
+    throw new Error("Composer did not mount");
+  }
+  return Object.assign(screen, { editor: element.editor });
 }
 
 function press(editor: Editor, key: string, options: KeyboardEventInit = {}) {
@@ -57,7 +42,8 @@
 }
 
 it("keeps Up before the opening bold marker in a quote, including while typing", async () => {
+  await using screen = await mountEditor(draft);
-  const editor = await mountEditor(draft);
+  const { editor } = screen;
   const position = editor.state.selection.from;
   await act(async () => {
     editor.view.focus();
@@ -77,7 +63,8 @@
 });
 
 it("keeps Down after a closing marker, including while typing", async () => {
+  await using screen = await mountEditor("> **bold**");
-  const editor = await mountEditor("> **bold**");
+  const { editor } = screen;
   await act(async () => {
     editor.view.dispatch(editor.state.tr.setSelection(TextSelection.atEnd(editor.state.doc)));
     expect(press(editor, "ArrowDown")).toBe(true);
@@ -88,7 +75,8 @@
 });
 
 it("leaves vertical navigation between paragraphs to the browser", async () => {
+  await using screen = await mountEditor("**first**\n**second**");
+  const { editor } = screen;
-  const editor = await mountEditor("**first**\n**second**");
   await act(async () => {
     const first = editor.state.doc.child(0);
     editor.commands.setTextSelection(first.nodeSize + 1);
@@ -101,7 +89,8 @@
 it.each([{ shiftKey: true }, { altKey: true }, { ctrlKey: true }, { isComposing: true }])(
   "preserves modified Up behavior for %j",
   async (options) => {
+    await using screen = await mountEditor(draft);
+    const { editor } = screen;
-    const editor = await mountEditor(draft);
     await act(async () => {
       expect(press(editor, "ArrowUp", options)).toBe(false);
       editor.commands.insertContent("x");
@@ -111,10 +100,11 @@
 );
 
 describe("macOS boundary navigation", () => {
-  beforeEach(() => {
-    vi.spyOn(navigator, "platform", "get").mockReturnValue("MacIntel");
+  function macContainer() {
+    const container = document.createElement("div");
     container.style.width = "600px";
+    return container;
-  });
+  }
 
   const markdown = String.raw`Fixed the top padding to 16px and updated the offline index and ZIP.
 
@@ -129,7 +119,9 @@
   it.each(["ArrowRight", "ArrowDown"])(
     "keeps Command-%s after the closing marker in the pasted draft",
     async (key) => {
+      using _platform = vi.spyOn(navigator, "platform", "get").mockReturnValue("MacIntel");
+      await using screen = await mountEditor(markdown, true, macContainer());
-      const editor = await mountEditor(markdown);
+      const { editor } = screen;
       await act(async () => {
         editor.view.dispatch(editor.state.tr.setSelection(TextSelection.atEnd(editor.state.doc)));
         editor.view.focus();
@@ -156,7 +148,9 @@
   it.each(["ArrowRight", "ArrowDown"])(
     "moves Command-%s from inside bold to outside its closing marker",
     async (key) => {
+      using _platform = vi.spyOn(navigator, "platform", "get").mockReturnValue("MacIntel");
+      await using screen = await mountEditor("**bold**", true, macContainer());
+      const { editor } = screen;
-      const editor = await mountEditor("**bold**");
       await act(async () => {
         editor.commands.setTextSelection(3);
         editor.view.focus();
@@ -168,7 +162,9 @@
   );
 
   it.each(["ArrowLeft", "ArrowUp"])("moves Command-%s before the opening marker", async (key) => {
+    using _platform = vi.spyOn(navigator, "platform", "get").mockReturnValue("MacIntel");
+    await using screen = await mountEditor("**bold**", true, macContainer());
-    const editor = await mountEditor("**bold**");
+    const { editor } = screen;
     await act(async () => {
       editor.commands.setTextSelection(3);
       editor.view.focus();
@@ -181,7 +177,9 @@
   it.each(["ArrowRight", "ArrowDown"])(
     "extends Shift-Command-%s to the end without collapsing the selection",
     async (key) => {
+      using _platform = vi.spyOn(navigator, "platform", "get").mockReturnValue("MacIntel");
+      await using screen = await mountEditor("**bold**", true, macContainer());
+      const { editor } = screen;
-      const editor = await mountEditor("**bold**");
       await act(async () => {
         editor.commands.setTextSelection(3);
         editor.view.focus();
@@ -195,8 +193,9 @@
   );
 
   it("leaves Command-arrow navigation to the browser on other platforms", async () => {
+    using _platform = vi.spyOn(navigator, "platform", "get").mockReturnValue("Linux x86_64");
+    await using screen = await mountEditor("**bold**", true, macContainer());
-    vi.spyOn(navigator, "platform", "get").mockReturnValue("Linux x86_64");
+    const { editor } = screen;
-    const editor = await mountEditor("**bold**");
     await act(async () => {
       editor.view.focus();
       expect(press(editor, "ArrowDown", { metaKey: true })).toBe(false);
@@ -205,7 +204,8 @@
 });
 
 it("leaves literal Markdown navigation to the browser in plain mode", async () => {
+  await using screen = await mountEditor(draft, false);
+  const { editor } = screen;
-  const editor = await mountEditor(draft, false);
   await act(async () => {
     editor.commands.setTextSelection(5);
     expect(press(editor, "ArrowUp")).toBe(false);
```

## apps/web/src/components/DiffPanel.browser.test.tsx

Status: modified; +57; −58

```diff
@@ -12,11 +12,11 @@
 import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings";
 import * as DateTime from "effect/DateTime";
 import { AsyncResult, Atom, AtomRegistry } from "effect/reactivity";
-import { act } from "react";
-import { createRoot, type Root } from "react-dom/client";
-import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
+import { expect, it, vi } from "vite-plus/test";
 import { page } from "vite-plus/test/browser";
 
+import { act, render } from "../../test/render";
+
 vi.mock("../hooks/useThreadRouteTarget", () => ({
   useThreadRouteRef: () => threadRef,
   useThreadRouteTarget: () => ({ ...threadRef, kind: "thread" }),
@@ -112,65 +112,62 @@
     path === "e.txt" ? AsyncResult.initial() : AsyncResult.success(loadedPatch(path)),
   ),
 );
-let registry: AtomRegistry.AtomRegistry;
+function setup() {
-let container: HTMLDivElement;
-let root: Root;
-
-beforeEach(() => {
-  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
   localStorage.setItem("t3code.diffFileTreeOpen", "true");
   useDiffPanelStore.setState({ byThreadKey: {}, branchBaseRefByThreadKey: {} });
+  const registry = AtomRegistry.make();
+  return {
+    registry,
+    [Symbol.dispose]() {
+      registry.dispose();
+      localStorage.clear();
-  registry = AtomRegistry.make();
+    },
-  container = document.createElement("div");
+  };
+}
+function renderPanel(registry: AtomRegistry.AtomRegistry) {
+  const container = document.createElement("div");
   container.style.cssText = "width: 1000px; height: 300px";
-  document.body.append(container);
-  root = createRoot(container);
-});
+  return render(
+    <RegistryContext.Provider value={registry}>
+      <DiffPanel
+        visible
+        mode="embedded"
+        composerDraftTarget={threadRef}
+        workspaceMutationId={null}
+      />
-afterEach(async () => {
-  await act(async () => root.unmount());
+    </RegistryContext.Provider>,
+    { container },
-  container.remove();
-  registry.dispose();
-  localStorage.clear();
-  vi.unstubAllGlobals();
-});
-async function render() {
-  await act(async () =>
-    root.render(
-      <RegistryContext.Provider value={registry}>
-        <DiffPanel
-          visible
-          mode="embedded"
-          composerDraftTarget={threadRef}
-          workspaceMutationId={null}
-        />
-      </RegistryContext.Provider>,
-    ),
   );
 }
 const toggle = (path: string, expanded: boolean) =>
   page.getByRole("button", { name: `${expanded ? "Collapse" : "Expand"} ${path}`, exact: true });
-const viewport = () => container.querySelector<HTMLElement>(".diff-render-surface")!;
+const viewport = (container: HTMLElement) =>
+  container.querySelector<HTMLElement>(".diff-render-surface")!;
 async function selectTurn(path?: string, turn = turns[0]!) {
   await act(async () => useDiffPanelStore.getState().selectTurn(threadRef, turn.runId, path));
 }
-async function expectRevealed(path: string) {
+async function expectRevealed(container: HTMLElement, path: string) {
   await expect.element(toggle(path, true)).toBeInTheDocument();
   await expect
     .poll(() => {
-      const bounds = viewport().getBoundingClientRect();
+      const bounds = viewport(container).getBoundingClientRect();
       const header = toggle(path, true).element().getBoundingClientRect();
+      return (
+        viewport(container).scrollTop > 0 &&
+        header.top >= bounds.top &&
+        header.bottom <= bounds.bottom
+      );
-      return viewport().scrollTop > 0 && header.top >= bounds.top && header.bottom <= bounds.bottom;
     })
     .toBe(true);
 }
+async function scrollToStart(container: HTMLElement) {
-async function scrollToStart() {
   await act(async () => {
-    viewport().scrollTop = 0;
-    viewport().dispatchEvent(new Event("scroll"));
+    viewport(container).scrollTop = 0;
+    viewport(container).dispatchEvent(new Event("scroll"));
     await new Promise(requestAnimationFrame);
   });
 }
-async function selectTreeFile(path: string) {
+async function selectTreeFile(container: HTMLElement, path: string) {
   const tree = container.querySelector("file-tree-container")!;
   const row = tree.shadowRoot!.querySelector<HTMLElement>(`[data-item-path="${path}"]`)!;
   await act(async () =>
@@ -181,30 +178,31 @@
 }
 
 it("reveals a delayed turn selection, preserves siblings, and repeats after scope changes", async () => {
+  using env = setup();
   await selectTurn("e.txt");
-  await render();
+  await using screen = await renderPanel(env.registry);
   await expect
     .element(page.getByRole("status", { name: "Loading checkpoint diff..." }))
     .toBeInTheDocument();
+  await act(async () => env.registry.set(checkpointAtom, paths.map(filePatch).join("")));
+  await expectRevealed(screen.container, "e.txt");
-  await act(async () => registry.set(checkpointAtom, paths.map(filePatch).join("")));
-  await expectRevealed("e.txt");
   await act(async () => useDiffPanelStore.getState().selectGitScope(threadRef, "branch"));
   await expect.element(toggle("a.txt", false)).toBeInTheDocument();
   await selectTurn("e.txt");
+  await expectRevealed(screen.container, "e.txt");
-  await expectRevealed("e.txt");
-  await scrollToStart();
+  await scrollToStart(screen.container);
   await toggle("a.txt", false).click();
   await selectTurn("b.txt");
   await expect.element(toggle("b.txt", true)).toBeInTheDocument();
   await toggle("b.txt", true).click();
   await selectTurn("e.txt");
+  await expectRevealed(screen.container, "e.txt");
-  await expectRevealed("e.txt");
+  await scrollToStart(screen.container);
-  await scrollToStart();
   await expect.element(toggle("a.txt", true)).toBeInTheDocument();
   await expect.element(toggle("b.txt", false)).toBeInTheDocument();
   await expect.element(toggle("c.txt", false)).toBeInTheDocument();
   await selectTurn("e.txt");
-  await expectRevealed("e.txt");
+  await expectRevealed(screen.container, "e.txt");
 
   await selectTurn(undefined, turns[1]!);
   await expect.element(toggle("a.txt", false)).toBeInTheDocument();
@@ -212,21 +210,22 @@
 });
 
 it("waits for a lazy Git patch while preserving sibling choices and expanded defaults", async () => {
+  using env = setup();
+  env.registry.set(collapsedAtom, false);
-  registry.set(collapsedAtom, false);
+  await using screen = await renderPanel(env.registry);
-  await render();
   await toggle("b.txt", true).click();
-  await selectTreeFile("b.txt");
+  await selectTreeFile(screen.container, "b.txt");
   await expect.element(toggle("b.txt", true)).toBeInTheDocument();
   await toggle("b.txt", true).click();
+  await scrollToStart(screen.container);
+  await selectTreeFile(screen.container, "e.txt");
-  await scrollToStart();
-  await selectTreeFile("e.txt");
-  expect(viewport().scrollTop).toBe(0);
+  expect(viewport(screen.container).scrollTop).toBe(0);
   await expect.element(toggle("e.txt", true)).not.toBeInTheDocument();
   await act(async () =>
-    registry.set(patchAtoms("e.txt"), AsyncResult.success(loadedPatch("e.txt"))),
+    env.registry.set(patchAtoms("e.txt"), AsyncResult.success(loadedPatch("e.txt"))),
   );
+  await expectRevealed(screen.container, "e.txt");
-  await expectRevealed("e.txt");
+  await scrollToStart(screen.container);
-  await scrollToStart();
   await expect.element(toggle("a.txt", true)).toBeInTheDocument();
   await expect.element(toggle("b.txt", false)).toBeInTheDocument();
   await act(async () => useDiffPanelStore.getState().selectGitScope(threadRef, "unstaged"));
```

## apps/web/src/components/GitActionsControl.browser.test.tsx

Status: modified; +54; −51

```diff
@@ -6,12 +6,12 @@
 } from "@t3tools/contracts";
 import * as Cause from "effect/Cause";
 import { AsyncResult } from "effect/reactivity";
-import { act, memo } from "react";
-import { createRoot, type Root } from "react-dom/client";
+import { memo } from "react";
+import { describe, expect, it, vi } from "vite-plus/test";
-import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
 import { page, userEvent } from "vite-plus/test/browser";
 
 import "~/index.css";
+import { act, render } from "../../test/render";
 
 type Branched = { readonly branch: string };
 type GitActionInput = { featureBranch?: boolean; threadId?: string; action: string };
@@ -168,20 +168,15 @@
 // ThreadDetailsPanel keeps this component mounted behind React.memo when threads change.
 const MemoGitActionsControl = memo(GitActionsControl);
 
+function control(thread = "thread", cwd = "/repo") {
+  return (
+    <MemoGitActionsControl
+      gitCwd={cwd}
+      activeThreadRef={{
+        environmentId: EnvironmentId.make("environment"),
+        threadId: ThreadId.make(thread),
+      }}
-let root: Root;
-let container: HTMLDivElement;
-
+    />
-function render(thread = "thread", cwd = "/repo") {
-  return act(async () =>
-    root.render(
-      <MemoGitActionsControl
-        gitCwd={cwd}
-        activeThreadRef={{
-          environmentId: EnvironmentId.make("environment"),
-          threadId: ThreadId.make(thread),
-        }}
-      />,
-    ),
   );
 }
 
@@ -195,8 +190,8 @@
   await expect.element(page.getByRole("dialog", { name: "Commit changes" })).toBeVisible();
 }
 
+// The mocked modules read `state`, so every test starts by resetting all of it.
+function setup() {
-beforeEach(() => {
-  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
   state.scopes = new Set([AuthSourceControlWriteScope]);
   state.primaryScopes = new Set([AuthSourceControlWriteScope, AuthOrchestrationOperateScope]);
   state.shell = { branch: "main" };
@@ -218,21 +213,12 @@
       if (state.draft && input.branch) state.draft = { ...state.draft, branch: input.branch };
     },
   });
-  container = document.createElement("div");
-  document.body.append(container);
-  root = createRoot(container);
-});
-
-afterEach(async () => {
-  await act(async () => root.unmount());
-  container.remove();
-  vi.unstubAllGlobals();
-  await vi.waitFor(() => expect(document.querySelector('[role="dialog"]')).toBeNull());
+}
-});
 
 describe("Git actions while thread details load", () => {
   it("does not offer a feature branch for a server thread without task permission", async () => {
+    setup();
+    await using _screen = await render(control());
-    await render();
     await openCommitDialog();
 
     // The primary environment's task grant must not leak into this thread's environment.
@@ -241,8 +227,9 @@
   });
 
   it("checks the task grant again when the feature-branch commit runs", async () => {
+    setup();
     state.scopes.add(AuthOrchestrationOperateScope);
-    await render();
+    await using _screen = await render(control());
     await openCommitDialog();
     state.scopes.delete(AuthOrchestrationOperateScope);
 
@@ -254,9 +241,10 @@
   });
 
   it("commits and synchronizes the server thread before details finish loading", async () => {
+    setup();
     state.primaryScopes.clear();
     state.scopes.add(AuthOrchestrationOperateScope);
+    await using _screen = await render(control());
-    await render();
     await openCommitDialog();
     await userEvent.click(button("Commit on new branch"));
 
@@ -267,11 +255,12 @@
   });
 
   it("synchronizes archived thread detail after its shell disappears", async () => {
+    setup();
     state.scopes.add(AuthOrchestrationOperateScope);
     state.detail = { branch: "main" };
-    await render();
+    await using screen = await render(control());
     state.shell = null;
-    await render();
+    await screen.rerender(control());
     await openCommitDialog();
     await userEvent.click(button("Commit on new branch"));
 
@@ -281,9 +270,10 @@
   });
 
   it("requires task permission before changing an archived thread's branch", async () => {
+    setup();
     state.shell = null;
     state.detail = { branch: "main" };
-    await render();
+    await using _screen = await render(control());
     await openCommitDialog();
 
     await expect.element(button("Commit on new branch")).toBeDisabled();
@@ -291,9 +281,10 @@
   });
 
   it("uses the current shell branch when cached detail names the new branch", async () => {
+    setup();
     state.scopes.add(AuthOrchestrationOperateScope);
     state.detail = { branch: "feature" };
-    await render();
+    await using _screen = await render(control());
     await openCommitDialog();
     await userEvent.click(button("Commit on new branch"));
 
@@ -307,9 +298,10 @@
   ] as const)(
     "uses the fresh task grant after Git finishes, revoking %s",
     async (revokedScope, syncsThread) => {
+      setup();
       state.scopes.add(AuthOrchestrationOperateScope);
       state.afterGitAction = () => state.scopes.delete(revokedScope);
-      await render();
+      await using _screen = await render(control());
       await openCommitDialog();
       await userEvent.click(button("Commit on new branch"));
 
@@ -321,7 +313,8 @@
   );
 
   it("keeps ordinary commits available while details load", async () => {
+    setup();
+    await using _screen = await render(control());
-    await render();
     await openCommitDialog();
     await userEvent.click(button("Commit"));
 
@@ -331,9 +324,10 @@
   });
 
   it("keeps feature-branch commits available for a local draft", async () => {
+    setup();
     state.shell = null;
     state.draft = { branch: "main", worktreePath: null, envMode: "local" };
+    await using _screen = await render(control());
-    await render();
     await openCommitDialog();
     await userEvent.click(button("Commit on new branch"));
 
@@ -344,17 +338,19 @@
 });
 
 describe("Changed-file editor targets", () => {
+  function setupNestedFiles() {
-  beforeEach(() => {
+    setup();
     state.scopes.add(AuthOrchestrationOperateScope);
     state.files = [
       { path: "nested/new.txt", insertions: 0, deletions: 0 },
       { path: "nested/tracked.txt", insertions: 1, deletions: 0 },
     ];
+  }
-  });
 
   it("resolves repository-relative paths against the status root, not the project cwd", async () => {
+    setupNestedFiles();
     state.workingTreeRoot = "/repo";
-    await render("thread", "/repo/nested");
+    await using _screen = await render(control("thread", "/repo/nested"));
     await openCommitDialog();
 
     await userEvent.click(button("Open nested/new.txt in editor"));
@@ -371,7 +367,8 @@
   });
 
   it("does not guess a file location when the server reports no root", async () => {
+    setupNestedFiles();
-    await render("thread", "/repo/nested");
+    await using _screen = await render(control("thread", "/repo/nested"));
     await openCommitDialog();
 
     await expect.element(button("Open nested/new.txt in editor")).toBeDisabled();
@@ -380,13 +377,15 @@
 });
 
 describe("Default branch prompt", () => {
+  function setupDefaultRef() {
-  beforeEach(() => {
+    setup();
     state.isDefaultRef = true;
     state.scopes.add(AuthOrchestrationOperateScope);
-  });
+  }
 
   it("moves the commit to a feature branch when asked", async () => {
+    setupDefaultRef();
-    await render();
+    await using _screen = await render(control());
     await userEvent.click(button(/^Commit & push/));
     await expect
       .element(page.getByRole("dialog", { name: "Commit & push to default ref?" }))
@@ -400,7 +399,8 @@
   });
 
   it("checks the task grant again when the feature branch is chosen", async () => {
-    await render();
+    setupDefaultRef();
+    await using _screen = await render(control());
     await userEvent.click(button(/^Commit & push/));
     await expect
       .element(page.getByRole("dialog", { name: "Commit & push to default ref?" }))
@@ -418,12 +418,13 @@
   const quickAction = () => button(/^Commit, push & /);
 
   it("runs a retained PR toast in the thread that produced it", async () => {
+    setup();
+    await using screen = await render(control("first", "/first"));
-    await render("first", "/first");
     await userEvent.click(quickAction());
     await vi.waitFor(() => expect(state.toasts.some((toast) => toast.actionProps)).toBe(true));
     const toast = state.toasts.find((toast) => toast.actionProps)!;
 
-    await render("second", "/second");
+    await screen.rerender(control("second", "/second"));
     await act(async () => toast.actionProps!.onClick());
 
     expect(state.actions.at(-1)).toMatchObject({
@@ -433,12 +434,14 @@
   });
 
   it("runs the memoized button in the current thread after navigation", async () => {
+    setup();
+    await using screen = await render(control("first", "/first"));
     for (const [thread, cwd] of [
       ["first", "/first"],
       ["second", "/second"],
       ["first", "/first"],
     ] as const) {
+      await screen.rerender(control(thread, cwd));
-      await render(thread, cwd);
       await userEvent.click(quickAction());
       await vi.waitFor(() =>
         expect(state.actions.at(-1)).toMatchObject({ scope: { cwd }, input: { threadId: thread } }),
```

## apps/web/src/components/HostedEmptyState.browser.test.tsx

Status: modified; +7; −19

```diff
@@ -2,9 +2,9 @@
 import { EnvironmentId } from "@t3tools/contracts";
 import * as Option from "effect/Option";
 import { AsyncResult } from "effect/reactivity";
-import { act } from "react";
+import { expect, it, vi } from "vite-plus/test";
+
+import { act, render } from "../../test/render";
-import { createRoot, type Root } from "react-dom/client";
-import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
 
 type DiscoveredEnvironments = Discovery.RelayEnvironmentDiscoveryState["environments"];
 type Availability = "checking" | "online";
@@ -83,11 +83,7 @@
 const registeredIds = () =>
   discovery.register.mock.calls.map(([registration]) => registration.target.environmentId);
 
+function setup() {
-let root: Root;
-let container: HTMLDivElement;
-
-beforeEach(() => {
-  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
   discovery.listeners.clear();
   discovery.state = {
     environments: new Map(),
@@ -96,21 +92,13 @@
     error: Option.none(),
   };
   discovery.register.mockReset().mockResolvedValue(AsyncResult.success(undefined));
-  container = document.createElement("div");
-  document.body.append(container);
-  root = createRoot(container);
-});
+}
 
-afterEach(async () => {
-  await act(async () => root.unmount());
-  container.remove();
-  vi.unstubAllGlobals();
-});
-
 // The first added computer replaces this screen with the app, so every
 // registration has to start in the same pass, after all health checks finish.
 it("adds every linked computer only after discovery finishes checking them", async () => {
+  setup();
+  await using _screen = await render(<T3ConnectSteps />);
-  await act(async () => root.render(<T3ConnectSteps />));
 
   await publish(linked({ [laptopId]: "checking", [desktopId]: "checking" }));
   await publish(linked({ [laptopId]: "online", [desktopId]: "checking" }));
```

## apps/web/src/components/KeybindingsConfigWarning.browser.test.tsx

Status: modified; +23; −32

```diff
@@ -6,9 +6,9 @@
   type AuthSessionState,
 } from "@t3tools/contracts";
 import { AsyncResult, Atom } from "effect/reactivity";
-import { act } from "react";
+import { expect, it, vi } from "vite-plus/test";
+
+import { act, render } from "../../test/render";
-import { createRoot, type Root } from "react-dom/client";
-import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
 
 type SessionResult = AsyncResult.AsyncResult<AuthSessionState, Error>;
 const state = vi.hoisted(() => ({
@@ -40,8 +40,6 @@
 import { KeybindingsConfigWarning } from "./KeybindingsConfigWarning";
 
 const environmentId = EnvironmentId.make("warning-environment");
-let root: Root;
-let container: HTMLDivElement;
 const session = (scopes: readonly AuthEnvironmentScope[]): AuthSessionState => ({
   authenticated: true,
   scopes,
@@ -54,10 +52,8 @@
 });
 const setScopes = (scopes: readonly AuthEnvironmentScope[]) =>
   appAtomRegistry.set(state.sessions.get(environmentId)!, AsyncResult.success(session(scopes)));
-const button = () => container.querySelector("button")!;
 
-beforeEach(() => {
+function setup(): Disposable {
-  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
   localStorage.setItem("t3code:last-editor", JSON.stringify("vscode"));
   state.sessions.set(
     environmentId,
@@ -67,33 +63,28 @@
   );
   state.run.mockReset().mockResolvedValue(AsyncResult.success(undefined));
   state.toast.mockReset();
-  container = document.createElement("div");
-  document.body.append(container);
+  return {
+    [Symbol.dispose]() {
+      localStorage.clear();
+      state.sessions.clear();
-  root = createRoot(container);
+    },
-});
+  };
+}
 
-afterEach(async () => {
-  await act(async () => root.unmount());
-  container.remove();
-  localStorage.clear();
-  state.sessions.clear();
-  vi.unstubAllGlobals();
-});
-
 it("updates a visible warning on revocation and blocks a click that raced it until regrant", async () => {
+  using _state = setup();
+  await using screen = await render(
+    <RegistryContext.Provider value={appAtomRegistry}>
-  await act(async () =>
+      <KeybindingsConfigWarning
+        environmentId={environmentId}
+        configPath="/t3/keybindings.json"
+        availableEditors={["vscode"]}
+        message="Invalid shortcut"
+      />
+    </RegistryContext.Provider>,
-    root.render(
-      <RegistryContext.Provider value={appAtomRegistry}>
-        <KeybindingsConfigWarning
-          environmentId={environmentId}
-          configPath="/t3/keybindings.json"
-          availableEditors={["vscode"]}
-          message="Invalid shortcut"
-        />
-      </RegistryContext.Provider>,
-    ),
   );
+  const button = () => screen.container.querySelector("button")!;
+  expect(screen.container.textContent).toContain("Invalid shortcut");
-  expect(container.textContent).toContain("Invalid shortcut");
   expect(button().disabled).toBe(false);
 
   // The click lands before React re-renders the revoked grant.
```

## apps/web/src/components/PermissionUpdateNotice.browser.test.tsx

Status: modified; +23; −27

```diff
@@ -1,6 +1,6 @@
-import { act } from "react";
+import { expect, it, vi } from "vite-plus/test";
+
+import { render } from "../../test/render";
-import { createRoot, type Root } from "react-dom/client";
-import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
 
 type Session = {
   _tag: "Success" | "Failure";
@@ -37,12 +37,9 @@
 });
 import { PermissionUpdateNotice } from "./PermissionUpdateNotice";
 
-let root: Root;
-let container: HTMLDivElement;
 const storageKey = () => `t3code:permission-update:v1:env-${state.id}`;
 
-beforeEach(() => {
-  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
+function setup(): Disposable {
   state.id++;
   state.session = {
     _tag: "Success",
@@ -50,15 +47,9 @@
     value: { authenticated: true, permissions: ["orchestration:operate"] },
   };
   vi.clearAllMocks();
+  return { [Symbol.dispose]: () => localStorage.clear() };
+}
-  container = document.createElement("div");
-  root = createRoot(container);
-});
-afterEach(async () => {
-  await act(async () => root.unmount());
-  localStorage.clear();
-  vi.unstubAllGlobals();
-});
-const render = () => act(async () => root.render(<PermissionUpdateNotice />));
+
 type Notice = {
   timeout: number;
   actionProps: { onClick: () => void };
@@ -67,7 +58,8 @@
 const notice = () => state.add.mock.calls[0]![0] as Notice;
 
 it("keeps the notice visible and persists dismissal, with a route to pairing settings", async () => {
+  using _state = setup();
-  await render();
+  await using _screen = await render(<PermissionUpdateNotice />);
   expect(notice().timeout).toBe(0);
   expect(localStorage.getItem(storageKey())).toBeNull();
   notice().actionProps.onClick();
@@ -76,37 +68,41 @@
   expect(localStorage.getItem(storageKey())).toBe("true");
 });
 it("does not repeat on session refresh or remount", async () => {
+  using _state = setup();
+  await using screen = await render(<PermissionUpdateNotice />);
+  await screen.unmount();
-  await render();
-  await act(async () => root.unmount());
-  root = createRoot(container);
   state.session = { ...state.session };
-  await render();
+  await using _remounted = await render(<PermissionUpdateNotice />);
   expect(state.add).toHaveBeenCalledTimes(1);
 });
 it("skips a notice dismissed on an earlier launch", async () => {
+  using _state = setup();
   localStorage.setItem(storageKey(), "true");
-  await render();
+  await using _screen = await render(<PermissionUpdateNotice />);
   expect(state.add).not.toHaveBeenCalled();
 });
 it("waits for a successful, settled session check", async () => {
+  using _state = setup();
   state.session._tag = "Failure";
-  await render();
+  await using screen = await render(<PermissionUpdateNotice />);
   expect(state.add).not.toHaveBeenCalled();
   state.session = { ...state.session, _tag: "Success", waiting: true };
-  await render();
+  await screen.rerender(<PermissionUpdateNotice />);
   expect(state.add).not.toHaveBeenCalled();
   state.session = { ...state.session, waiting: false };
+  await screen.rerender(<PermissionUpdateNotice />);
-  await render();
   expect(state.add).toHaveBeenCalledTimes(1);
 });
 it("does not warn for a granular grant", async () => {
+  using _state = setup();
   state.session.value.permissions.push("filesystem:read");
+  await using _screen = await render(<PermissionUpdateNotice />);
-  await render();
   expect(state.add).not.toHaveBeenCalled();
 });
 
 it("persists the secondary dismissal without relying on the toast close callback", async () => {
+  using _state = setup();
+  await using _screen = await render(<PermissionUpdateNotice />);
-  await render();
   notice().data.secondaryActionProps.onClick();
   expect(localStorage.getItem(storageKey())).toBe("true");
   expect(state.close).toHaveBeenCalledWith("notice");
```

## apps/web/src/components/ReopenClosedViewShortcut.browser.test.tsx

Status: modified; +59; −49

```diff
@@ -2,9 +2,9 @@
 import { DEFAULT_RESOLVED_KEYBINDINGS } from "@t3tools/shared/keybindings";
 import * as Cause from "effect/Cause";
 import { AsyncResult } from "effect/reactivity";
-import { act } from "react";
+import { describe, expect, it, vi } from "vite-plus/test";
+
+import { act, render } from "../../test/render";
-import { createRoot, type Root } from "react-dom/client";
-import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
 
 const state = vi.hoisted(() => ({
   keybindings: [] as ResolvedKeybindingsConfig,
@@ -86,33 +86,9 @@
   canGoForward: false,
   updatedAt: "2026-09-22T12:00:00.000Z",
 } as const;
-let root: Root;
 let menuAction: ((action: string) => void) | undefined;
 
-async function render() {
-  await act(() => root.render(<ReopenClosedViewShortcut />));
-}
-
-function press(target: Element = document.body) {
-  const event = new KeyboardEvent("keydown", {
-    key: "T",
-    ctrlKey: true,
-    shiftKey: true,
-    bubbles: true,
-    cancelable: true,
-  });
-  target.dispatchEvent(event);
-  return event;
-}
-
-function openCommandPalette() {
-  const palette = document.createElement("div");
-  palette.dataset.commandPalette = "";
-  document.body.append(palette);
-  return palette;
-}
-
+function setup() {
-beforeEach(() => {
   vi.clearAllMocks();
   Object.assign(state, {
     keybindings: DEFAULT_RESOLVED_KEYBINDINGS,
@@ -127,8 +103,11 @@
     terminalUiStateByThreadKey: {},
     suppressedTerminalIdsByThreadKey: {},
   });
-  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
-  vi.spyOn(navigator, "platform", "get").mockReturnValue("Linux x86_64");
+}
+
+async function renderShortcut() {
+  await using stack = new AsyncDisposableStack();
+  stack.use(vi.spyOn(navigator, "platform", "get").mockReturnValue("Linux x86_64"));
   window.desktopBridge = {
     preview: { setForwardedShortcuts: state.setShortcuts },
     onMenuAction: (listener: (action: string) => void) => {
@@ -138,23 +117,47 @@
       };
     },
   } as unknown as NonNullable<typeof window.desktopBridge>;
+  stack.defer(() => {
+    delete window.desktopBridge;
+  });
+  stack.use(await render(<ReopenClosedViewShortcut />));
+  const environment = stack.move();
+  return {
+    /** Attaches an element to the document until the test ends. */
+    append(element: HTMLElement) {
+      document.body.append(element);
-  root = createRoot(document.createElement("div"));
+      environment.defer(() => element.remove());
-});
+      return element;
+    },
+    [Symbol.asyncDispose]: () => environment.disposeAsync(),
+  };
+}
 
+function press(target: Element = document.body) {
+  const event = new KeyboardEvent("keydown", {
+    key: "T",
+    ctrlKey: true,
+    shiftKey: true,
+    bubbles: true,
+    cancelable: true,
+  });
+  target.dispatchEvent(event);
+  return event;
+}
+
+function commandPalette() {
+  const palette = document.createElement("div");
+  palette.dataset.commandPalette = "";
+  return palette;
-afterEach(async () => {
+}
-  await act(() => root.unmount());
-  delete window.desktopBridge;
-  document.body.replaceChildren();
-  vi.restoreAllMocks();
-  vi.unstubAllGlobals();
-});
 
 describe("root reopen shortcut", () => {
   it("restores a tab from settings to its owning remote thread", async () => {
+    setup();
     useClosedViewStore
       .getState()
       .remember({ kind: "panel-tab", threadRef: ref, surface: { kind: "diff", id: "diff" } });
-    await render();
+    await using _view = await renderShortcut();
     await act(() => {
       expect(press().defaultPrevented).toBe(true);
     });
@@ -166,32 +169,35 @@
   });
 
   it("leaves native shortcuts alone without history or while the palette is open", async () => {
+    setup();
+    await using view = await renderShortcut();
-    await render();
     expect(press().defaultPrevented).toBe(false);
     await act(() => {
       useClosedViewStore
         .getState()
         .remember({ kind: "panel-tab", threadRef: ref, surface: { kind: "diff", id: "diff" } });
     });
+    view.append(commandPalette());
-    openCommandPalette();
     expect(press().defaultPrevented).toBe(false);
     expect(state.navigate).not.toHaveBeenCalled();
     expect(useClosedViewStore.getState().entries).toHaveLength(1);
   });
 
   it("lets the keybinding recorder capture the chord", async () => {
+    setup();
     useClosedViewStore
       .getState()
       .remember({ kind: "panel-tab", threadRef: ref, surface: { kind: "diff", id: "diff" } });
-    await render();
+    await using view = await renderShortcut();
     const recorder = document.createElement("input");
     recorder.dataset.keybindingCapture = "";
+    view.append(recorder);
-    document.body.append(recorder);
     expect(press(recorder).defaultPrevented).toBe(false);
     expect(useClosedViewStore.getState().entries).toHaveLength(1);
   });
 
   it("serializes deliberate presses while a browser restore waits for RPC", async () => {
+    setup();
     useClosedViewStore
       .getState()
       .remember({ kind: "panel-tab", threadRef: ref, surface: { kind: "files", id: "files" } });
@@ -202,7 +208,7 @@
         finish = resolve;
       }),
     );
+    await using _view = await renderShortcut();
-    await render();
     await act(() => {
       press();
       press();
@@ -217,6 +223,7 @@
   });
 
   it("does not create a second browser when navigation fails after restoring it", async () => {
+    setup();
     const store = useClosedViewStore.getState();
     const older = store.remember({
       kind: "panel-tab",
@@ -226,7 +233,7 @@
     store.remember({ kind: "browser", threadRef: ref, snapshot });
     state.openPreview.mockResolvedValue(AsyncResult.success({ tabId: "new-tab" }));
     state.navigate.mockRejectedValueOnce(new Error("navigation failed"));
-    await render();
+    await using _view = await renderShortcut();
     await act(() => {
       press();
     });
@@ -250,6 +257,7 @@
   it.each(["files", "file", "browser"] as const)(
     "keeps a failed %s restore retryable without blocking older history",
     async (kind) => {
+      setup();
       const store = useClosedViewStore.getState();
       const older = store.remember({
         kind: "panel-tab",
@@ -276,7 +284,7 @@
       );
       state.workspaceAvailable = false;
       state.openPreview.mockResolvedValue(AsyncResult.failure(Cause.fail(new Error("offline"))));
+      await using _view = await renderShortcut();
-      await render();
       await act(() => {
         press();
       });
@@ -307,6 +315,7 @@
   );
 
   it("forwards the chord to a focused desktop browser only while history exists", async () => {
+    setup();
     const forwardedCommands = () =>
       (state.setShortcuts.mock.lastCall as unknown as [Array<{ command: string }>])[0].map(
         ({ command }) => command,
@@ -314,7 +323,7 @@
     useClosedViewStore
       .getState()
       .remember({ kind: "panel-tab", threadRef: ref, surface: { kind: "diff", id: "diff" } });
+    await using _view = await renderShortcut();
-    await render();
     expect(forwardedCommands()).toContain("view.reopenClosed");
     await act(() => {
       menuAction?.("view.reopenClosed");
@@ -324,7 +333,8 @@
   });
 
   it("always forwards the layout toggles out of a focused desktop browser", async () => {
+    setup();
+    await using _view = await renderShortcut();
-    await render();
     expect(state.setShortcuts).toHaveBeenLastCalledWith([
       expect.objectContaining({ command: "sidebar.toggle" }),
       expect.objectContaining({ command: "rightPanel.toggle" }),
```

## apps/web/src/components/RightPanelTabs.browser.test.tsx

Status: modified; +84; −87

```diff
@@ -1,9 +1,9 @@
 import type { DesktopPreviewFavicon, PreviewSessionSnapshot } from "@t3tools/contracts";
 import { DEFAULT_RESOLVED_KEYBINDINGS } from "@t3tools/shared/keybindings";
-import { act } from "react";
-import { createRoot, type Root } from "react-dom/client";
-import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
+import { describe, expect, it, vi } from "vite-plus/test";
 
+import { act, render } from "../../test/render";
+
 const setAudioMuted = vi.hoisted(() => vi.fn(async () => undefined));
 
 vi.mock(import("~/hooks/useTheme"), async (original) => ({
@@ -80,107 +80,101 @@
   };
 }
 
-let root: Root;
-let container: HTMLDivElement;
 const noop = () => undefined;
 
-beforeEach(() => {
-  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
-  setAudioMuted.mockClear();
-  container = document.createElement("div");
-  document.body.append(container);
-  root = createRoot(container);
-});
+function tabs(
-
-afterEach(async () => {
-  await act(() => root.unmount());
-  container.remove();
-  vi.unstubAllGlobals();
-});
-
-async function renderTabs(
   first: DesktopPreviewFavicon | null,
   second?: DesktopPreviewFavicon,
   audio?: { audible?: boolean; audioMuted?: boolean },
   previewRuntimeTabId: ((tabId: string) => string) | null = (tabId) => `runtime:${tabId}`,
 ) {
+  return (
+    <RightPanelTabs
+      mode="inline"
+      open
-  await act(() =>
+      keybindings={DEFAULT_RESOLVED_KEYBINDINGS}
-    root.render(
-      <RightPanelTabs
-        mode="inline"
-        open
-        keybindings={DEFAULT_RESOLVED_KEYBINDINGS}
-        getShortcutContext={() => ({
-          terminalFocus: false,
-          terminalOpen: false,
-          previewFocus: false,
-          previewOpen: false,
-          isWeb: true,
-          isDesktop: false,
-        })}
+      getShortcutContext={() => ({
+        terminalFocus: false,
+        terminalOpen: false,
+        previewFocus: false,
+        previewOpen: false,
+        isWeb: true,
+        isDesktop: false,
+      })}
+      surfaces={second ? [previewSurface, secondSurface] : [previewSurface]}
+      environmentId={null}
+      activeSurfaceId={previewSurface.id}
+      pendingSurfaceIds={new Set()}
+      previewSessions={sessions}
+      desktopByTabId={{
+        "tab-1": overlay(first, audio),
+        ...(second ? { "tab-2": overlay(second) } : {}),
+      }}
+      {...(previewRuntimeTabId ? { previewRuntimeTabId } : {})}
+      terminalLabelsById={new Map()}
+      onActivate={noop}
+      onCloseSurface={noop}
+      onCloseOtherSurfaces={noop}
+      onCloseSurfacesToRight={noop}
+      onCloseAllSurfaces={noop}
+      onCopyFilePath={noop}
+      onAddBrowser={noop}
+      onAddBrowserInProfile={noop}
+      onAddTerminal={noop}
+      onAddPullRequest={noop}
+      onAddPullRequests={noop}
+      onAddDiff={noop}
+      onAddFiles={noop}
+      onAddDevice={noop}
+      browserAvailable
+      terminalAvailable={false}
+      diffAvailable={false}
+      filesAvailable={false}
+      pullRequestAvailable={false}
+      pullRequestsAvailable={false}
+      deviceAvailable={false}
+    >
+      <div>content</div>
+    </RightPanelTabs>
-        surfaces={second ? [previewSurface, secondSurface] : [previewSurface]}
-        environmentId={null}
-        activeSurfaceId={previewSurface.id}
-        pendingSurfaceIds={new Set()}
-        previewSessions={sessions}
-        desktopByTabId={{
-          "tab-1": overlay(first, audio),
-          ...(second ? { "tab-2": overlay(second) } : {}),
-        }}
-        {...(previewRuntimeTabId ? { previewRuntimeTabId } : {})}
-        terminalLabelsById={new Map()}
-        onActivate={noop}
-        onCloseSurface={noop}
-        onCloseOtherSurfaces={noop}
-        onCloseSurfacesToRight={noop}
-        onCloseAllSurfaces={noop}
-        onCopyFilePath={noop}
-        onAddBrowser={noop}
-        onAddBrowserInProfile={noop}
-        onAddTerminal={noop}
-        onAddPullRequest={noop}
-        onAddPullRequests={noop}
-        onAddDiff={noop}
-        onAddFiles={noop}
-        onAddDevice={noop}
-        browserAvailable
-        terminalAvailable={false}
-        diffAvailable={false}
-        filesAvailable={false}
-        pullRequestAvailable={false}
-        pullRequestsAvailable={false}
-        deviceAvailable={false}
-      >
-        <div>content</div>
-      </RightPanelTabs>,
-    ),
   );
 }
 
+function setup() {
+  setAudioMuted.mockClear();
+}
+
+function renderTabs(...args: Parameters<typeof tabs>) {
+  return render(tabs(...args));
+}
-const imageSources = () => [...container.querySelectorAll("img")].map((image) => image.src);
+
-const button = (name: string) =>
+const imageSources = (container: HTMLElement) =>
+  [...container.querySelectorAll("img")].map((image) => image.src);
+const button = (container: HTMLElement, name: string) =>
   [...container.querySelectorAll("button")].find((element) => element.ariaLabel === name);
 
 describe("preview tab favicon", () => {
   it("prefers a live capture and never asks a public provider about a private hostname", async () => {
+    setup();
+    await using screen = await renderTabs(favicon(PIXEL_A, "http://24x.xf.local/"));
+    expect(imageSources(screen.container)).toEqual([PIXEL_A]);
-    await renderTabs(favicon(PIXEL_A, "http://24x.xf.local/"));
-    expect(imageSources()).toEqual([PIXEL_A]);
 
+    await screen.rerender(tabs(null));
+    expect(imageSources(screen.container)).toEqual([]);
-    await renderTabs(null);
-    expect(imageSources()).toEqual([]);
   });
 
   it("keeps route-specific captures isolated between live tabs on one origin", async () => {
-    await renderTabs(
+    setup();
+    await using screen = await renderTabs(
       favicon(PIXEL_A, "http://24x.xf.local/"),
       favicon(PIXEL_B, "http://24x.xf.local/admin"),
     );
+    expect(imageSources(screen.container)).toEqual([PIXEL_A, PIXEL_B]);
-    expect(imageSources()).toEqual([PIXEL_A, PIXEL_B]);
   });
 
   it("hides a capture while the server session still describes another origin", async () => {
-    await renderTabs(favicon(PIXEL_A, "https://example.com/"));
+    setup();
+    await using screen = await renderTabs(favicon(PIXEL_A, "https://example.com/"));
-    expect(imageSources()).not.toContain(PIXEL_A);
+    expect(imageSources(screen.container)).not.toContain(PIXEL_A);
   });
 });
 
@@ -189,22 +183,25 @@
     { audible: false, audioMuted: false },
     { audible: false, audioMuted: true },
   ])("is absent for a silent tab (muted=$audioMuted)", async (audio) => {
+    setup();
+    await using screen = await renderTabs(null, undefined, audio);
+    expect(button(screen.container, "Mute Local site")).toBeUndefined();
+    expect(button(screen.container, "Unmute Local site")).toBeUndefined();
-    await renderTabs(null, undefined, audio);
-    expect(button("Mute Local site")).toBeUndefined();
-    expect(button("Unmute Local site")).toBeUndefined();
   });
 
   it.each([
     { audioMuted: false, label: "Mute Local site", nextMuted: true },
     { audioMuted: true, label: "Unmute Local site", nextMuted: false },
   ])("$label addresses the desktop by runtime tab id", async ({ audioMuted, label, nextMuted }) => {
+    setup();
+    await using screen = await renderTabs(null, undefined, { audible: true, audioMuted });
+    await act(() => button(screen.container, label)!.click());
-    await renderTabs(null, undefined, { audible: true, audioMuted });
-    await act(() => button(label)!.click());
     expect(setAudioMuted).toHaveBeenCalledExactlyOnceWith("runtime:tab-1", nextMuted);
   });
 
   it("hides the toggle when no runtime tab id can be resolved", async () => {
+    setup();
+    await using screen = await renderTabs(null, undefined, { audible: true }, null);
+    expect(button(screen.container, "Mute Local site")).toBeUndefined();
-    await renderTabs(null, undefined, { audible: true }, null);
-    expect(button("Mute Local site")).toBeUndefined();
   });
 });
```

## apps/web/src/components/RightPanelTabs.keyboard.browser.test.tsx

Status: modified; +129; −124

```diff
@@ -1,109 +1,108 @@
-import { act, type ComponentProps } from "react";
-import { createRoot, type Root } from "react-dom/client";
-import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
+import type { ComponentProps } from "react";
+import { describe, expect, it, vi } from "vite-plus/test";
 import {
   compileResolvedKeybindingsConfig,
   DEFAULT_RESOLVED_KEYBINDINGS,
 } from "@t3tools/shared/keybindings";
 
+import { act, fakeTimers, render, stubGlobal } from "../../test/render";
 import { RightPanelTabs } from "./RightPanelTabs";
 
 vi.mock("~/hooks/useTheme", () => ({ useTheme: () => ({ resolvedTheme: "light" }) }));
 vi.mock("~/browser/browserDefaults", () => ({ useBrowserDefaults: () => ({ profiles: [] }) }));
 
-let root: Root;
-let container: HTMLDivElement;
-const addFiles = vi.fn();
 const noop = () => undefined;
 
-beforeEach(() => {
-  vi.useFakeTimers();
-  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
-  vi.stubGlobal(
-    "ResizeObserver",
-    class {
-      observe() {}
-      disconnect() {}
-      unobserve() {}
-    },
-  );
-  Object.defineProperty(Element.prototype, "getAnimations", {
-    configurable: true,
-    value: () => [],
-  });
-  vi.spyOn(navigator, "platform", "get").mockReturnValue("MacIntel");
-  addFiles.mockClear();
-  container = document.createElement("div");
-  document.body.append(container);
-  root = createRoot(container);
-});
-
-afterEach(async () => {
-  await act(() => root.unmount());
-  container.remove();
-  vi.restoreAllMocks();
-  vi.unstubAllGlobals();
-  vi.useRealTimers();
-});
+async function settle() {
+  await act(() => vi.advanceTimersByTimeAsync(0));
+  await act(() => vi.advanceTimersToNextFrame());
+}
 
-async function renderPanel(overrides: Partial<ComponentProps<typeof RightPanelTabs>> = {}) {
-  await act(() =>
-    root.render(
-      <RightPanelTabs
-        mode="inline"
-        open
-        keybindings={DEFAULT_RESOLVED_KEYBINDINGS}
-        getShortcutContext={() => ({
+async function renderPanel(
+  overrides: Partial<ComponentProps<typeof RightPanelTabs>> = {},
+  platform = "MacIntel",
+) {
+  await using stack = new AsyncDisposableStack();
+  stack.use(fakeTimers());
+  stack.use(
+    stubGlobal(
+      "ResizeObserver",
+      class {
+        observe() {}
+        disconnect() {}
+        unobserve() {}
+      },
-          terminalFocus: false,
-          terminalOpen: false,
-          previewFocus: false,
-          previewOpen: false,
-          isWeb: true,
-          isDesktop: false,
-        })}
-        surfaces={[]}
-        environmentId={null}
-        activeSurfaceId={null}
-        pendingSurfaceIds={new Set()}
-        previewSessions={{}}
-        desktopByTabId={{}}
-        terminalLabelsById={new Map()}
-        onActivate={noop}
-        onCloseSurface={noop}
-        onCloseOtherSurfaces={noop}
-        onCloseSurfacesToRight={noop}
-        onCloseAllSurfaces={noop}
-        onCopyFilePath={noop}
-        onAddBrowser={noop}
-        onAddBrowserInProfile={noop}
-        onAddTerminal={noop}
-        onAddDiff={noop}
-        onAddFiles={addFiles}
-        onAddPullRequest={noop}
-        onAddPullRequests={noop}
-        onAddDevice={noop}
-        browserAvailable={false}
-        terminalAvailable={false}
-        diffAvailable={false}
-        filesAvailable
-        pullRequestAvailable={false}
-        pullRequestsAvailable={false}
-        deviceAvailable={false}
-        {...overrides}
-      >
-        content
-      </RightPanelTabs>,
     ),
   );
+  // Menus wait for exit animations to finish, which never happens under fake timers.
+  stack.use(vi.spyOn(Element.prototype, "getAnimations").mockReturnValue([]));
+  stack.use(vi.spyOn(navigator, "platform", "get").mockReturnValue(platform));
+  const addFiles = vi.fn();
+  const panel = (props: Partial<ComponentProps<typeof RightPanelTabs>>) => (
+    <RightPanelTabs
+      mode="inline"
+      open
+      keybindings={DEFAULT_RESOLVED_KEYBINDINGS}
+      getShortcutContext={() => ({
+        terminalFocus: false,
+        terminalOpen: false,
+        previewFocus: false,
+        previewOpen: false,
+        isWeb: true,
+        isDesktop: false,
+      })}
+      surfaces={[]}
+      environmentId={null}
+      activeSurfaceId={null}
+      pendingSurfaceIds={new Set()}
+      previewSessions={{}}
+      desktopByTabId={{}}
+      terminalLabelsById={new Map()}
+      onActivate={noop}
+      onCloseSurface={noop}
+      onCloseOtherSurfaces={noop}
+      onCloseSurfacesToRight={noop}
+      onCloseAllSurfaces={noop}
+      onCopyFilePath={noop}
+      onAddBrowser={noop}
+      onAddBrowserInProfile={noop}
+      onAddTerminal={noop}
+      onAddDiff={noop}
+      onAddFiles={addFiles}
+      onAddPullRequest={noop}
+      onAddPullRequests={noop}
+      onAddDevice={noop}
+      browserAvailable={false}
+      terminalAvailable={false}
+      diffAvailable={false}
+      filesAvailable
+      pullRequestAvailable={false}
+      pullRequestsAvailable={false}
+      deviceAvailable={false}
+      {...props}
+    >
+      content
+    </RightPanelTabs>
+  );
+  // Nothing here navigates, and the test router would load on the fake clock too.
+  const screen = stack.use(await render(panel(overrides), { router: false }));
+  await settle();
+  const environment = stack.move();
+  return {
+    container: screen.container,
-  await act(() => vi.advanceTimersByTimeAsync(0));
-  await act(() => vi.advanceTimersToNextFrame());
+    addFiles,
+    rerender: async (props: Partial<ComponentProps<typeof RightPanelTabs>>) => {
+      await screen.rerender(panel(props));
+      await settle();
+    },
+    [Symbol.asyncDispose]: () => environment.disposeAsync(),
+  };
 }
 
 async function press(key: string, options: KeyboardEventInit = {}) {
   const event = new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true, ...options });
   await act(() => (document.activeElement ?? document.body).dispatchEvent(event));
-  await act(() => vi.advanceTimersByTimeAsync(0));
-  await act(() => vi.advanceTimersToNextFrame());
+  await settle();
   return event;
 }
 
@@ -111,8 +110,7 @@
   it.each(["MacIntel", "Win32", "Linux x86_64"])(
     "opens the menu on %s and chooses a tab",
     async (platform) => {
-      vi.spyOn(navigator, "platform", "get").mockReturnValue(platform);
-      await renderPanel();
+      await using screen = await renderPanel({}, platform);
       const event = await press(
         "t",
         platform === "MacIntel" ? { metaKey: true } : { ctrlKey: true },
@@ -132,35 +130,35 @@
         ).defaultPrevented,
       ).toBe(true);
       await press("f");
+      expect(screen.addFiles).toHaveBeenCalledOnce();
-      expect(addFiles).toHaveBeenCalledOnce();
       expect(document.querySelector('[role="menu"]:not([data-closed])')).toBeNull();
     },
   );
 
   it("opens Files while a closed popup remains mounted", async () => {
-    await renderPanel();
+    await using screen = await renderPanel();
     const popup = document.createElement("div");
     popup.dataset.slot = "menu-popup";
     popup.dataset.closed = "";
-    container.append(popup);
+    screen.container.append(popup);
 
     expect((await press("f")).defaultPrevented).toBe(true);
-    expect(addFiles).toHaveBeenCalledOnce();
+    expect(screen.addFiles).toHaveBeenCalledOnce();
   });
 
   it("opens the add-surface menu while a closed popup remains mounted", async () => {
+    await using screen = await renderPanel();
-    await renderPanel();
     const popup = document.createElement("div");
     popup.dataset.slot = "menu-popup";
     popup.dataset.closed = "";
-    container.append(popup);
+    screen.container.append(popup);
 
     expect((await press("t", { metaKey: true })).defaultPrevented).toBe(true);
     expect(document.querySelector('[role="menu"]:not([data-closed])')).not.toBeNull();
   });
 
   it("leaves the shortcut alone while the mounted panel is closed", async () => {
+    await using _screen = await renderPanel({ open: false });
-    await renderPanel({ open: false });
     expect((await press("t", { metaKey: true })).defaultPrevented).toBe(false);
     expect(document.querySelector('[role="menu"]:not([data-closed])')).toBeNull();
   });
@@ -168,21 +166,23 @@
   it.each(["Win32", "Linux x86_64"])(
     "leaves Ctrl+T for the focused terminal on %s",
     async (platform) => {
-      vi.spyOn(navigator, "platform", "get").mockReturnValue(platform);
-      await renderPanel({
-        getShortcutContext: () => ({
-          terminalFocus: true,
-          terminalOpen: true,
-          previewFocus: false,
-          previewOpen: false,
-          isWeb: true,
-          isDesktop: false,
-        }),
-      });
+      await using screen = await renderPanel(
+        {
+          getShortcutContext: () => ({
+            terminalFocus: true,
+            terminalOpen: true,
+            previewFocus: false,
+            previewOpen: false,
+            isWeb: true,
+            isDesktop: false,
+          }),
+        },
+        platform,
+      );
       const terminal = document.createElement("textarea");
       const onKeyDown = vi.fn();
       terminal.addEventListener("keydown", onKeyDown);
-      container.append(terminal);
+      screen.container.append(terminal);
       await act(() => terminal.focus());
       const event = await press("t", { ctrlKey: true });
       expect(event.defaultPrevented).toBe(false);
@@ -193,10 +193,11 @@
 
   it("uses a custom binding from a text field and closes on Escape", async () => {
     const input = document.createElement("input");
-    await renderPanel({
+    await using screen = await renderPanel({
       keybindings: compileResolvedKeybindingsConfig([{ key: "mod+y", command: "rightPanel.new" }]),
     });
     document.body.append(input);
+    using _input = { [Symbol.dispose]: () => input.remove() };
     input.focus();
     expect((await press("t", { metaKey: true })).defaultPrevented).toBe(false);
     await press("y", { metaKey: true });
@@ -205,25 +206,24 @@
     expect(document.querySelector('[role="menu"]:not([data-closed])')).toBeNull();
     // The menu hands focus back to its trigger once its close finishes, a few frames later.
     // Timers are fake here, so advance them instead of polling in real time.
-    const trigger = container.querySelector('[aria-label="Add panel surface"]');
+    const trigger = screen.container.querySelector('[aria-label="Add panel surface"]');
     for (let frame = 0; frame < 10 && document.activeElement !== trigger; frame++) {
       await act(() => vi.advanceTimersToNextFrame());
     }
     expect(document.activeElement).toBe(trigger);
-    input.remove();
   });
 
   it("does not open over another popup or during text composition", async () => {
-    await renderPanel();
+    await using screen = await renderPanel();
     expect((await press("t", { metaKey: true, isComposing: true })).defaultPrevented).toBe(false);
     const dialog = document.createElement("div");
     dialog.dataset.slot = "dialog-popup";
     dialog.dataset.open = "";
-    container.append(dialog);
+    screen.container.append(dialog);
     for (const state of ["data-open", "data-ending-style"]) {
       dialog.setAttribute(state, "");
       expect((await press("f")).defaultPrevented, state).toBe(false);
-      expect(addFiles, state).not.toHaveBeenCalled();
+      expect(screen.addFiles, state).not.toHaveBeenCalled();
       expect((await press("t", { metaKey: true })).defaultPrevented, state).toBe(false);
       expect(document.querySelector('[role="menu"]'), state).toBeNull();
       dialog.removeAttribute(state);
@@ -231,16 +231,16 @@
   });
 
   it("blocks panel shortcuts until a mounted popup finishes closing", async () => {
-    await renderPanel();
+    await using screen = await renderPanel();
     const popover = document.createElement("div");
     popover.dataset.slot = "popover-popup";
     popover.dataset.open = "";
-    container.append(popover);
+    screen.container.append(popover);
 
     expect((await press("t", { metaKey: true })).defaultPrevented).toBe(false);
     expect((await press("f")).defaultPrevented).toBe(false);
     expect(document.querySelector('[role="menu"]')).toBeNull();
-    expect(addFiles).not.toHaveBeenCalled();
+    expect(screen.addFiles).not.toHaveBeenCalled();
 
     delete popover.dataset.open;
     popover.dataset.closed = "";
@@ -248,12 +248,12 @@
     expect((await press("t", { metaKey: true })).defaultPrevented).toBe(false);
     expect((await press("f")).defaultPrevented).toBe(false);
     expect(document.querySelector('[role="menu"]')).toBeNull();
-    expect(addFiles).not.toHaveBeenCalled();
+    expect(screen.addFiles).not.toHaveBeenCalled();
 
     delete popover.dataset.endingStyle;
     popover.hidden = true;
     expect((await press("f")).defaultPrevented).toBe(true);
-    expect(addFiles).toHaveBeenCalledOnce();
+    expect(screen.addFiles).toHaveBeenCalledOnce();
     expect((await press("t", { metaKey: true })).defaultPrevented).toBe(true);
     expect(document.querySelector('[role="menu"]')?.textContent).toContain("Files");
     expect(document.activeElement?.closest('[role="menu"]')).not.toBeNull();
@@ -264,18 +264,18 @@
       mode: "sheet" as const,
       surfaces: [{ id: "files" as const, kind: "files" as const }],
     };
-    await renderPanel(panel);
+    await using screen = await renderPanel(panel);
     await press("t", { metaKey: true });
     await press("ArrowDown");
     await press("ArrowDown");
     await press("ArrowDown");
     expect(document.activeElement?.textContent).toContain("Files");
     await press("Enter");
-    expect(addFiles).toHaveBeenCalledOnce();
+    expect(screen.addFiles).toHaveBeenCalledOnce();
     await press("t", { metaKey: true });
-    await renderPanel({ ...panel, open: false });
+    await screen.rerender({ ...panel, open: false });
     expect(document.querySelector('[role="menu"]:not([data-closed])')).toBeNull();
-    await renderPanel(panel);
+    await screen.rerender(panel);
     expect(document.querySelector('[role="menu"]:not([data-closed])')).toBeNull();
   });
 });
@@ -285,8 +285,13 @@
     const onActivate = vi.fn();
     const onCloseSurface = vi.fn();
     const files = { id: "files" as const, kind: "files" as const };
+    await using screen = await renderPanel({
+      surfaces: [files],
+      activeSurfaceId: files.id,
+      onActivate,
+      onCloseSurface,
+    });
-    await renderPanel({ surfaces: [files], activeSurfaceId: files.id, onActivate, onCloseSurface });
+    const tabLabel = screen.container.querySelector("[data-active-tab] span.truncate")!;
-    const tabLabel = container.querySelector("[data-active-tab] span.truncate")!;
     const mouse = async (type: string, button: number) => {
       const event = new MouseEvent(type, { button, bubbles: true, cancelable: true });
       await act(() => tabLabel.dispatchEvent(event));
```

[Next page](https://git.cafe/pingdotgg/t3code/pull/22187/diffs?after=ImFwcHMvd2ViL3NyYy9jb21wb25lbnRzL1JpZ2h0UGFuZWxUYWJzLmtleWJvYXJkLmJyb3dzZXIudGVzdC50c3gi&expectedVersion=30&format=markdown)
