import { describe, expect, it, vi } from "vitest"; import { createState } from "./reducer-test-utils"; import { getToolcraftControlsResetHistorySource } from "./history-patch-metadata"; import { getToolcraftStoreChanges, toolcraftStoreDependenciesChanged, } from "./toolcraft-external-store-dependencies"; import { createToolcraftExternalStore } from "./toolcraft-external-store"; import type { ToolcraftTimelineKeyframeGroup } from "./types"; function createKeyframeGroup( controlId: string, keyframeId: string, ): ToolcraftTimelineKeyframeGroup { return { controlId, keyframes: [ { controlId, controlLabel: controlId, id: keyframeId, timeSeconds: 1, value: 1, valueLabel: "1", }, ], label: controlId, }; } function instrumentGroupFinds( groups: ToolcraftTimelineKeyframeGroup[], onFind: () => void, ): ToolcraftTimelineKeyframeGroup[] { return new Proxy(groups, { get(target, property, receiver) { if (property === "find") { onFind(); } return Reflect.get(target, property, receiver); }, }); } describe("Toolcraft external-store dependencies", () => { it("skips selected-keyframe target scans when selection references are unchanged", () => { const scans = vi.fn(); const groups = instrumentGroupFinds( [ createKeyframeGroup("value.first", "selected"), ...Array.from({ length: 200 }, (_, index) => createKeyframeGroup(`value.${index + 2}`, `keyframe.${index + 2}`), ), ], scans, ); const baseState = createState({ timeline: { selectedKeyframeId: "selected" }, }); const state = { ...baseState, timeline: { ...baseState.timeline, keyframeGroups: groups }, }; getToolcraftStoreChanges(state, { ...state, timeline: { ...state.timeline, currentTimeSeconds: 2 }, }); getToolcraftStoreChanges(state, { ...state, canvas: { ...state.canvas, zoom: 150 }, }); getToolcraftStoreChanges(state, { ...state, values: { ...state.values, "selectedLayer.opacity": 42 }, }); expect(scans).not.toHaveBeenCalled(); }); it("resolves changed selected-keyframe targets accurately", () => { const scans = vi.fn(); const previousGroups = instrumentGroupFinds( [ createKeyframeGroup("value.first", "selected"), createKeyframeGroup("value.second", "second"), ], scans, ); const baseState = createState({ timeline: { selectedKeyframeId: "selected" }, }); const previous = { ...baseState, timeline: { ...baseState.timeline, keyframeGroups: previousGroups }, }; const selectionChanges = getToolcraftStoreChanges(previous, { ...previous, timeline: { ...previous.timeline, selectedKeyframeId: "second" }, }); expect(selectionChanges.keyframeSelectionTargets).toEqual( new Set(["value.first", "value.second"]), ); expect(scans).toHaveBeenCalledTimes(2); scans.mockClear(); const selectedKeyframe = previousGroups[0]?.keyframes[0]; const nextGroups = instrumentGroupFinds( [ { ...previousGroups[0]!, keyframes: [] }, { ...previousGroups[1]!, keyframes: selectedKeyframe ? [selectedKeyframe] : [], }, ], scans, ); const groupChanges = getToolcraftStoreChanges(previous, { ...previous, timeline: { ...previous.timeline, keyframeGroups: nextGroups }, }); expect(groupChanges.keyframeSelectionTargets).toEqual( new Set(["value.first", "value.second"]), ); expect(scans).toHaveBeenCalledTimes(2); }); it("notifies reset-source dependencies only when undo generation changes", () => { const store = createToolcraftExternalStore(createState()); const listener = vi.fn(); store.subscribeDependencies( [ { kind: "history.source", source: getToolcraftControlsResetHistorySource(), }, ], listener, ); for (let value = 1; value <= 20; value += 1) { store.dispatch({ target: "selectedLayer.opacity", type: "controls.setValue", value, }); } store.dispatch({ history: "merge", historyGroup: "ordinary-drag", target: "selectedLayer.opacity", type: "controls.setValue", value: 21, }); store.dispatch({ history: "merge", historyGroup: "ordinary-drag", target: "selectedLayer.opacity", type: "controls.setValue", value: 22, }); expect(listener).not.toHaveBeenCalled(); store.dispatch({ type: "controls.reset" }); expect(listener).toHaveBeenCalledTimes(1); store.dispatch({ target: "selectedLayer.opacity", type: "controls.setValue", value: 30, }); store.dispatch({ type: "history.undo" }); expect(listener).toHaveBeenCalledTimes(1); store.dispatch({ type: "history.undo" }); expect(listener).toHaveBeenCalledTimes(2); store.dispatch({ type: "history.redo" }); expect(listener).toHaveBeenCalledTimes(3); store.dispatch({ type: "history.undo" }); expect(listener).toHaveBeenCalledTimes(4); store.dispatch({ target: "selectedLayer.opacity", type: "controls.setValue", value: 31, }); expect(listener).toHaveBeenCalledTimes(4); expect(store.getState().history.redo).toEqual([]); }); it("matches a reset-source dependency from transition metadata", () => { const previous = createState(); const store = createToolcraftExternalStore(previous); store.dispatch({ target: "selectedLayer.opacity", type: "controls.setValue", value: 12, }); store.dispatch({ type: "controls.reset" }); const changes = getToolcraftStoreChanges(previous, store.getState()); expect( toolcraftStoreDependenciesChanged( [ { kind: "history.source", source: getToolcraftControlsResetHistorySource(), }, ], changes, ), ).toBe(true); }); });