import { act, cleanup, render, screen } from "@testing-library/react";
import * as React from "react";
import { renderToString } from "react-dom/server";
import { afterEach, describe, expect, it, vi } from "vitest";
import { defineToolcraft } from "../../schema/define-toolcraft";
import { createToolcraftState } from "../../state/create-template-state";
import { createToolcraftExternalStore } from "../../state/toolcraft-external-store";
import type { ToolcraftExternalStore } from "../../state/toolcraft-external-store";
import { ToolcraftStoreContext } from "./toolcraft-store-context";
import { useToolcraftSelector } from "./use-toolcraft";
function createSelectorStore() {
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 StoreProvider({
children,
store,
}: {
children: React.ReactNode;
store: ToolcraftExternalStore;
}) {
return (
{children}
);
}
afterEach(cleanup);
describe("useToolcraftSelector", () => {
it("keeps the committed selector active when a different selector suspends and is interrupted", async () => {
const store = createSelectorStore();
const suspendedRender = new Promise(() => undefined);
const attemptedTargets: string[] = [];
function Probe({ shouldSuspend, target }: { shouldSuspend: boolean; target: string }) {
const value = useToolcraftSelector((state) => state.values[target]);
attemptedTargets.push(target);
if (shouldSuspend) {
throw suspendedRender;
}
return ;
}
const view = render(
Pending}>
,
);
await act(async () => {
React.startTransition(() => {
view.rerender(
Pending}>
,
);
});
});
expect(attemptedTargets).toContain("value.second");
expect(screen.getByTestId("selector-value").textContent).toBe("1");
act(() => {
store.dispatch({
target: "value.first",
type: "controls.setValue",
value: 11,
});
});
expect(screen.getByTestId("selector-value").textContent).toBe("11");
});
it("keeps one subscription across parent, playback, and selector-prop renders", () => {
const store = createSelectorStore();
const subscriptionStarts = vi.fn();
const subscriptionStops = vi.fn();
const subscribe = store.subscribe;
const subscribeSelector = store.subscribeSelector;
store.subscribe = ((listener) => {
subscriptionStarts();
const unsubscribe = subscribe(listener);
return () => {
subscriptionStops();
unsubscribe();
};
}) as ToolcraftExternalStore["subscribe"];
store.subscribeSelector = ((selector, listener, equality) => {
subscriptionStarts();
const unsubscribe = subscribeSelector(selector, listener, equality);
return () => {
subscriptionStops();
unsubscribe();
};
}) as ToolcraftExternalStore["subscribeSelector"];
function Probe({ label, target }: { label: string; target: "playback" | "value.second" }) {
const value = useToolcraftSelector((state) =>
target === "playback"
? state.timeline.currentTimeSeconds
: state.values[target],
);
return ;
}
const view = render(
,
);
view.rerender(
,
);
act(() => {
store.dispatchTransient({
currentTimeSeconds: 1,
type: "timeline.setCurrentTime",
});
});
expect(screen.getByTestId("selector-value").textContent).toBe("parent:1");
view.rerender(
,
);
act(() => {
store.dispatch({
target: "value.second",
type: "controls.setValue",
value: 20,
});
});
expect(screen.getByTestId("selector-value").textContent).toBe("switched:20");
expect(subscriptionStarts).toHaveBeenCalledTimes(1);
expect(subscriptionStops).not.toHaveBeenCalled();
view.unmount();
expect(subscriptionStops).toHaveBeenCalledTimes(1);
});
it("uses custom equality without rerendering for an equivalent selection", () => {
const store = createSelectorStore();
let renderCount = 0;
function Probe() {
const offset = useToolcraftSelector(
(state) => ({ ...state.canvas.offset }),
(previous, next) => previous.x === next.x && previous.y === next.y,
);
renderCount += 1;
return ;
}
render(
,
);
act(() => {
store.dispatchTransient({
offset: { x: 0, y: 0 },
type: "canvas.setViewport",
zoom: 150,
});
});
expect(renderCount).toBe(1);
});
it("uses a stable server snapshot", () => {
const store = createSelectorStore();
function Probe() {
return (
);
}
expect(
renderToString(
,
),
).toContain("1");
});
});