import { act, cleanup, render } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { defineToolcraft } from "../../schema/define-toolcraft";
import { createToolcraftState } from "../../state/create-template-state";
import { createState } from "../../state/reducer-test-utils";
import { createToolcraftExternalStore } from "../../state/toolcraft-external-store";
import type { ToolcraftExternalStore } from "../../state/toolcraft-external-store";
import type { ToolcraftCommand } from "../../state/types";
import { useToolcraftStoreSelector } from "./toolcraft-selectors";
import { ToolcraftStoreContext } from "./toolcraft-store-context";
const valueDependencies = [{ kind: "value", target: "value.first" }] as const;
const otherValueDependencies = [
{ kind: "value", target: "value.second" },
] as const;
const playbackDependencies = [{ kind: "playback" }] as const;
const viewportDependencies = [{ kind: "viewport.zoom" }] as const;
const firstKeyframeDependencies = [
{ kind: "keyframeGroup", target: "value.first" },
] as const;
const secondKeyframeDependencies = [
{ kind: "keyframeGroup", target: "value.second" },
] as const;
const keyframeEditCommands: Array<{
command: ToolcraftCommand;
label: string;
}> = [
{
command: {
keyframeId: "value.first::1",
timeSeconds: 2,
type: "timeline.moveKeyframe",
},
label: "move",
},
{
command: {
keyframeId: "value.first::1",
type: "timeline.deleteKeyframe",
},
label: "delete",
},
{
command: {
easing: { type: "step" },
keyframeId: "value.first::1",
type: "timeline.changeKeyframeEasing",
},
label: "easing change",
},
];
function createSelectorLaneStore() {
const schema = defineToolcraft({
canvas: { enabled: true },
panels: {
controls: {
sections: [
{
controls: {
first: {
defaultValue: 1,
target: "value.first",
type: "slider",
},
second: {
defaultValue: 2,
target: "value.second",
type: "slider",
},
},
},
],
title: "Controls",
},
},
});
return createToolcraftExternalStore(createToolcraftState(schema));
}
function createKeyframeLaneStore() {
return createToolcraftExternalStore(
createState({
timeline: {
keyframeGroups: [
{
controlId: "value.first",
keyframes: [
{
controlId: "value.first",
controlLabel: "First",
id: "value.first::1",
timeSeconds: 1,
value: 10,
valueLabel: "10",
},
],
label: "First",
},
{
controlId: "value.second",
keyframes: [
{
controlId: "value.second",
controlLabel: "Second",
id: "value.second::1",
timeSeconds: 1,
value: 20,
valueLabel: "20",
},
],
label: "Second",
},
],
},
}),
);
}
function LaneProvider({
children,
store,
}: {
children: React.ReactNode;
store: ToolcraftExternalStore;
}) {
return (
{children}
);
}
afterEach(cleanup);
describe("Toolcraft selector execution lanes", () => {
it("does not execute selectors outside their changed dependency fields", () => {
const store = createSelectorLaneStore();
const executions = {
playback: vi.fn(),
otherValue: vi.fn(),
value: vi.fn(),
viewport: vi.fn(),
};
function ValueProbe() {
useToolcraftStoreSelector(
(state) => {
executions.value();
return state.values["value.first"];
},
Object.is,
valueDependencies,
);
return null;
}
function PlaybackProbe() {
useToolcraftStoreSelector(
(state) => {
executions.playback();
return state.timeline.currentTimeSeconds;
},
Object.is,
playbackDependencies,
);
return null;
}
function OtherValueProbe() {
useToolcraftStoreSelector(
(state) => {
executions.otherValue();
return state.values["value.second"];
},
Object.is,
otherValueDependencies,
);
return null;
}
function ViewportProbe() {
useToolcraftStoreSelector(
(state) => {
executions.viewport();
return state.canvas.zoom;
},
Object.is,
viewportDependencies,
);
return null;
}
render(
,
);
const initial = {
playback: executions.playback.mock.calls.length,
otherValue: executions.otherValue.mock.calls.length,
value: executions.value.mock.calls.length,
viewport: executions.viewport.mock.calls.length,
};
act(() => {
store.dispatchTransient({
currentTimeSeconds: 1,
type: "timeline.setCurrentTime",
});
});
expect(executions.playback.mock.calls.length).toBeGreaterThan(
initial.playback,
);
expect(executions.value).toHaveBeenCalledTimes(initial.value);
expect(executions.otherValue).toHaveBeenCalledTimes(initial.otherValue);
expect(executions.viewport).toHaveBeenCalledTimes(initial.viewport);
const afterPlayback = {
playback: executions.playback.mock.calls.length,
otherValue: executions.otherValue.mock.calls.length,
value: executions.value.mock.calls.length,
viewport: executions.viewport.mock.calls.length,
};
act(() => {
store.dispatchTransient({
offset: { x: 0, y: 0 },
type: "canvas.setViewport",
zoom: 150,
});
});
expect(executions.viewport.mock.calls.length).toBeGreaterThan(
afterPlayback.viewport,
);
expect(executions.value).toHaveBeenCalledTimes(afterPlayback.value);
expect(executions.otherValue).toHaveBeenCalledTimes(
afterPlayback.otherValue,
);
expect(executions.playback).toHaveBeenCalledTimes(
afterPlayback.playback,
);
const afterViewport = {
playback: executions.playback.mock.calls.length,
otherValue: executions.otherValue.mock.calls.length,
value: executions.value.mock.calls.length,
viewport: executions.viewport.mock.calls.length,
};
act(() => {
store.dispatch({
target: "value.first",
type: "controls.setValue",
value: 10,
});
});
expect(executions.value.mock.calls.length).toBeGreaterThan(
afterViewport.value,
);
expect(executions.otherValue).toHaveBeenCalledTimes(
afterViewport.otherValue,
);
expect(executions.playback).toHaveBeenCalledTimes(
afterViewport.playback,
);
expect(executions.viewport).toHaveBeenCalledTimes(
afterViewport.viewport,
);
});
it.each(keyframeEditCommands)(
"does not execute target B selector for target A $label",
({ command }) => {
const store = createKeyframeLaneStore();
const executions = { first: vi.fn(), second: vi.fn() };
function FirstKeyframeProbe() {
useToolcraftStoreSelector(
(state) => {
executions.first();
return state.timeline.keyframeGroups.find(
(group) => group.controlId === "value.first",
);
},
Object.is,
firstKeyframeDependencies,
);
return null;
}
function SecondKeyframeProbe() {
useToolcraftStoreSelector(
(state) => {
executions.second();
return state.timeline.keyframeGroups.find(
(group) => group.controlId === "value.second",
);
},
Object.is,
secondKeyframeDependencies,
);
return null;
}
render(
,
);
const initialFirstExecutions = executions.first.mock.calls.length;
const initialSecondExecutions = executions.second.mock.calls.length;
act(() => {
store.dispatch(command);
});
expect(executions.first.mock.calls.length).toBeGreaterThan(
initialFirstExecutions,
);
expect(executions.second).toHaveBeenCalledTimes(
initialSecondExecutions,
);
},
);
});