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>
220 lines
8.8 KiB
TypeScript
220 lines
8.8 KiB
TypeScript
// IMP-90 (#90) u12 — vitest coverage for `computeEditModeGates`, the pure
|
|
// helper that drives SlideCanvas's mutually-exclusive gesture gating.
|
|
// u11 introduced the `EditMode` enum + toolbar; u12 splits the prior
|
|
// `isEditMode` shim (which fired ALL gates whenever any edit mode was
|
|
// active) into 5 per-gate booleans:
|
|
// textEditing — designMode + contentEditable (text mode only).
|
|
// imageSelection — in-iframe user-content image click listener
|
|
// (image-zone mode only).
|
|
// iframePointerAuto — iframe pointer-events:auto so in-iframe gestures
|
|
// (text caret OR image click) can reach the doc.
|
|
// text mode + image-zone mode; structure stays
|
|
// pe:none because u14 will overlay React controls.
|
|
// zoneGestures — zone resize 8-handle ring + drag perimeter strips
|
|
// + canDrag in handleZoneMouseDown
|
|
// (image-zone mode only).
|
|
// imageOverlay — React-side image edit overlay (image-zone only).
|
|
//
|
|
// Mutually-exclusive contract (from the issue body's "discriminated edit
|
|
// mode"): no editMode value enables both `textEditing` and either
|
|
// `imageSelection` or `zoneGestures` simultaneously. structure mode is
|
|
// the no-op placeholder — u14 will plant the structure overlay there.
|
|
// pendingLayout fully suppresses every gate (mirrors the existing
|
|
// useEffect that forces editMode='off' on pendingLayout entry).
|
|
//
|
|
// Scope guard: this test exercises the pure helper only — no React
|
|
// rendering, no DOM. testing-library/react is NOT in devDependencies
|
|
// (verified in Front/package.json); helper-level coverage is the
|
|
// established u11 pattern.
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
import {
|
|
computeEditModeGates,
|
|
type EditMode,
|
|
type EditModeGates,
|
|
} from "../src/components/SlideCanvas";
|
|
|
|
const ALL_MODES: EditMode[] = ["off", "text", "structure", "image-zone"];
|
|
|
|
describe("computeEditModeGates (IMP-90 u12) — pendingLayout suppression", () => {
|
|
it.each<EditMode>(ALL_MODES)(
|
|
"pendingLayout=true forces every gate false (editMode=%s)",
|
|
(mode) => {
|
|
const g = computeEditModeGates(mode, true);
|
|
expect(g).toEqual<EditModeGates>({
|
|
textEditing: false,
|
|
imageSelection: false,
|
|
iframePointerAuto: false,
|
|
zoneGestures: false,
|
|
imageOverlay: false,
|
|
});
|
|
}
|
|
);
|
|
});
|
|
|
|
describe("computeEditModeGates (IMP-90 u12) — off baseline", () => {
|
|
it("editMode=off pendingLayout=false: every gate false", () => {
|
|
expect(computeEditModeGates("off", false)).toEqual<EditModeGates>({
|
|
textEditing: false,
|
|
imageSelection: false,
|
|
iframePointerAuto: false,
|
|
zoneGestures: false,
|
|
imageOverlay: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("computeEditModeGates (IMP-90 u12) — text mode", () => {
|
|
const g = computeEditModeGates("text", false);
|
|
|
|
it("textEditing = true (designMode + contentEditable activate)", () => {
|
|
expect(g.textEditing).toBe(true);
|
|
});
|
|
it("iframePointerAuto = true (caret needs to reach the doc)", () => {
|
|
expect(g.iframePointerAuto).toBe(true);
|
|
});
|
|
it("imageSelection = false (no in-iframe image click listener)", () => {
|
|
expect(g.imageSelection).toBe(false);
|
|
});
|
|
it("zoneGestures = false (no zone resize / drag affordances)", () => {
|
|
expect(g.zoneGestures).toBe(false);
|
|
});
|
|
it("imageOverlay = false (no React-side image overlay)", () => {
|
|
expect(g.imageOverlay).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("computeEditModeGates (IMP-90 u12) — structure mode", () => {
|
|
const g = computeEditModeGates("structure", false);
|
|
|
|
// structure mode is the u14 placeholder — no gestures here yet. All five
|
|
// gates stay false so the iframe and React overlays remain quiescent
|
|
// until u14 plants the structure overlay on the React layer.
|
|
it("every gate false (u14 will plant the structure overlay later)", () => {
|
|
expect(g).toEqual<EditModeGates>({
|
|
textEditing: false,
|
|
imageSelection: false,
|
|
iframePointerAuto: false,
|
|
zoneGestures: false,
|
|
imageOverlay: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("computeEditModeGates (IMP-90 u12) — image-zone mode", () => {
|
|
const g = computeEditModeGates("image-zone", false);
|
|
|
|
it("textEditing = false (contentEditable would steal image clicks)", () => {
|
|
expect(g.textEditing).toBe(false);
|
|
});
|
|
it("imageSelection = true (in-iframe img click → selectedImageId)", () => {
|
|
expect(g.imageSelection).toBe(true);
|
|
});
|
|
it("iframePointerAuto = true (so image clicks reach the doc)", () => {
|
|
expect(g.iframePointerAuto).toBe(true);
|
|
});
|
|
it("zoneGestures = true (zone resize + drag affordances visible)", () => {
|
|
expect(g.zoneGestures).toBe(true);
|
|
});
|
|
it("imageOverlay = true (React-side overlay renders the drag handles)", () => {
|
|
expect(g.imageOverlay).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("computeEditModeGates (IMP-90 u12) — mutually exclusive contract", () => {
|
|
it("text mode never co-activates image-zone gates (imageSelection / zoneGestures / imageOverlay)", () => {
|
|
const g = computeEditModeGates("text", false);
|
|
expect(g.textEditing).toBe(true);
|
|
expect(g.imageSelection).toBe(false);
|
|
expect(g.zoneGestures).toBe(false);
|
|
expect(g.imageOverlay).toBe(false);
|
|
});
|
|
|
|
it("image-zone mode never co-activates text gates (textEditing)", () => {
|
|
const g = computeEditModeGates("image-zone", false);
|
|
expect(g.imageSelection).toBe(true);
|
|
expect(g.textEditing).toBe(false);
|
|
});
|
|
|
|
it.each<EditMode>(ALL_MODES)(
|
|
"for every editMode (%s), textEditing AND zoneGestures are NEVER both true",
|
|
(mode) => {
|
|
const g = computeEditModeGates(mode, false);
|
|
expect(g.textEditing && g.zoneGestures).toBe(false);
|
|
}
|
|
);
|
|
|
|
it.each<EditMode>(ALL_MODES)(
|
|
"for every editMode (%s), textEditing AND imageOverlay are NEVER both true",
|
|
(mode) => {
|
|
const g = computeEditModeGates(mode, false);
|
|
expect(g.textEditing && g.imageOverlay).toBe(false);
|
|
}
|
|
);
|
|
|
|
it.each<EditMode>(ALL_MODES)(
|
|
"for every editMode (%s), textEditing AND imageSelection are NEVER both true",
|
|
(mode) => {
|
|
const g = computeEditModeGates(mode, false);
|
|
expect(g.textEditing && g.imageSelection).toBe(false);
|
|
}
|
|
);
|
|
});
|
|
|
|
describe("computeEditModeGates (IMP-90 u12) — iframePointerAuto coupling", () => {
|
|
// pe:auto is the iframe-side prerequisite for ANY in-iframe gesture
|
|
// (text caret OR image click). The helper must NOT advertise an
|
|
// in-iframe gate as active while pe is none, or those gestures would
|
|
// be silently swallowed by the wrapper.
|
|
it.each<EditMode>(ALL_MODES)(
|
|
"textEditing → iframePointerAuto (editMode=%s)",
|
|
(mode) => {
|
|
const g = computeEditModeGates(mode, false);
|
|
if (g.textEditing) expect(g.iframePointerAuto).toBe(true);
|
|
}
|
|
);
|
|
it.each<EditMode>(ALL_MODES)(
|
|
"imageSelection → iframePointerAuto (editMode=%s)",
|
|
(mode) => {
|
|
const g = computeEditModeGates(mode, false);
|
|
if (g.imageSelection) expect(g.iframePointerAuto).toBe(true);
|
|
}
|
|
);
|
|
});
|
|
|
|
describe("computeEditModeGates (IMP-90 u12) — referential transparency", () => {
|
|
it("multiple calls with the same inputs return equal output", () => {
|
|
const a = computeEditModeGates("image-zone", false);
|
|
const b = computeEditModeGates("image-zone", false);
|
|
const c = computeEditModeGates("image-zone", false);
|
|
expect(a).toEqual(b);
|
|
expect(b).toEqual(c);
|
|
});
|
|
|
|
it("does not mutate captured state across calls (independent invocations)", () => {
|
|
const a = computeEditModeGates("text", false);
|
|
const _b = computeEditModeGates("image-zone", false);
|
|
// a must still reflect text mode after b's call.
|
|
expect(a.textEditing).toBe(true);
|
|
expect(a.imageSelection).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("computeEditModeGates (IMP-90 u12) — gate truthtable snapshot", () => {
|
|
// Snapshot for human-readable inspection — the per-mode flag layout
|
|
// is the contract u13 (text capture) and u14 (structure overlay)
|
|
// will build against. Any change requires updating both this test
|
|
// AND the consuming gates in SlideCanvas.tsx.
|
|
it("non-pendingLayout truthtable matches the u12 contract", () => {
|
|
const rows = (["off", "text", "structure", "image-zone"] as EditMode[]).map(
|
|
(m) => ({ mode: m, ...computeEditModeGates(m, false) })
|
|
);
|
|
expect(rows).toEqual([
|
|
{ mode: "off", textEditing: false, imageSelection: false, iframePointerAuto: false, zoneGestures: false, imageOverlay: false },
|
|
{ mode: "text", textEditing: true, imageSelection: false, iframePointerAuto: true, zoneGestures: false, imageOverlay: false },
|
|
{ mode: "structure", textEditing: false, imageSelection: false, iframePointerAuto: false, zoneGestures: false, imageOverlay: false },
|
|
{ mode: "image-zone", textEditing: false, imageSelection: true, iframePointerAuto: true, zoneGestures: true, imageOverlay: true },
|
|
]);
|
|
});
|
|
});
|