// IMP-90 (#90) u11 — vitest coverage for the discriminated EditMode enum // and its pure transition helper `nextEditMode`. Replaces the prior single // `isEditMode` boolean state. u11 introduces ONLY the state surface + the // toolbar UI; gesture gating per mode is u12 (mutually exclusive) and must // not regress this contract. // // Scope (Stage 2 unit u11 contract): // 1) EDIT_MODES is the canonical ['text','structure','image-zone'] list // in toolbar render order. 'off' is intentionally excluded from the // iterable because it is the implicit baseline (no button); the // toolbar only renders the three active modes per the u11 design. // 2) nextEditMode is a pure (current, requested) -> EditMode mapping // with three rules: // - requested === 'off' -> 'off' (explicit exit) // - requested === current -> 'off' (toggle exit) // - requested !== current && != 'off'-> requested (mode switch) // 3) The helper is referentially transparent — no side effects, no // React, no useState, no DOM. SlideCanvas wires it as the useState // updater callback (`setEditMode((prev) => nextEditMode(prev, m))`), // so covering the helper here covers every toolbar click outcome // directly without DOM rendering. (@testing-library/react is NOT in // devDependencies; this mirrors the imp47b_human_review_toast pattern.) // 4) The exported EditMode type union must contain exactly the four // members 'off' | 'text' | 'structure' | 'image-zone'. The runtime // EDIT_MODES list intentionally excludes 'off' (see (1) above). // // Forward-compat note: u12 will discriminate per-mode gating but MUST NOT // alter the (current, requested) -> next contract verified here. Any // change to the toggle/switch/exit semantics is a scope-violation against // the u11 binding contract. import { describe, it, expect } from "vitest"; import { EDIT_MODES, nextEditMode, type EditMode, } from "../src/components/SlideCanvas"; describe("EDIT_MODES (IMP-90 u11 — toolbar render order)", () => { it("contains exactly the three active modes in toolbar order", () => { expect(EDIT_MODES).toEqual(["text", "structure", "image-zone"]); }); it("excludes 'off' — baseline is implicit, no toolbar button", () => { expect(EDIT_MODES).not.toContain("off" as EditMode); }); it("has length 3", () => { expect(EDIT_MODES.length).toBe(3); }); }); describe("nextEditMode (IMP-90 u11 — pure transition helper)", () => { describe("explicit 'off' request always exits", () => { it.each(["off", "text", "structure", "image-zone"])( "current=%s, requested=off -> off", (current) => { expect(nextEditMode(current, "off")).toBe("off"); } ); }); describe("clicking the active mode toggles back to 'off'", () => { it.each(["text", "structure", "image-zone"])( "current=%s, requested=%s -> off", (mode) => { expect(nextEditMode(mode, mode)).toBe("off"); } ); }); describe("clicking a different mode switches", () => { const cases: Array<[EditMode, EditMode]> = [ ["off", "text"], ["off", "structure"], ["off", "image-zone"], ["text", "structure"], ["text", "image-zone"], ["structure", "text"], ["structure", "image-zone"], ["image-zone", "text"], ["image-zone", "structure"], ]; it.each(cases)("current=%s, requested=%s -> requested", (current, requested) => { expect(nextEditMode(current, requested)).toBe(requested); }); }); it("is referentially transparent — multiple calls with same inputs return same output", () => { const a = nextEditMode("text", "structure"); const b = nextEditMode("text", "structure"); const c = nextEditMode("text", "structure"); expect(a).toBe("structure"); expect(b).toBe("structure"); expect(c).toBe("structure"); }); it("never returns a value outside the EditMode union", () => { const all: EditMode[] = ["off", "text", "structure", "image-zone"]; for (const current of all) { for (const requested of all) { const result = nextEditMode(current, requested); expect(all).toContain(result); } } }); it("preserves toggle semantics under repeated identical clicks", () => { // off -> text -> off -> text -> off (toggle behavior) let m: EditMode = "off"; m = nextEditMode(m, "text"); expect(m).toBe("text"); m = nextEditMode(m, "text"); expect(m).toBe("off"); m = nextEditMode(m, "text"); expect(m).toBe("text"); m = nextEditMode(m, "text"); expect(m).toBe("off"); }); it("preserves switch semantics across distinct mode clicks", () => { // off -> text -> structure -> image-zone -> off (via toggle) let m: EditMode = "off"; m = nextEditMode(m, "text"); expect(m).toBe("text"); m = nextEditMode(m, "structure"); expect(m).toBe("structure"); m = nextEditMode(m, "image-zone"); expect(m).toBe("image-zone"); m = nextEditMode(m, "image-zone"); expect(m).toBe("off"); }); });