feat(#73): IMP-44 u1~u5 layout override unknown-key guard + frontend zone_geometries validation
Some checks failed
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 23s
Some checks failed
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 23s
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
222
Front/client/tests/zone_geometries_validation.test.ts
Normal file
222
Front/client/tests/zone_geometries_validation.test.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
// IMP-44 (#73) u3 — vitest coverage for `validateZoneGeometriesAgainstLayout`.
|
||||
//
|
||||
// Pairs with the backend [override-warning] guards added in u1 (1-D
|
||||
// horizontal-2 / vertical-2 branches of `build_layout_css`) and u2 (2-D
|
||||
// `_override_to_grid_tracks` call site). Same WARN+DROP unknown / KEEP known
|
||||
// contract; this helper lets handleGenerate (u4) validate against the active
|
||||
// layout before forwarding so the user sees a toast on dropped keys rather
|
||||
// than the backend silently even-splitting non-overridden zones with a false
|
||||
// `computation=user_override_geometry` signal.
|
||||
//
|
||||
// Cases (Stage 2 scope-lock):
|
||||
// 1) horizontal-2 → vertical-2 mismatch (all keys dropped)
|
||||
// 2) passthrough (all keys recognized)
|
||||
// 3) partial mix (some kept, some dropped)
|
||||
// 4) empty input ({} on a known layout)
|
||||
// 5) unknown-layout fail-safe (preset null / undefined / unknown string)
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { validateZoneGeometriesAgainstLayout } from "../src/utils/slidePlanUtils";
|
||||
|
||||
const g = (x: number, y: number, w: number, h: number) => ({ x, y, w, h });
|
||||
|
||||
describe("validateZoneGeometriesAgainstLayout (IMP-44 u3)", () => {
|
||||
// ── 1. mismatch ──────────────────────────────────────────────────────────
|
||||
it("drops horizontal-2 keys when the active layout is vertical-2", () => {
|
||||
const result = validateZoneGeometriesAgainstLayout(
|
||||
{ top: g(0, 0, 1, 0.4), bottom: g(0, 0.4, 1, 0.6) },
|
||||
"vertical-2",
|
||||
);
|
||||
expect(result.kept).toEqual({});
|
||||
expect(result.dropped).toEqual({
|
||||
top: g(0, 0, 1, 0.4),
|
||||
bottom: g(0, 0.4, 1, 0.6),
|
||||
});
|
||||
expect(result.expectedPositions).toEqual(["left", "right"]);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
it("drops vertical-2 keys when the active layout is horizontal-2", () => {
|
||||
const result = validateZoneGeometriesAgainstLayout(
|
||||
{ left: g(0, 0, 0.5, 1), right: g(0.5, 0, 0.5, 1) },
|
||||
"horizontal-2",
|
||||
);
|
||||
expect(result.kept).toEqual({});
|
||||
expect(Object.keys(result.dropped).sort()).toEqual(["left", "right"]);
|
||||
expect(result.expectedPositions).toEqual(["top", "bottom"]);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
// ── 2. passthrough ───────────────────────────────────────────────────────
|
||||
it("keeps all keys when every input key is in the active layout positions", () => {
|
||||
const input = {
|
||||
top: g(0, 0, 1, 0.4),
|
||||
bottom: g(0, 0.4, 1, 0.6),
|
||||
};
|
||||
const result = validateZoneGeometriesAgainstLayout(input, "horizontal-2");
|
||||
expect(result.kept).toEqual(input);
|
||||
expect(result.dropped).toEqual({});
|
||||
expect(result.expectedPositions).toEqual(["top", "bottom"]);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it("passes a single 'primary' key through on the 'single' preset", () => {
|
||||
const result = validateZoneGeometriesAgainstLayout(
|
||||
{ primary: g(0, 0, 1, 1) },
|
||||
"single",
|
||||
);
|
||||
expect(result.kept).toEqual({ primary: g(0, 0, 1, 1) });
|
||||
expect(result.dropped).toEqual({});
|
||||
expect(result.expectedPositions).toEqual(["primary"]);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it("recognizes the 2-D preset positions reported by computeZonePositions (top-1-bottom-2)", () => {
|
||||
const input = {
|
||||
top: g(0, 0, 1, 0.5),
|
||||
"bottom-left": g(0, 0.5, 0.5, 0.5),
|
||||
"bottom-right": g(0.5, 0.5, 0.5, 0.5),
|
||||
};
|
||||
const result = validateZoneGeometriesAgainstLayout(input, "top-1-bottom-2");
|
||||
expect(result.kept).toEqual(input);
|
||||
expect(result.dropped).toEqual({});
|
||||
expect(result.expectedPositions).toEqual([
|
||||
"top",
|
||||
"bottom-left",
|
||||
"bottom-right",
|
||||
]);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
// ── 3. partial mix ───────────────────────────────────────────────────────
|
||||
it("keeps known keys and drops unknown keys on a partial-mix input", () => {
|
||||
const result = validateZoneGeometriesAgainstLayout(
|
||||
{ top: g(0, 0, 1, 0.4), foo: g(0, 0, 1, 1) },
|
||||
"horizontal-2",
|
||||
);
|
||||
expect(result.kept).toEqual({ top: g(0, 0, 1, 0.4) });
|
||||
expect(result.dropped).toEqual({ foo: g(0, 0, 1, 1) });
|
||||
expect(result.expectedPositions).toEqual(["top", "bottom"]);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
it("on a 2-D preset, keeps known 2-D track keys and drops legacy 1-D keys", () => {
|
||||
// Simulates the user resizing under top-1-bottom-2, then flipping to
|
||||
// grid-2x2 — legacy `bottom-left` stays valid; `top` (no longer a 2x2
|
||||
// position) gets dropped.
|
||||
const result = validateZoneGeometriesAgainstLayout(
|
||||
{
|
||||
top: g(0, 0, 1, 0.5),
|
||||
"bottom-left": g(0, 0.5, 0.5, 0.5),
|
||||
"top-left": g(0, 0, 0.5, 0.5),
|
||||
},
|
||||
"grid-2x2",
|
||||
);
|
||||
expect(result.kept).toEqual({
|
||||
"bottom-left": g(0, 0.5, 0.5, 0.5),
|
||||
"top-left": g(0, 0, 0.5, 0.5),
|
||||
});
|
||||
expect(result.dropped).toEqual({ top: g(0, 0, 1, 0.5) });
|
||||
expect(result.expectedPositions).toEqual([
|
||||
"top-left",
|
||||
"top-right",
|
||||
"bottom-left",
|
||||
"bottom-right",
|
||||
]);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
// ── 4. empty input ───────────────────────────────────────────────────────
|
||||
it("returns empty kept/dropped and valid=true on an empty {} input", () => {
|
||||
const result = validateZoneGeometriesAgainstLayout({}, "horizontal-2");
|
||||
expect(result.kept).toEqual({});
|
||||
expect(result.dropped).toEqual({});
|
||||
expect(result.expectedPositions).toEqual(["top", "bottom"]);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it("treats null / undefined geoms as empty input (no throw, valid=true on a known layout)", () => {
|
||||
const nullResult = validateZoneGeometriesAgainstLayout(null, "vertical-2");
|
||||
expect(nullResult.kept).toEqual({});
|
||||
expect(nullResult.dropped).toEqual({});
|
||||
expect(nullResult.expectedPositions).toEqual(["left", "right"]);
|
||||
expect(nullResult.valid).toBe(true);
|
||||
|
||||
const undefResult = validateZoneGeometriesAgainstLayout(
|
||||
undefined,
|
||||
"vertical-2",
|
||||
);
|
||||
expect(undefResult.kept).toEqual({});
|
||||
expect(undefResult.dropped).toEqual({});
|
||||
expect(undefResult.expectedPositions).toEqual(["left", "right"]);
|
||||
expect(undefResult.valid).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores array payloads (defensive against hand-edited persisted files)", () => {
|
||||
const result = validateZoneGeometriesAgainstLayout(
|
||||
[] as unknown as Record<string, { x: number; y: number; w: number; h: number }>,
|
||||
"horizontal-2",
|
||||
);
|
||||
expect(result.kept).toEqual({});
|
||||
expect(result.dropped).toEqual({});
|
||||
expect(result.expectedPositions).toEqual(["top", "bottom"]);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
// ── 5. unknown-layout fail-safe ──────────────────────────────────────────
|
||||
it("drops every input key when layout is null (fail-safe)", () => {
|
||||
const result = validateZoneGeometriesAgainstLayout(
|
||||
{ top: g(0, 0, 1, 0.4), bottom: g(0, 0.4, 1, 0.6) },
|
||||
null,
|
||||
);
|
||||
expect(result.kept).toEqual({});
|
||||
expect(result.dropped).toEqual({
|
||||
top: g(0, 0, 1, 0.4),
|
||||
bottom: g(0, 0.4, 1, 0.6),
|
||||
});
|
||||
expect(result.expectedPositions).toEqual([]);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
it("drops every input key when layout is undefined (fail-safe)", () => {
|
||||
const result = validateZoneGeometriesAgainstLayout(
|
||||
{ primary: g(0, 0, 1, 1) },
|
||||
undefined,
|
||||
);
|
||||
expect(result.kept).toEqual({});
|
||||
expect(result.dropped).toEqual({ primary: g(0, 0, 1, 1) });
|
||||
expect(result.expectedPositions).toEqual([]);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
it("drops every input key when layout is an unknown preset string (fail-safe)", () => {
|
||||
const result = validateZoneGeometriesAgainstLayout(
|
||||
{ top: g(0, 0, 1, 0.4) },
|
||||
"rogue-preset" as unknown as string,
|
||||
);
|
||||
expect(result.kept).toEqual({});
|
||||
expect(result.dropped).toEqual({ top: g(0, 0, 1, 0.4) });
|
||||
expect(result.expectedPositions).toEqual([]);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
it("returns empty kept/dropped/expectedPositions when layout is unknown AND geoms is empty", () => {
|
||||
const result = validateZoneGeometriesAgainstLayout({}, null);
|
||||
expect(result.kept).toEqual({});
|
||||
expect(result.dropped).toEqual({});
|
||||
expect(result.expectedPositions).toEqual([]);
|
||||
// No keys to drop ⇒ vacuously valid; handleGenerate (u4) gates the toast
|
||||
// on `Object.keys(dropped).length > 0`, not `valid`, so this is safe.
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
// ── purity / mutation safety ─────────────────────────────────────────────
|
||||
it("does not mutate the input geometries object", () => {
|
||||
const input = { top: g(0, 0, 1, 0.4), foo: g(0, 0, 1, 1) };
|
||||
const inputKeysBefore = Object.keys(input).sort();
|
||||
validateZoneGeometriesAgainstLayout(input, "horizontal-2");
|
||||
expect(Object.keys(input).sort()).toEqual(inputKeysBefore);
|
||||
// Sample value still pristine.
|
||||
expect(input.top).toEqual(g(0, 0, 1, 0.4));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user