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>
151 lines
6.3 KiB
TypeScript
151 lines
6.3 KiB
TypeScript
// IMP-90 (#90) u14 — vitest coverage for the pure helpers exported by
|
|
// `StructureEditOverlay`. The React component itself is not rendered
|
|
// (jsdom / @testing-library NOT in Front devDependencies — verified in
|
|
// `Front/package.json`); we test the deterministic pieces that drive its
|
|
// JSX: `resolveEffectiveSlotOrder` (effective-order resolution under
|
|
// override) and `moveItem` (immutable reorder primitive).
|
|
//
|
|
// Upstream / downstream contracts (verified by prior units):
|
|
// - u2 KNOWN_AXES += structure_overrides (Python backend).
|
|
// - u3 vite allowlist += structure_overrides.
|
|
// - u6 structure_override_resolver — inner shape locked to
|
|
// {slot_order, hidden_slots}; frame swap REJECTED to existing
|
|
// frames axis.
|
|
// - u10 typed-client `StructureOverridePerZone` + extract helper.
|
|
// - u15 (next) will debounce + PUT the emitted capture.
|
|
//
|
|
// u14 scope: pure helpers only. React render path is verified by Codex
|
|
// auditor via static read of the JSX (no runtime test possible without
|
|
// jsdom). Tests below are intentionally side-effect-free.
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
import {
|
|
resolveEffectiveSlotOrder,
|
|
moveItem,
|
|
} from "../src/components/StructureEditOverlay";
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
// resolveEffectiveSlotOrder
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
describe("resolveEffectiveSlotOrder — no override", () => {
|
|
it("returns a fresh copy of the discovered keys when slotOrder is undefined", () => {
|
|
const discovered = ["a", "b", "c"];
|
|
const out = resolveEffectiveSlotOrder(discovered, undefined);
|
|
expect(out).toEqual(["a", "b", "c"]);
|
|
expect(out).not.toBe(discovered);
|
|
});
|
|
it("returns a fresh copy when slotOrder is null", () => {
|
|
const out = resolveEffectiveSlotOrder(["a", "b"], null);
|
|
expect(out).toEqual(["a", "b"]);
|
|
});
|
|
it("returns a fresh copy when slotOrder is empty []", () => {
|
|
const out = resolveEffectiveSlotOrder(["a", "b"], []);
|
|
expect(out).toEqual(["a", "b"]);
|
|
});
|
|
it("handles empty discovered list (no slots in zone)", () => {
|
|
expect(resolveEffectiveSlotOrder([], undefined)).toEqual([]);
|
|
expect(resolveEffectiveSlotOrder([], ["x"])).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("resolveEffectiveSlotOrder — full override", () => {
|
|
it("reorders all discovered keys per slotOrder", () => {
|
|
expect(
|
|
resolveEffectiveSlotOrder(["a", "b", "c"], ["c", "a", "b"]),
|
|
).toEqual(["c", "a", "b"]);
|
|
});
|
|
it("is idempotent when slotOrder matches discovered order", () => {
|
|
expect(
|
|
resolveEffectiveSlotOrder(["a", "b", "c"], ["a", "b", "c"]),
|
|
).toEqual(["a", "b", "c"]);
|
|
});
|
|
});
|
|
|
|
describe("resolveEffectiveSlotOrder — partial / drift override", () => {
|
|
it("appends missing discovered keys in backend order at the tail", () => {
|
|
// user reordered b -> first, but c was added later by backend.
|
|
expect(
|
|
resolveEffectiveSlotOrder(["a", "b", "c"], ["b", "a"]),
|
|
).toEqual(["b", "a", "c"]);
|
|
});
|
|
it("drops override entries that no longer exist in discovered keys", () => {
|
|
// user had slot 'x' before; backend dropped it.
|
|
expect(
|
|
resolveEffectiveSlotOrder(["a", "b"], ["x", "a", "b"]),
|
|
).toEqual(["a", "b"]);
|
|
});
|
|
it("dedupes duplicate entries within slotOrder", () => {
|
|
expect(
|
|
resolveEffectiveSlotOrder(["a", "b", "c"], ["a", "a", "b"]),
|
|
).toEqual(["a", "b", "c"]);
|
|
});
|
|
it("dedupe + drop + append all together (stress)", () => {
|
|
expect(
|
|
resolveEffectiveSlotOrder(
|
|
["a", "b", "c", "d"],
|
|
["d", "x", "d", "a", "ghost"],
|
|
),
|
|
).toEqual(["d", "a", "b", "c"]);
|
|
});
|
|
it("ignores non-string entries in slotOrder", () => {
|
|
const bogus = ["a", null as unknown as string, undefined as unknown as string, "b"];
|
|
expect(resolveEffectiveSlotOrder(["a", "b"], bogus)).toEqual(["a", "b"]);
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
// moveItem
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
describe("moveItem — happy paths", () => {
|
|
it("moves index 0 down by 1 (swap with index 1)", () => {
|
|
expect(moveItem(["a", "b", "c"], 0, 1)).toEqual(["b", "a", "c"]);
|
|
});
|
|
it("moves index 2 up by 1 (swap with index 1)", () => {
|
|
expect(moveItem(["a", "b", "c"], 2, -1)).toEqual(["a", "c", "b"]);
|
|
});
|
|
it("moves across larger delta (swap with target)", () => {
|
|
expect(moveItem(["a", "b", "c", "d"], 0, 2)).toEqual(["c", "b", "a", "d"]);
|
|
});
|
|
});
|
|
|
|
describe("moveItem — bounds", () => {
|
|
it("no-op (fresh copy) when moving first up", () => {
|
|
const src = ["a", "b", "c"];
|
|
const out = moveItem(src, 0, -1);
|
|
expect(out).toEqual(["a", "b", "c"]);
|
|
expect(out).not.toBe(src);
|
|
});
|
|
it("no-op when moving last down", () => {
|
|
expect(moveItem(["a", "b", "c"], 2, 1)).toEqual(["a", "b", "c"]);
|
|
});
|
|
it("no-op when index negative", () => {
|
|
expect(moveItem(["a", "b"], -1, 1)).toEqual(["a", "b"]);
|
|
});
|
|
it("no-op when index past end", () => {
|
|
expect(moveItem(["a", "b"], 5, -1)).toEqual(["a", "b"]);
|
|
});
|
|
it("no-op when target falls out of range from large delta", () => {
|
|
expect(moveItem(["a", "b", "c"], 1, 99)).toEqual(["a", "b", "c"]);
|
|
});
|
|
it("no-op on empty array (any index)", () => {
|
|
expect(moveItem<string>([], 0, 1)).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("moveItem — immutability", () => {
|
|
it("never mutates the input array", () => {
|
|
const src = ["a", "b", "c"];
|
|
moveItem(src, 0, 1);
|
|
expect(src).toEqual(["a", "b", "c"]);
|
|
});
|
|
it("returns a new reference even when no-op", () => {
|
|
const src = ["a", "b"];
|
|
expect(moveItem(src, 0, -1)).not.toBe(src);
|
|
});
|
|
it("preserves T-typed values (number array)", () => {
|
|
expect(moveItem([1, 2, 3], 0, 1)).toEqual([2, 1, 3]);
|
|
});
|
|
});
|