Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions src/react/components/chat/chat/components/code-block.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { flushSync } from "react-dom";
import { createRoot } from "react-dom/client";
import { createRoot, type Root } from "react-dom/client";
import { renderToString } from "react-dom/server";
import { JSDOM } from "npm:jsdom@28.0.0";
import { assert, assertEquals, assertStringIncludes } from "#veryfront/testing/assert";
Expand Down Expand Up @@ -48,6 +48,14 @@ async function settle(): Promise<void> {
flushSync(() => {});
}

// Unmounting leaves a scheduler callback queued; drain it so the leak
// sanitizer does not attribute that timer to the test.
async function unmount(root: Root | undefined): Promise<void> {
if (!root) return;
flushSync(() => root.unmount());
await new Promise((resolve) => setTimeout(resolve, 0));
}

describe("RichCodeBlock — inline mode", () => {
it("renders an inline <code> element with no language label or copy button", () => {
const html = renderToString(<RichCodeBlock code="const x = 1;" inline />);
Expand Down Expand Up @@ -90,6 +98,7 @@ describe("RichCodeBlock — block mode", () => {

it("reports failed copies without leaking the fallback textarea", async () => {
const dom = installDom();
let root: Root | undefined;
Object.defineProperty(dom.window.navigator, "clipboard", {
configurable: true,
value: { writeText: () => Promise.reject(new Error("denied")) },
Expand All @@ -106,8 +115,9 @@ describe("RichCodeBlock — block mode", () => {
try {
const rootElement = document.getElementById("root");
assert(rootElement, "root fixture exists");
const root = createRoot(rootElement);
flushSync(() => root.render(<RichCodeBlock code="not copied" />));
const mountedRoot = createRoot(rootElement);
root = mountedRoot;
flushSync(() => mountedRoot.render(<RichCodeBlock code="not copied" />));

const button = rootElement.querySelector("button");
assert(button, "copy control renders");
Expand All @@ -122,14 +132,15 @@ describe("RichCodeBlock — block mode", () => {
"Unable to copy code",
);
assertEquals(document.querySelectorAll("textarea").length, 0);
flushSync(() => root.unmount());
} finally {
Comment thread
kojiwakayama marked this conversation as resolved.
await unmount(root);
dom.restore();
}
});

it("does not show stale success after the displayed code changes", async () => {
const dom = installDom();
let root: Root | undefined;
const pending: Array<() => void> = [];
Object.defineProperty(dom.window.navigator, "clipboard", {
configurable: true,
Expand All @@ -144,14 +155,15 @@ describe("RichCodeBlock — block mode", () => {
try {
const rootElement = document.getElementById("root");
assert(rootElement, "root fixture exists");
const root = createRoot(rootElement);
flushSync(() => root.render(<RichCodeBlock code="old code" />));
const mountedRoot = createRoot(rootElement);
root = mountedRoot;
flushSync(() => mountedRoot.render(<RichCodeBlock code="old code" />));

const firstButton = rootElement.querySelector("button");
assert(firstButton, "first copy control renders");
firstButton.dispatchEvent(new dom.window.MouseEvent("click", { bubbles: true }));

flushSync(() => root.render(<RichCodeBlock code="new code" />));
flushSync(() => mountedRoot.render(<RichCodeBlock code="new code" />));
const secondButton = rootElement.querySelector("button");
assert(secondButton, "updated copy control renders");
secondButton.dispatchEvent(new dom.window.MouseEvent("click", { bubbles: true }));
Expand All @@ -166,9 +178,8 @@ describe("RichCodeBlock — block mode", () => {
assertEquals(secondButton.textContent?.trim(), "Copied");
assertStringIncludes(rootElement.textContent ?? "", "new code");
assert(!(rootElement.textContent ?? "").includes("old code"));

flushSync(() => root.unmount());
} finally {
await unmount(root);
dom.restore();
}
});
Expand Down
19 changes: 13 additions & 6 deletions src/react/components/ui/command.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type * as React from "react";
import { flushSync } from "react-dom";
import { createRoot } from "react-dom/client";
import { createRoot, type Root } from "react-dom/client";
import { renderToString } from "react-dom/server";
import { JSDOM } from "npm:jsdom@28.0.0";
import { assert, assertEquals } from "#veryfront/testing/assert.ts";
Expand Down Expand Up @@ -92,6 +92,13 @@ function createChangeEvent(value: string): React.ChangeEvent<HTMLInputElement> {
return event as unknown as React.ChangeEvent<HTMLInputElement>;
}

// Unmounting leaves a scheduler callback queued; drain it so the leak
// sanitizer does not attribute that timer to the test.
async function unmount(root: Root): Promise<void> {
flushSync(() => root.unmount());
await new Promise((resolve) => setTimeout(resolve, 0));
}

describe("Command", () => {
it("exposes a listbox contract and tracks pointer-active options", async () => {
const dom = new JSDOM(
Expand Down Expand Up @@ -166,7 +173,7 @@ describe("Command", () => {
flushSync(() => disabled.click());
assertEquals(selected, ["beta"]);
} finally {
flushSync(() => root.unmount());
await unmount(root);
restore();
}
});
Expand Down Expand Up @@ -219,7 +226,7 @@ describe("Command", () => {
flushSync(() => cancelled.click());
assertEquals(calls, ["click", "select", "cancel-click"]);
} finally {
flushSync(() => root.unmount());
await unmount(root);
restore();
}
});
Expand Down Expand Up @@ -328,7 +335,7 @@ describe("Command", () => {
return document.getElementById(active ?? "")?.textContent === "Beta";
});
} finally {
flushSync(() => root.unmount());
await unmount(root);
restore();
}
});
Expand Down Expand Up @@ -410,7 +417,7 @@ describe("Command", () => {
await waitFor(() => document.querySelector("[data-empty]") === null);
assert(document.querySelector("[data-always]"));
} finally {
flushSync(() => root.unmount());
await unmount(root);
restore();
}
});
Expand Down Expand Up @@ -442,7 +449,7 @@ describe("Command", () => {
assertEquals(empty.getAttribute("aria-disabled"), "true");
assertEquals(empty.getAttribute("aria-live"), "polite");
} finally {
flushSync(() => root.unmount());
await unmount(root);
restore();
}
});
Expand Down
6 changes: 6 additions & 0 deletions src/react/components/ui/select.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1112,6 +1112,12 @@ describe("Select", () => {
() => document.getElementById("dynamic-default-list") === null,
"default-open duplicate content suppression",
);
// Suppression is synchronous with the invalid render; the close request
// to the owner is an effect that lands afterwards.
await waitFor(
() => openChanges.length === 1,
"default-open close request",
);
Comment thread
kojiwakayama marked this conversation as resolved.
assertEquals(rootElement.contains(trigger), true);
assertEquals(trigger.disabled, true);
assertEquals(openChanges, [false]);
Expand Down
38 changes: 23 additions & 15 deletions src/react/components/ui/tooltip.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { RefCallback } from "react";
import { flushSync } from "react-dom";
import { createRoot, hydrateRoot } from "react-dom/client";
import { createRoot, hydrateRoot, type Root } from "react-dom/client";
import { renderToString } from "react-dom/server";
import { JSDOM } from "npm:jsdom@28.0.0";
import { assert, assertEquals } from "#veryfront/testing/assert.ts";
Expand Down Expand Up @@ -100,6 +100,14 @@ function escape(window: JSDOM["window"], target: EventTarget): KeyboardEvent {
return event;
}

// Unmounting leaves a scheduler callback queued; drain it so the leak
// sanitizer does not attribute that timer to the test.
async function unmount(root: Root | undefined): Promise<void> {
if (!root) return;
flushSync(() => root.unmount());
await new Promise((resolve) => setTimeout(resolve, 0));
}

describe("Tooltip", () => {
it("gives the default trigger a keyboard path and honors an explicit tab index", async () => {
const dom = createDom();
Expand Down Expand Up @@ -140,7 +148,7 @@ describe("Tooltip", () => {
assert(tooltipId);
assertEquals(defaultTrigger.getAttribute("aria-describedby"), tooltipId);
} finally {
flushSync(() => root.unmount());
await unmount(root);
restore();
}
});
Expand Down Expand Up @@ -225,7 +233,7 @@ describe("Tooltip", () => {
assertEquals(childFocusCalls, 1);
assertEquals(recoverableErrors, []);
} finally {
root?.unmount();
await unmount(root);
restore();
}
});
Expand Down Expand Up @@ -263,7 +271,7 @@ describe("Tooltip", () => {
await waitFor(() => document.querySelector('[role="tooltip"]') === null);
assertEquals(trigger.hasAttribute("aria-describedby"), false);
} finally {
flushSync(() => root.unmount());
await unmount(root);
restore();
}
});
Expand Down Expand Up @@ -313,7 +321,7 @@ describe("Tooltip", () => {
});
await waitFor(() => document.querySelector('[role="tooltip"]') !== null);
} finally {
flushSync(() => root.unmount());
await unmount(root);
restore();
}
});
Expand Down Expand Up @@ -356,7 +364,7 @@ describe("Tooltip", () => {
assertEquals(calls, ["child", "trigger"]);
assert(document.querySelector('[role="tooltip"]') === null);
} finally {
flushSync(() => root.unmount());
await unmount(root);
restore();
}
});
Expand Down Expand Up @@ -391,7 +399,7 @@ describe("Tooltip", () => {
flushSync(() => unhover(dom.window, trigger));
await waitFor(() => document.querySelector('[role="tooltip"]') === null);
} finally {
flushSync(() => root.unmount());
await unmount(root);
restore();
}
});
Expand Down Expand Up @@ -439,7 +447,7 @@ describe("Tooltip", () => {
await new Promise((resolve) => setTimeout(resolve, 50));
assert(document.querySelector('[role="tooltip"]') === null);
} finally {
if (rootMounted) flushSync(() => root.unmount());
if (rootMounted) await unmount(root);
restore();
}
});
Expand Down Expand Up @@ -530,7 +538,7 @@ describe("Tooltip", () => {
flushSync(() => escape(targetDom.window, targetDocument));
await waitFor(() => targetDocument.querySelector('[role="tooltip"]') === null);
} finally {
flushSync(() => root.unmount());
await unmount(root);
targetDom.window.close();
restore();
}
Expand Down Expand Up @@ -571,7 +579,7 @@ describe("Tooltip", () => {
assertEquals(tooltip.style.overflowWrap, "normal");
assertEquals(tooltip.style.whiteSpace, "pre-wrap");
} finally {
flushSync(() => root.unmount());
await unmount(root);
restore();
}
});
Expand Down Expand Up @@ -599,12 +607,12 @@ describe("Tooltip", () => {
flushSync(() => hover(dom.window, trigger));
await waitFor(() => document.querySelector('[role="tooltip"]') !== null);
} finally {
flushSync(() => root.unmount());
await unmount(root);
restore();
}
});

it("caps excessive provider delays and cancels the owner-window timer", () => {
it("caps excessive provider delays and cancels the owner-window timer", async () => {
const dom = createDom();
const restore = installDom(dom);
const root = createRoot(document.getElementById("root")!);
Expand Down Expand Up @@ -654,7 +662,7 @@ describe("Tooltip", () => {
flushSync(() => unhover(dom.window, trigger));
assertEquals(clearedHandles, [47]);
} finally {
flushSync(() => root.unmount());
await unmount(root);
if (setTimeoutDescriptor) {
Object.defineProperty(dom.window, "setTimeout", setTimeoutDescriptor);
} else delete (dom.window as unknown as Record<string, unknown>).setTimeout;
Expand Down Expand Up @@ -704,7 +712,7 @@ describe("Tooltip", () => {
assertEquals(cleanupCalls, 0);
assertEquals(nullCalls, 0);
} finally {
flushSync(() => root.unmount());
await unmount(root);
assertEquals(attachedElement, null);
assertEquals(cleanupCalls, 1);
assertEquals(nullCalls, 0);
Expand Down Expand Up @@ -754,7 +762,7 @@ describe("Tooltip", () => {
assert(document.querySelector('[role="tooltip"]'));
assertEquals(refCalls, ["attach:1", "cleanup:1", "attach:2"]);
} finally {
flushSync(() => root.unmount());
await unmount(root);
assertEquals(refCalls, [
"attach:1",
"cleanup:1",
Expand Down
Loading