import {
act,
cleanup,
fireEvent,
render,
screen,
} from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { ToolcraftExternalStore } from "../../state/toolcraft-external-store";
import { ToolcraftRoot } from "./toolcraft-root";
import {
CaptureStore,
createHotPersistenceSchema,
createValuesPersistenceSchema,
StorePersistenceProbe,
} from "./toolcraft-root-persistence-test-utils";
afterEach(() => {
vi.useRealTimers();
cleanup();
window.localStorage.clear();
});
describe("ToolcraftRoot committed and effective persistence", () => {
it("does not restart persistence debounce for transient or non-included state", () => {
vi.useFakeTimers();
const schema = createValuesPersistenceSchema();
render(
,
);
act(() => {
vi.advanceTimersByTime(100);
});
fireEvent.click(screen.getByRole("button", { name: "Scrub timeline" }));
fireEvent.click(
screen.getByRole("button", { name: "Expand non-persisted timeline" }),
);
act(() => {
vi.advanceTimersByTime(20);
});
expect(
JSON.parse(
window.localStorage.getItem(
"toolcraft:root-test-committed-values:state:v1",
) ?? "{}",
),
).toMatchObject({
state: { values: { "selectedLayer.opacity": 75 } },
version: 1,
});
window.localStorage.setItem(
"toolcraft:root-test-committed-values:state:v1",
"unchanged",
);
act(() => {
vi.advanceTimersByTime(200);
});
expect(
window.localStorage.getItem(
"toolcraft:root-test-committed-values:state:v1",
),
).toBe("unchanged");
});
it("restarts persistence debounce for mandatory canvas changes", () => {
vi.useFakeTimers();
const schema = createValuesPersistenceSchema();
render(
,
);
act(() => {
vi.advanceTimersByTime(120);
});
window.localStorage.setItem(
"toolcraft:root-test-committed-values:state:v1",
"unchanged",
);
fireEvent.click(screen.getByRole("button", { name: "Resize canvas" }));
act(() => {
vi.advanceTimersByTime(119);
});
expect(
window.localStorage.getItem(
"toolcraft:root-test-committed-values:state:v1",
),
).toBe("unchanged");
act(() => {
vi.advanceTimersByTime(1);
});
expect(
JSON.parse(
window.localStorage.getItem(
"toolcraft:root-test-committed-values:state:v1",
) ?? "{}",
),
).toMatchObject({
state: {
canvas: {
size: { height: 480, unit: "px", width: 640 },
},
},
version: 1,
});
});
it("schedules a persisted hot slice only when its transient lane commits", () => {
vi.useFakeTimers();
const schema = createHotPersistenceSchema();
render(
,
);
act(() => {
vi.advanceTimersByTime(120);
});
window.localStorage.setItem(
"toolcraft:root-test-hot:state:v1",
"unchanged",
);
fireEvent.click(screen.getByRole("button", { name: "Scrub timeline" }));
act(() => {
vi.advanceTimersByTime(200);
});
expect(
window.localStorage.getItem("toolcraft:root-test-hot:state:v1"),
).toBe("unchanged");
fireEvent.click(screen.getByRole("button", { name: "Commit playback" }));
act(() => {
vi.advanceTimersByTime(119);
});
expect(
window.localStorage.getItem("toolcraft:root-test-hot:state:v1"),
).toBe("unchanged");
act(() => {
vi.advanceTimersByTime(1);
});
expect(
JSON.parse(
window.localStorage.getItem("toolcraft:root-test-hot:state:v1") ??
"{}",
),
).toMatchObject({
state: { timeline: { currentTimeSeconds: 2 } },
version: 1,
});
});
it("debounces committed state instead of transient overlays", () => {
vi.useFakeTimers();
const schema = createHotPersistenceSchema();
render(
,
);
act(() => {
vi.advanceTimersByTime(120);
});
fireEvent.click(screen.getByRole("button", { name: "Scrub timeline" }));
fireEvent.click(screen.getByRole("button", { name: "Move viewport" }));
fireEvent.click(
screen.getByRole("button", { name: "Set persisted value" }),
);
act(() => {
vi.advanceTimersByTime(120);
});
expect(
JSON.parse(
window.localStorage.getItem("toolcraft:root-test-hot:state:v1") ??
"{}",
),
).toMatchObject({
state: {
canvas: { offset: { x: 0, y: 0 }, zoom: 100 },
timeline: { currentTimeSeconds: 0 },
values: { "selectedLayer.opacity": 50 },
},
version: 1,
});
});
it("flushes committed state on pagehide without transient overlays", () => {
vi.useFakeTimers();
const schema = createHotPersistenceSchema();
render(
,
);
fireEvent.click(screen.getByRole("button", { name: "Scrub timeline" }));
fireEvent.click(screen.getByRole("button", { name: "Move viewport" }));
fireEvent.click(
screen.getByRole("button", { name: "Set persisted value" }),
);
window.dispatchEvent(new Event("pagehide"));
expect(
JSON.parse(
window.localStorage.getItem("toolcraft:root-test-hot:state:v1") ??
"{}",
),
).toMatchObject({
state: {
canvas: { offset: { x: 0, y: 0 }, zoom: 100 },
timeline: { currentTimeSeconds: 0 },
values: { "selectedLayer.opacity": 50 },
},
version: 1,
});
act(() => {
vi.advanceTimersByTime(200);
});
expect(
JSON.parse(
window.localStorage.getItem("toolcraft:root-test-hot:state:v1") ??
"{}",
),
).toMatchObject({
state: {
canvas: { offset: { x: 0, y: 0 }, zoom: 100 },
timeline: { currentTimeSeconds: 0 },
values: { "selectedLayer.opacity": 50 },
},
version: 1,
});
});
it("keeps transient hot fields out of persisted keyframe history roundtrips", () => {
vi.useFakeTimers();
const schema = createHotPersistenceSchema();
render(
,
);
act(() => {
vi.advanceTimersByTime(120);
});
fireEvent.click(screen.getByRole("button", { name: "Scrub timeline" }));
fireEvent.click(screen.getByRole("button", { name: "Move viewport" }));
fireEvent.click(screen.getByRole("button", { name: "Set keyframe" }));
fireEvent.click(screen.getByRole("button", { name: "Undo" }));
fireEvent.click(screen.getByRole("button", { name: "Redo" }));
act(() => {
vi.advanceTimersByTime(120);
});
expect(
JSON.parse(
window.localStorage.getItem("toolcraft:root-test-hot:state:v1") ??
"{}",
),
).toMatchObject({
state: {
canvas: { offset: { x: 0, y: 0 }, zoom: 100 },
timeline: {
currentTimeSeconds: 0,
keyframeGroups: [
{
keyframes: [{ id: "selectedLayer.opacity::2" }],
},
],
},
},
version: 1,
});
});
it("flushes pending committed state and cleans up subscriptions on unmount", () => {
vi.useFakeTimers();
let store: ToolcraftExternalStore | undefined;
const schema = createHotPersistenceSchema();
const view = render(
{ store = value; }} />
,
);
act(() => {
vi.advanceTimersByTime(120);
});
window.localStorage.setItem(
"toolcraft:root-test-hot:state:v1",
"unchanged",
);
act(() => {
store?.dispatch({
target: "selectedLayer.opacity",
type: "controls.setValue",
value: 50,
});
view.unmount();
});
expect(
JSON.parse(
window.localStorage.getItem("toolcraft:root-test-hot:state:v1") ??
"{}",
),
).toMatchObject({
state: { values: { "selectedLayer.opacity": 50 } },
version: 1,
});
act(() => {
store?.dispatch({
target: "selectedLayer.opacity",
type: "controls.setValue",
value: 25,
});
vi.advanceTimersByTime(200);
});
expect(
JSON.parse(
window.localStorage.getItem("toolcraft:root-test-hot:state:v1") ??
"{}",
).state.values["selectedLayer.opacity"],
).toBe(50);
});
});