/** * CE-WP-0003-T06 integration — focusing a field auto-activates its first * linked evidence, the chip is marked aria-current, and the ScrollBridge * fires scrollTo on the viewer with the right annotation. * * Flow: * 1. Review mode: open fixture, inject a selection capture, save the * evidence item (T09 pattern). * 2. Switch to Forms mode. * 3. Stage the saved evidence in the strip. * 4. Click a field → BindingService creates a link. * 5. Click the field again → ActiveStateProvider focuses target → * chip list re-derives → auto-activates first chip → ScrollBridge * calls scrollTo → mock viewer sees `scrollToAnnotationId`. */ // @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 { pdfUrl: string | null; scrollToAnnotationId: string | null; onSelectionCaptured: ViewerProps["onSelectionCaptured"] | null; storedAnnotationIds: string[]; } const viewerSnapshot: ViewerSnapshot = { pdfUrl: null, scrollToAnnotationId: null, onSelectionCaptured: null, storedAnnotationIds: [], }; vi.mock("@anchor/index", async (importOriginal) => { const original = await importOriginal(); const MockPdfSpikeViewer = (props: ViewerProps) => { viewerSnapshot.pdfUrl = props.pdfUrl; viewerSnapshot.scrollToAnnotationId = props.scrollToAnnotationId ?? null; viewerSnapshot.onSelectionCaptured = props.onSelectionCaptured; viewerSnapshot.storedAnnotationIds = props.storedAnnotations.map((a) => a.id); return (
); }; return { ...original, PdfSpikeViewer: MockPdfSpikeViewer }; }); const FIXTURE = manifest.fixtures.find((f) => f.id === "fristsetzung-bezifferung")!; const SYNTHETIC_CANONICAL = [ "Pre quote.", FIXTURE.known_good_quote, "Post quote.", ].join(" "); // CE-WP-0005: pre-seed a session with the fixture doc; no ingestPdf mock needed. 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(); } function resetSnapshot() { viewerSnapshot.pdfUrl = null; viewerSnapshot.scrollToAnnotationId = null; viewerSnapshot.onSelectionCaptured = null; viewerSnapshot.storedAnnotationIds = []; } describe("FormsApp — active-evidence cycling (CE-WP-0003-T06)", () => { beforeEach(() => { resetSnapshot(); globalThis.localStorage?.clear(); if (typeof window !== "undefined") { history.replaceState(null, "", window.location.pathname); } seedSessionWithDoc({ sessionName: "T06-cycling", documentTitle: FIXTURE.filename, canonicalText: SYNTHETIC_CANONICAL, }); }); afterEach(() => { vi.restoreAllMocks(); cleanup(); }); it("focusing a linked field auto-activates the first evidence and bridges to viewer scroll", { timeout: 15000 }, async () => { const user = userEvent.setup(); await loadApp(); // --- Review mode: create an evidence item via the captured-selection flow. // CE-WP-0005: doc pre-seeded — skip fixture 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"), "Form-cycling test evidence", ); await user.click(screen.getByTestId("inline-capture-save")); await screen.findByText(/Form-cycling test evidence/); // --- Switch to Forms mode. await user.click(screen.getByRole("button", { name: "Capture" })); // The evidence should appear in the Forms strip too (it queries by doc). const stripCard = await screen.findByRole("button", { name: /Form-cycling test evidence/, }); // Focus Summary, then click strip card → link gets created. const summaryField = screen.getByLabelText("Summary of the matter"); await user.click(summaryField); await user.click(stripCard); // Link chip on Summary now shows "1 evidence" await waitFor( () => { expect(screen.queryByTestId("field-summary-chip")).not.toBeNull(); }, { timeout: 4000 }, ); // Resetting the previous scrollToAnnotationId so we can detect a *new* // scroll triggered by chip auto-activation. viewerSnapshot.scrollToAnnotationId = null; // Click the Summary field again — this re-focuses the target. The chip // computed for it should now contain our evidence; the chips' auto- // activation effect fires setActiveEvidence; ScrollBridge translates // it to a viewer scroll. // // Note: clicking the same field doesn't fire onFocus if it's already // focused. Move focus elsewhere first, then back. await user.click(screen.getByLabelText("Disputed amount")); await user.click(summaryField); // The strip card for the linked evidence has aria-current="true". await waitFor(() => { const card = document.querySelector( 'button[aria-current="true"]', ); expect(card).not.toBeNull(); expect(card!.textContent).toMatch(/Form-cycling test evidence/); }); // The viewer was asked to scroll to the underlying annotation. await waitFor(() => { expect(viewerSnapshot.scrollToAnnotationId).toMatch(/^ann_/); }); }); });