/** * CE-WP-0004-T05 — end-to-end test of PRD scenario step 10. * * Continues the slice-1 scenario verified by CE-WP-0002-T09: * * 1-5 (recap from T09): load app → pick fixture → inject selection → * save evidence item. * 10. Click Export → Copy as Markdown on the saved evidence item. * 11. Read the clipboard; assert it contains the quote text, the * document title, the commentary, and a URL matching the * `/viewer?document=...&annotation=...` shape. * * The Playwright form mentioned in the workplan is approximated with the * same happy-dom integration setup the rest of the slice-1 tests use * (see ADR-0004 for the headless-Chromium limitation that motivates * this trade-off). The viewer and the PDF ingest pipeline are mocked; * the clipboard is intercepted by patching `Clipboard.prototype.writeText`. */ // @vitest-environment happy-dom import { act, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { Selector } from "@shared/selector"; import type { PdfSelectionCapture } from "@anchor/index"; import manifest from "../../fixtures/pdfs/manifest.json" with { type: "json" }; import { seedSessionWithDoc } from "./helpers/seed-session"; // --------------------------------------------------------------------------- // Mocks // --------------------------------------------------------------------------- interface ViewerProps { pdfUrl: string; storedAnnotations: readonly { id: string; text: string; selectors: readonly Selector[] }[]; scrollToAnnotationId?: string; onSelectionCaptured(capture: PdfSelectionCapture, selectors: Selector[]): void; } interface ViewerSnapshot { pdfUrl: string | null; onSelectionCaptured: ViewerProps["onSelectionCaptured"] | null; } const viewerSnapshot: ViewerSnapshot = { pdfUrl: null, onSelectionCaptured: null, }; vi.mock("@anchor/index", async (importOriginal) => { const original = await importOriginal(); const MockPdfSpikeViewer = (props: ViewerProps) => { viewerSnapshot.pdfUrl = props.pdfUrl; viewerSnapshot.onSelectionCaptured = props.onSelectionCaptured; return
; }; return { ...original, PdfSpikeViewer: MockPdfSpikeViewer }; }); const FIXTURE = manifest.fixtures.find((f) => f.id === "fristsetzung-bezifferung")!; const SYNTHETIC_CANONICAL = ["Pre.", FIXTURE.known_good_quote, "Post."].join(" "); // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function syntheticCaptureFor(text: string, page: number): PdfSelectionCapture { return { kind: "pdf", text, page, rects: [{ x: 0.1, y: 0.2, width: 0.4, height: 0.04 }], boundingRect: { x: 0.1, y: 0.2, width: 0.4, height: 0.04 }, }; } let writeText: ReturnType; function installClipboardSpy() { writeText = vi.fn(async () => undefined); // happy-dom's Clipboard.writeText lives on the prototype. Patch there // so the spy intercepts every call without fighting the Navigator // class's private #clipboard field. const proto = Object.getPrototypeOf(navigator.clipboard); Object.defineProperty(proto, "writeText", { configurable: true, writable: true, value: writeText, }); } async function loadApp() { const { App } = await import("@app/App"); return render(); } // --------------------------------------------------------------------------- // Test // --------------------------------------------------------------------------- describe("CE-WP-0004-T05 — PRD scenario steps 10-11 end-to-end", () => { beforeEach(() => { viewerSnapshot.pdfUrl = null; viewerSnapshot.onSelectionCaptured = null; globalThis.localStorage?.clear(); if (typeof window !== "undefined") { history.replaceState(null, "", window.location.pathname); } seedSessionWithDoc({ sessionName: "T05", documentTitle: FIXTURE.filename, canonicalText: SYNTHETIC_CANONICAL, }); }); afterEach(() => { vi.restoreAllMocks(); }); it( "saves an evidence item, exports it as Markdown, and writes the citation card to the clipboard", { timeout: 15000 }, async () => { const user = userEvent.setup(); await loadApp(); installClipboardSpy(); // --- Steps 1-5 (recap from CE-WP-0002-T09) ------------------------- // CE-WP-0005: doc pre-seeded into session — skip fixture-button click. await waitFor(() => expect(viewerSnapshot.onSelectionCaptured).not.toBeNull()); await act(async () => { viewerSnapshot.onSelectionCaptured!( syntheticCaptureFor(FIXTURE.known_good_quote, FIXTURE.known_good_quote_page), [{ type: "TextQuoteSelector", exact: FIXTURE.known_good_quote }], ); }); await user.type( screen.getByTestId("inline-capture-commentary"), "Export E2E commentary", ); await user.click(screen.getByTestId("inline-capture-save")); await screen.findByText(/Export E2E commentary/); // --- Step 10: click Export → Copy as Markdown ---------------------- // Reinstall after render so happy-dom's prototype reset (observed // between beforeEach and the actual click) doesn't swallow the spy. installClipboardSpy(); await user.click(await screen.findByLabelText("Export evidence item")); await user.click( await screen.findByRole("menuitem", { name: "Copy as Markdown" }), ); // --- Step 11: clipboard contains the full citation card ------------ await waitFor(() => expect(writeText).toHaveBeenCalledTimes(1)); const written = writeText.mock.calls[0]![0] as string; // Quote text from the manifest fixture. expect(written).toContain(FIXTURE.known_good_quote); // Document title — App's load path uses the fixture filename as // the document title (via ingestPdf options.filename). expect(written).toContain(FIXTURE.filename); // Commentary entered above. expect(written).toContain("Export E2E commentary"); // Open-context URL of the canonical shape. The doc and annotation // ids are randomly minted; assert via regex. expect(written).toMatch(/\(\/viewer\?document=doc_[^&]+&annotation=ann_[^)]+\)/); // Success toast surfaces to the user. const toast = await screen.findByTestId("export-toast"); expect(toast.getAttribute("data-tone")).toBe("success"); expect(toast.textContent).toContain("Copied as Markdown"); }, ); });