test(ui): guard issue selection through polling

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-07-11 02:40:59 +00:00
1 parent 02e2dd271b
commit 769700ddb1
1 file changed
+132 -1
@@ -2,7 +2,8 @@
import { flushSync } from "react-dom";
import { createRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { act as reactAct } from "react";
import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";
import { afterEach, describe, expect, it, vi } from "vitest";
import { ThemeProvider } from "../context/ThemeContext";
import { MarkdownBody } from "./MarkdownBody";
@@ -30,6 +31,7 @@ let root: ReturnType<typeof createRoot> | null = null;
let container: HTMLDivElement | null = null;
afterEach(() => {
vi.useRealTimers();
if (root) {
flushSync(() => root?.unmount());
}
@@ -38,6 +40,21 @@ afterEach(() => {
container = null;
});
async function act<T>(callback: () => T | Promise<T>): Promise<T> {
if (typeof reactAct === "function") {
return await (reactAct(callback) as T | Promise<T>);
}
let result: T | Promise<T> | undefined;
flushSync(() => {
result = callback();
});
const resolved = await result;
await Promise.resolve();
flushSync(() => {});
return resolved as T;
}
const SAMPLE = "Some text\n\n```ts\nconst answer = 42;\n```\n\nAnd a [link](https://example.com).";
function tree(children: string, queryClient: QueryClient) {
@@ -50,6 +67,51 @@ function tree(children: string, queryClient: QueryClient) {
);
}
function IssueDetailPollingHarness({
loadContent,
loadRuns,
}: {
loadContent: () => Promise<{
description: string;
comment: string;
document: string;
refresh: number;
}>;
loadRuns: () => Promise<{ refresh: number }>;
}) {
const { data: content } = useQuery({
queryKey: ["selection-stability", "content"],
queryFn: loadContent,
});
useQuery({
queryKey: ["selection-stability", "runs"],
queryFn: loadRuns,
refetchInterval: 5_000,
});
if (!content) return null;
return (
<main>
<section data-testid="issue-description">
<MarkdownBody>{content.description}</MarkdownBody>
</section>
<section data-testid="issue-comment">
<MarkdownBody>{content.comment}</MarkdownBody>
</section>
<section data-testid="issue-document">
<MarkdownBody>{content.document}</MarkdownBody>
</section>
</main>
);
}
async function flushQueries() {
await act(async () => {
await Promise.resolve();
});
}
describe("MarkdownBody re-render stability (PAP-10767)", () => {
it("preserves rendered DOM nodes across a parent re-render with unchanged props", () => {
container = document.createElement("div");
@@ -111,4 +173,73 @@ describe("MarkdownBody re-render stability (PAP-10767)", () => {
expect(window.getSelection()?.toString()).toBe("Some text");
});
it("preserves issue text nodes through polling cycles and query invalidation", async () => {
vi.useFakeTimers();
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
let contentRefresh = 0;
let runsRefresh = 0;
const loadContent = vi.fn(async () => ({
description: "Stable issue description",
comment: "Stable issue comment",
document: "Stable issue document",
refresh: contentRefresh++,
}));
const loadRuns = vi.fn(async () => ({ refresh: runsRefresh++ }));
queryClient.setQueryData(["selection-stability", "content"], {
description: "Stable issue description",
comment: "Stable issue comment",
document: "Stable issue document",
refresh: -1,
});
queryClient.setQueryData(["selection-stability", "runs"], { refresh: -1 });
await act(async () => {
root?.render(
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<IssueDetailPollingHarness loadContent={loadContent} loadRuns={loadRuns} />
</ThemeProvider>
</QueryClientProvider>,
);
});
await flushQueries();
const descriptionNode = container.querySelector('[data-testid="issue-description"] p')?.firstChild;
const commentNode = container.querySelector('[data-testid="issue-comment"] p')?.firstChild;
const documentNode = container.querySelector('[data-testid="issue-document"] p')?.firstChild;
expect(descriptionNode?.nodeType).toBe(Node.TEXT_NODE);
expect(commentNode?.nodeType).toBe(Node.TEXT_NODE);
expect(documentNode?.nodeType).toBe(Node.TEXT_NODE);
const range = document.createRange();
range.setStart(documentNode!, 0);
range.setEnd(documentNode!, "Stable issue document".length);
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);
await act(async () => {
await vi.advanceTimersByTimeAsync(30_000);
});
expect(loadRuns).toHaveBeenCalledTimes(7);
expect(selection?.toString()).toBe("Stable issue document");
await act(async () => {
await queryClient.invalidateQueries({ queryKey: ["selection-stability", "content"] });
});
await flushQueries();
expect(container.querySelector('[data-testid="issue-description"] p')?.firstChild).toBe(descriptionNode);
expect(container.querySelector('[data-testid="issue-comment"] p')?.firstChild).toBe(commentNode);
expect(container.querySelector('[data-testid="issue-document"] p')?.firstChild).toBe(documentNode);
expect(selection?.toString()).toBe("Stable issue document");
expect(loadContent).toHaveBeenCalledTimes(2);
vi.useRealTimers();
});
});