// @vitest-environment jsdom
import { act, type ReactNode } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createIssue } from "../../storybook/fixtures/paperclipData";
import { TaskList, type TaskListItem } from "./TaskList";
import { TooltipProvider } from "./ui/tooltip";
vi.mock("./IssueRow", () => ({ IssueRow: ({ issue, leadingControl }: { issue: { id: string; title: string }; leadingControl?: ReactNode }) =>
{leadingControl}
}));
(globalThis as Record).IS_REACT_ACT_ENVIRONMENT = true;
const tasks = (count: number, parentId: string | null = null): TaskListItem[] => Array.from({ length: count }, (_, i) => ({
issue: createIssue({ id: `task-${i}`, title: `Task ${i}`, parentId, createdAt: new Date(0), updatedAt: new Date(100_000 - i) }),
}));
describe("TaskList progressive rendering", () => {
let host: HTMLDivElement, root: Root;
beforeEach(() => { host = document.createElement("div"); document.body.append(host); root = createRoot(host); });
afterEach(async () => { await act(async () => root.unmount()); host.remove(); vi.unstubAllGlobals(); });
const rows = () => host.querySelectorAll("[data-issue-row-details]");
async function render(items: TaskListItem[], collectionKey?: string) {
await act(async () => root.render());
}
async function more() {
await act(async () => Array.from(host.querySelectorAll("button")).find(b => b.textContent === "Show more tasks")!.click());
}
it("bounds the first render, reveals batches, and retains mounted rows through enrichment and append", async () => {
const items = tasks(400);
await render(items);
expect(rows()).toHaveLength(100);
const first = rows()[0];
await more();
expect(rows()).toHaveLength(250);
await render(items.map(item => ({ ...item, assignee: "New name" })));
expect(rows()).toHaveLength(250);
expect(rows()[0]).toBe(first);
await render(tasks(450));
expect(rows()).toHaveLength(250);
await more(); await more();
expect(rows()).toHaveLength(450);
expect(host.textContent).not.toContain("Show more tasks");
});
it("retains revealed rows when an older server page sorts before them", async () => {
const items = tasks(200).map((item, i) => ({ issue: { ...item.issue, identifier: `PAP-${i + 21}` } }));
await render(items, "company:person:paged");
await more();
const originalRows = [...rows()];
const older = tasks(20).map((item, i) => ({ issue: { ...item.issue, id: `older-${i}`, identifier: `PAP-${i + 1}` } }));
await render([...items, ...older], "company:person:paged");
expect(rows()).toHaveLength(220);
expect(originalRows.every(row => row.isConnected)).toBe(true);
expect(rows()[0].getAttribute("data-issue-row-details")).toBe("older-0");
await act(async () => root.unmount());
root = createRoot(host);
await render([...items, ...older], "company:person:paged");
expect(rows()).toHaveLength(220);
});
it("uses one budget across a large expanded tree, preserving its disclosure and parent", async () => {
const parent = { issue: createIssue({ id: "root", title: "Root", parentId: null }) };
await render([parent, ...tasks(220, "root")]);
expect(rows()).toHaveLength(100);
expect(rows()[0].getAttribute("data-issue-row-details")).toBe("root");
const collapse = host.querySelector('button[aria-label="Collapse Root"]')!;
await act(async () => collapse.click());
expect(rows()).toHaveLength(1);
await act(async () => host.querySelector('button[aria-label="Expand Root"]')!.click());
expect(rows()).toHaveLength(100);
await more();
expect(rows()).toHaveLength(221);
});
it("reveals more when the sentinel intersects and resets the budget for a different result set", async () => {
let intersect!: (entries: { isIntersecting: boolean }[]) => void;
vi.stubGlobal("IntersectionObserver", class {
constructor(callback: typeof intersect) { intersect = callback; }
observe() {} disconnect() {}
});
await render(tasks(300));
expect(rows()).toHaveLength(100);
await act(async () => intersect([{ isIntersecting: true }]));
expect(rows()).toHaveLength(250);
await render(tasks(300).map(item => ({ issue: { ...item.issue, id: `other-${item.issue.id}` } })), "different-filter");
expect(rows()).toHaveLength(100);
});
});