import { mediaSourceModule } from "../modules/built-ins/media-source/media-source-module"; import { describe, expect, it } from "vitest"; import { defineToolcraft } from "../schema/define-toolcraft"; import { createToolcraftState } from "./create-template-state"; import { isToolcraftCanvasStateHistoryPatch, isToolcraftControlsResetHistoryPatch, } from "./history-patch-metadata"; import { toolcraftReducer } from "../composition/public-state"; import { importImageMedia } from "./reducer-media-test-utils"; function createResetState(defaultBackedCollisions = false) { return createToolcraftState( defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft fixture", }, canvas: { enabled: true, upload: true, size: { width: 1024, height: 768, unit: "px" }, }, panels: { controls: { title: "Controls", sections: [ { id: "fixture-section-1", controls: { amount: { applicability: { mode: "always" as const }, type: "slider", target: "amount", defaultValue: 50, }, source: { applicability: { mode: "always" as const }, type: "fileDrop", target: "media.source", defaultValue: null, }, ...(defaultBackedCollisions ? { layerCount: { applicability: { mode: "always" as const }, type: "slider" as const, target: "layers", defaultValue: 50, }, timing: { applicability: { mode: "always" as const }, type: "slider" as const, target: "timeline", defaultValue: 50, }, } : {}), }, }, ], }, }, }, modules: [mediaSourceModule()], }), { values: { amount: 75, "composition.layout": { x: 120 }, timeline: defaultBackedCollisions ? 75 : { productOnly: true }, layers: defaultBackedCollisions ? 75 : { productOnly: true }, }, }, ); } describe("Reset preserves non-default product values", () => { it.each([ { defaultBacked: false, canvas: false }, { defaultBacked: false, canvas: true }, { defaultBacked: true, canvas: false }, { defaultBacked: true, canvas: true }, ])( "isolates section Reset values (default-backed: $defaultBacked, canvas: $canvas)", ({ defaultBacked, canvas }) => { const initial = createResetState(defaultBacked); const imported = importImageMedia(initial, { sourceTarget: "media.source", }); const resized = canvas ? toolcraftReducer(imported, { type: "canvas.setSize", size: { width: 640, height: 480, unit: "px" }, }) : imported; const changed = toolcraftReducer(resized, { type: "controls.setValue", target: "layers", value: initial.values.layers, }); const reset = toolcraftReducer(changed, { type: "controls.resetTargets", targets: [ "amount", "media.source", "layers", "timeline", ...(canvas ? ["canvas.size.width"] : []), ], }); const undone = toolcraftReducer(reset, { type: "history.undo" }); const redone = toolcraftReducer(undone, { type: "history.redo" }); expect( isToolcraftControlsResetHistoryPatch(reset.history.undo.at(-1)), ).toBe(false); expect( isToolcraftCanvasStateHistoryPatch(reset.history.undo.at(-1)), ).toBe(canvas); for (const [state, amount] of [ [reset, 50], [undone, 75], [redone, 50], ] as const) { expect(state.values.amount).toBe(amount); expect(state.values.layers).toBe( defaultBacked ? amount : initial.values.layers, ); expect(state.values.timeline).toBe( defaultBacked ? amount : initial.values.timeline, ); expect(state.values["composition.layout"]).toBe( initial.values["composition.layout"], ); expect(state.timeline).toBe(initial.timeline); expect(Array.isArray(state.layers)).toBe(true); } expect(reset.mediaAssets).toEqual([]); expect(undone.mediaAssets).toEqual(changed.mediaAssets); expect(redone.mediaAssets).toEqual([]); }, ); it.each([false, true])( "isolates default-backed slice-name controls in Reset history (media: %s)", (withMedia) => { const initial = createResetState(true); const seeded = withMedia ? importImageMedia(initial, { sourceTarget: "media.source" }) : initial; const changed = toolcraftReducer(seeded, { type: "controls.setValue", target: "layers", value: 75, }); const reset = toolcraftReducer(changed, { type: "controls.reset" }); const undone = toolcraftReducer(reset, { type: "history.undo" }); const redone = toolcraftReducer(undone, { type: "history.redo" }); for (const [state, value] of [ [reset, 50], [undone, 75], [redone, 50], ] as const) { expect(state.values.layers).toBe(value); expect(state.values.timeline).toBe(value); expect(state.timeline).toBe(initial.timeline); expect(Array.isArray(state.layers)).toBe(true); expect(state.values["composition.layout"]).toBe( initial.values["composition.layout"], ); } expect(reset.layers).toEqual([]); expect(undone.layers).toEqual(changed.layers); expect(undone.mediaAssets).toEqual(changed.mediaAssets); expect(redone.layers).toEqual([]); expect(redone.mediaAssets).toEqual([]); }, ); it("keeps additional data through value-only Reset, Undo, and Redo", () => { const initial = createResetState(); const reset = toolcraftReducer(initial, { type: "controls.reset" }); const undone = toolcraftReducer(reset, { type: "history.undo" }); const redone = toolcraftReducer(undone, { type: "history.redo" }); const patch = reset.history.undo.at(-1)!; expect(isToolcraftControlsResetHistoryPatch(patch)).toBe(true); for (const side of [patch.before, patch.after]) { expect(Object.keys(side).sort()).toEqual( Object.keys(initial.defaults).sort(), ); expect(side).not.toHaveProperty("composition.layout"); expect(side).not.toHaveProperty("timeline"); expect(side).not.toHaveProperty("layers"); } for (const [state, amount] of [ [reset, 50], [undone, 75], [redone, 50], ] as const) { expect(state.values.amount).toBe(amount); expect(state.values["composition.layout"]).toBe( initial.values["composition.layout"], ); expect(state.values.timeline).toBe(initial.values.timeline); expect(state.values.layers).toBe(initial.values.layers); expect(state.timeline).toBe(initial.timeline); expect(state.layers).toBe(initial.layers); } expect(undone.history.redo.at(-1)).toBe(patch); expect(redone.history.undo.at(-1)).toBe(patch); }); it("does not flatten extra values into combined canvas/media Reset history", () => { const initial = createResetState(); const imported = importImageMedia(initial, { sourceTarget: "media.source", }); const resized = toolcraftReducer(imported, { type: "canvas.setSize", size: { width: 640, height: 480, unit: "px" }, }); const changed = toolcraftReducer(resized, { type: "controls.setValue", target: "layers", value: initial.values.layers, }); expect(changed.values.layers).toBe(initial.values.layers); const reset = toolcraftReducer(changed, { type: "controls.reset" }); const undone = toolcraftReducer(reset, { type: "history.undo" }); const redone = toolcraftReducer(undone, { type: "history.redo" }); const patch = reset.history.undo.at(-1)!; expect(isToolcraftControlsResetHistoryPatch(patch)).toBe(true); for (const side of [patch.before, patch.after]) { expect(Object.hasOwn(side, "composition.layout")).toBe(false); expect(Object.hasOwn(side, "timeline")).toBe(false); } for (const [state, amount] of [ [reset, 50], [undone, 75], [redone, 50], ] as const) { expect(state.values.amount).toBe(amount); expect(state.values["composition.layout"]).toBe( initial.values["composition.layout"], ); expect(state.values.timeline).toBe(initial.values.timeline); expect(state.values.layers).toBe(initial.values.layers); expect(state.timeline).toBe(initial.timeline); } expect(reset.canvas.size).toEqual(initial.canvas.size); expect(undone.canvas.size).toEqual(changed.canvas.size); expect(redone.canvas.size).toEqual(initial.canvas.size); expect(reset.mediaAssets).toEqual([]); expect(undone.mediaAssets).toEqual(changed.mediaAssets); expect(undone.layers).toEqual(changed.layers); expect(undone.selectedLayerId).toBe(changed.selectedLayerId); expect(redone.mediaAssets).toEqual([]); }); });