/** * CE-WP-0003-T05 / CE-WP-0006-T05 — bidirectional evidence ↔ field linking. * * Flow: * 1. Review mode: seed session, capture selection, save evidence. * 2. Capture mode: field-focus-gated direct link (focus field, click card). */ // @vitest-environment happy-dom import { act, cleanup, 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" }; interface ViewerProps { pdfUrl: string; storedAnnotations: readonly { id: string; text: string; selectors: readonly Selector[] }[]; scrollToAnnotationId?: string; onSelectionCaptured(capture: PdfSelectionCapture, selectors: Selector[]): void; } interface ViewerSnapshot { onSelectionCaptured: ViewerProps["onSelectionCaptured"] | null; } const viewerSnapshot: ViewerSnapshot = { onSelectionCaptured: null }; vi.mock("@anchor/index", async (importOriginal) => { const original = await importOriginal(); const MockPdfSpikeViewer = (props: ViewerProps) => { 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(" "); import { seedSessionWithDoc } from "./helpers/seed-session"; 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 }, }; } async function loadApp() { const { App } = await import("@app/App"); return render(); } async function saveEvidenceInReview( user: ReturnType, commentary: string, ) { 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"), commentary); await user.click(screen.getByTestId("inline-capture-save")); await screen.findByText(new RegExp(commentary)); } describe("FormsApp — focus-gated linking (CE-WP-0007-T02)", () => { beforeEach(() => { viewerSnapshot.onSelectionCaptured = null; globalThis.localStorage?.clear(); if (typeof window !== "undefined") { history.replaceState(null, "", window.location.pathname); } }); afterEach(() => { vi.restoreAllMocks(); cleanup(); }); it( "links when field has focus: focus field then click strip card", { timeout: 15000 }, async () => { seedSessionWithDoc({ sessionName: "T05-field-first", documentTitle: FIXTURE.filename, canonicalText: SYNTHETIC_CANONICAL, }); const user = userEvent.setup(); await loadApp(); await saveEvidenceInReview(user, "Field-first link test"); await user.click(screen.getByRole("button", { name: "Capture" })); await screen.findByRole("button", { name: /Field-first link test/ }); await user.click(screen.getByLabelText("Disputed amount")); const stripCard = screen.getByRole("button", { name: /Field-first link test/, }); await user.click(stripCard); await waitFor(() => { expect(screen.getByTestId("field-amount-chip").textContent).toMatch( /1 evidence/, ); }); }, ); it( "does not link when no field is focused", { timeout: 15000 }, async () => { seedSessionWithDoc({ sessionName: "T07-no-focus", documentTitle: FIXTURE.filename, canonicalText: SYNTHETIC_CANONICAL, }); const user = userEvent.setup(); await loadApp(); await saveEvidenceInReview(user, "No-focus link test"); await user.click(screen.getByRole("button", { name: "Capture" })); const stripCard = await screen.findByRole("button", { name: /No-focus link test/, }); await user.click(stripCard); expect(screen.queryByTestId("field-summary-chip")).toBeNull(); expect(screen.queryByTestId("field-amount-chip")).toBeNull(); }, ); it("starts in the empty state when no session is active (CE-WP-0005 default)", async () => { const { unmount } = await loadApp(); expect(screen.getAllByTestId("empty-state").length).toBeGreaterThanOrEqual(1); expect(screen.queryByText("Demo evidence-backed form")).toBeNull(); unmount(); }); });