// IMP-#84 u1 — FramePanel reject silent-automation contract. // // Stage 2 unit u1 scope: // 1) `applyFrameSelection(candidate, onFrameSelect)` invokes onFrameSelect // with candidate.id verbatim for EVERY V4 label // (use_as_is / light_edit / restructure / reject) — no window.confirm // gate, no label-conditional branch, no frame swap. // 2) Source-presence checks pin the FramePanel.tsx wiring so the runtime // button → handler → helper chain stays intact even though we cannot // mount React (no jsdom / RTL / happy-dom in Front devDependencies — // verified against the IMP-56 u20 `imp90_bottom_actions.test.ts` and // IMP-92 u5 `imp47b_human_review_toast.test.tsx` precedent that // explicitly skip DOM mounting). // 3) No `window.confirm` substring remains in FramePanel.tsx after u1. // // Out of scope (Stage 2 exit-report contract): // - Home.tsx:523-524 `toast.error(aiReviewMsg)` (#92 operational-only). // - FramePanel reject badge/tooltip read-only labels at L102/L147/L156 // (no popup trigger; preserved as silent operator hint). // - Backend `zone.provisional` emission (handled by u2 template-only). import { readFileSync } from "node:fs"; import { resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { describe, it, expect, vi } from "vitest"; import { applyFrameSelection } from "../src/components/FramePanel"; import type { FrameCandidate } from "../src/types/designAgent"; const __dirname = dirname(fileURLToPath(import.meta.url)); const FRAME_PANEL_SOURCE = readFileSync( resolve(__dirname, "../src/components/FramePanel.tsx"), "utf-8", ); function makeCandidate( label: FrameCandidate["label"], id: string, ): FrameCandidate { return { id, name: `Frame ${id}`, score: 0.5, confidence: "medium", label, }; } describe("applyFrameSelection (IMP-#84 u1 — silent-automation contract)", () => { it("forwards candidate.id to onFrameSelect for use_as_is label", () => { const onFrameSelect = vi.fn(); applyFrameSelection(makeCandidate("use_as_is", "frame_a"), onFrameSelect); expect(onFrameSelect).toHaveBeenCalledTimes(1); expect(onFrameSelect).toHaveBeenCalledWith("frame_a"); }); it("forwards candidate.id to onFrameSelect for light_edit label", () => { const onFrameSelect = vi.fn(); applyFrameSelection(makeCandidate("light_edit", "frame_b"), onFrameSelect); expect(onFrameSelect).toHaveBeenCalledTimes(1); expect(onFrameSelect).toHaveBeenCalledWith("frame_b"); }); it("forwards candidate.id to onFrameSelect for restructure label", () => { const onFrameSelect = vi.fn(); applyFrameSelection(makeCandidate("restructure", "frame_c"), onFrameSelect); expect(onFrameSelect).toHaveBeenCalledTimes(1); expect(onFrameSelect).toHaveBeenCalledWith("frame_c"); }); it("forwards candidate.id to onFrameSelect for reject label — no popup, no frame swap", () => { // Reject is the silent-automation pivot case: prior IMP-47B u11 gated // this path with window.confirm; post-IMP-#84 the helper invokes // onFrameSelect with the reject frame.id directly. Backend / AI 격리 // contract handles AI 재구성 (content-only, frame preserved). const onFrameSelect = vi.fn(); applyFrameSelection(makeCandidate("reject", "frame_d"), onFrameSelect); expect(onFrameSelect).toHaveBeenCalledTimes(1); expect(onFrameSelect).toHaveBeenCalledWith("frame_d"); }); it("does not call onFrameSelect more than once per invocation", () => { const onFrameSelect = vi.fn(); applyFrameSelection(makeCandidate("reject", "frame_e"), onFrameSelect); applyFrameSelection(makeCandidate("use_as_is", "frame_f"), onFrameSelect); expect(onFrameSelect).toHaveBeenCalledTimes(2); expect(onFrameSelect).toHaveBeenNthCalledWith(1, "frame_e"); expect(onFrameSelect).toHaveBeenNthCalledWith(2, "frame_f"); }); }); describe("FramePanel.tsx source — silent-automation wiring pins (IMP-#84 u1)", () => { it("has no window.confirm(...) call (popup removed; narrative mentions in comments are allowed)", () => { // Match the call form `window.confirm(` rather than the bare substring // so that explanatory comments documenting the removed popup are not // flagged. A re-introduced call would carry an opening paren. expect(FRAME_PANEL_SOURCE).not.toMatch(/\bwindow\.confirm\s*\(/); }); it("does not embed the legacy reject-confirm Korean prompt body", () => { // Prior IMP-47B u11 string fragment; absence guards against re-introduction. expect(FRAME_PANEL_SOURCE).not.toContain("V4 reject 라벨입니다"); expect(FRAME_PANEL_SOURCE).not.toContain("계속하시겠습니까?"); }); it("wires the button onClick to handleFrameSelect(candidate)", () => { expect(FRAME_PANEL_SOURCE).toContain( "onClick={() => handleFrameSelect(candidate)}", ); }); it("delegates handleFrameSelect body to applyFrameSelection", () => { expect(FRAME_PANEL_SOURCE).toContain( "applyFrameSelection(candidate, onFrameSelect)", ); }); it("exports applyFrameSelection as a named export for caller-independent reuse", () => { expect(FRAME_PANEL_SOURCE).toMatch( /export function applyFrameSelection\(/, ); }); });