diff --git a/ui/src/components/MarkdownBody.rerender.test.tsx b/ui/src/components/MarkdownBody.rerender.test.tsx index cb655537236..db5f232dd97 100644 --- a/ui/src/components/MarkdownBody.rerender.test.tsx +++ b/ui/src/components/MarkdownBody.rerender.test.tsx @@ -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 | 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(callback: () => T | Promise): Promise { + if (typeof reactAct === "function") { + return await (reactAct(callback) as T | Promise); + } + + let result: T | Promise | 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 ( +
+
+ {content.description} +
+
+ {content.comment} +
+
+ {content.document} +
+
+ ); +} + +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( + + + + + , + ); + }); + 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(); + }); });