feat(#79): IMP-51 image_overrides axis (u1~u11 backend stamp+CLI+CSS inject + frontend drag/resize+persistence + tests)

This commit is contained in:
2026-05-22 21:54:38 +09:00
parent bd8bcf748b
commit 6f1c7367e0
18 changed files with 2311 additions and 32 deletions

View File

@@ -480,6 +480,82 @@ describe("UserOverridesPartial type (IMP-52 u5)", () => {
const b: UserOverridesPartial = { layout: null };
const c: UserOverridesPartial = { frames: { unit: "tmpl" } };
const d: UserOverridesPartial = {};
expect([a, b, c, d]).toHaveLength(4);
const e: UserOverridesPartial = {
image_overrides: { "img-1": { x: 10, y: 20, w: 30, h: 25 } },
};
const f: UserOverridesPartial = { image_overrides: null };
expect([a, b, c, d, e, f]).toHaveLength(6);
});
});
// ============================================================================
// IMP-51 #79 u3 — image_overrides axis (5th axis) parity coverage
//
// Same debounce / coalescing / clear / per-key isolation guarantees as the
// 4 sibling axes (layout / frames / zone_geometries / zone_sections), but
// asserted explicitly so a regression in the type or the runtime allowlist
// fails here instead of in a downstream u8~u11 handler.
// ============================================================================
describe("saveUserOverrides (IMP-51 #79 u3) — image_overrides axis", () => {
it("PUT body carries only image_overrides when that is the sole mutated axis", async () => {
fetchMock.mockResolvedValue(mockResponse({}));
void saveUserOverrides("03", {
image_overrides: { "img-1": { x: 10, y: 20, w: 30, h: 25 } },
});
vi.advanceTimersByTime(300);
await drainMicrotasks();
const body = lastPutBody() as Record<string, unknown>;
expect(Object.keys(body)).toEqual(["image_overrides"]);
expect(body.image_overrides).toEqual({
"img-1": { x: 10, y: 20, w: 30, h: 25 },
});
expect("layout" in body).toBe(false);
expect("frames" in body).toBe(false);
expect("zone_geometries" in body).toBe(false);
expect("zone_sections" in body).toBe(false);
});
it("per-axis later-wins: same image_id mutated twice keeps the LAST value", async () => {
fetchMock.mockResolvedValue(mockResponse({}));
void saveUserOverrides("03", {
image_overrides: { "img-1": { x: 0, y: 0, w: 50, h: 50 } },
});
void saveUserOverrides("03", {
image_overrides: { "img-1": { x: 25, y: 25, w: 30, h: 30 } },
});
vi.advanceTimersByTime(300);
await drainMicrotasks();
expect(putCallsCount()).toBe(1);
expect(lastPutBody()).toEqual({
image_overrides: { "img-1": { x: 25, y: 25, w: 30, h: 30 } },
});
});
it("forwards null sentinel verbatim (clear all image_overrides on disk)", async () => {
fetchMock.mockResolvedValue(mockResponse({}));
void saveUserOverrides("03", { image_overrides: null });
vi.advanceTimersByTime(300);
await drainMicrotasks();
expect(lastPutBody()).toEqual({ image_overrides: null });
});
it("coalesces with sibling axes in a single PUT", async () => {
fetchMock.mockResolvedValue(mockResponse({}));
void saveUserOverrides("03", { layout: "two_zone_split" });
void saveUserOverrides("03", {
image_overrides: { "img-1": { x: 10, y: 20, w: 30, h: 25 } },
});
vi.advanceTimersByTime(300);
await drainMicrotasks();
expect(putCallsCount()).toBe(1);
expect(lastPutBody()).toEqual({
layout: "two_zone_split",
image_overrides: { "img-1": { x: 10, y: 20, w: 30, h: 25 } },
});
});
});