Some checks failed
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 20s
u1: text_overrides axis in user_overrides_io u2: structure_overrides axis in user_overrides_io u3: vite allowlist for new endpoints u4: text_override_resolver u5: Step 12 text_overrides apply in phase_z2_pipeline u6: structure_override_resolver u7: text_path_stamper u8: SlideCanvas text-edit capture u9: SlideCanvas structure-edit overlay u10: userOverridesApi service extension u11: designAgent types extension u12: slidePlanUtils restore u13: user_overrides endpoint tests u14: user_overrides restore tests u15: pipeline fallback tests u16: edit-mode state + gating tests u17: slide_base print mode CSS u18: /api/connect endpoint (vite) u19: /api/export endpoint (vite) Recovery scope: 29 files (12 modified + 17 new). u20 already pushed in 9439575; this commit lands u1-u19 that were authored but not committed before #90 was externally closed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
134 lines
5.0 KiB
TypeScript
134 lines
5.0 KiB
TypeScript
// 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<EditMode>(["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<EditMode>(["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");
|
|
});
|
|
});
|