import { describe, expect, it, vi } from "vitest"; import { createState } from "./reducer-test-utils"; import { createToolcraftExternalStore } from "./toolcraft-external-store"; const opacityTarget = "selectedLayer.opacity"; describe("Toolcraft external-store publication atomicity", () => { it("commits without throwing and continues after throwing listeners", () => { const store = createToolcraftExternalStore(createState()); const listenerError = new Error("listener failed"); const dependencyListenerError = new Error("dependency listener failed"); const throwingListener = vi.fn(() => { throw listenerError; }); const healthyListener = vi.fn(); const throwingDependencyListener = vi.fn(() => { throw dependencyListenerError; }); const healthyDependencyListener = vi.fn(); store.subscribe(throwingListener); store.subscribe(healthyListener); store.subscribeDependencies( [{ kind: "value", target: opacityTarget }], throwingDependencyListener, ); store.subscribeDependencies( [{ kind: "value", target: opacityTarget }], healthyDependencyListener, ); expect(() => store.dispatch({ target: opacityTarget, type: "controls.setValue", value: 40, }), ).not.toThrow(); expect(store.getCommittedState().values[opacityTarget]).toBe(40); expect(store.getState().values[opacityTarget]).toBe(40); expect(throwingListener).toHaveBeenCalledOnce(); expect(healthyListener).toHaveBeenCalledOnce(); expect(throwingDependencyListener).toHaveBeenCalledOnce(); expect(healthyDependencyListener).toHaveBeenCalledOnce(); }); it("isolates throwing selectors, equalities, and selector listeners", () => { const store = createToolcraftExternalStore(createState()); const selectorError = new Error("selector failed"); const equalityError = new Error("equality failed"); const selectorListenerError = new Error("selector listener failed"); const throwingSelectorListener = vi.fn(); const throwingEquality = vi.fn(() => { throw equalityError; }); const throwingEqualityListener = vi.fn(); const throwingSelectorCallback = vi.fn(() => { throw selectorListenerError; }); const healthySelectorListener = vi.fn(); const unrelatedSelectorListener = vi.fn(); store.subscribeSelector((state) => { if (state.values[opacityTarget] === 40) { throw selectorError; } return state.values[opacityTarget]; }, throwingSelectorListener); store.subscribeSelector( (state) => state.values[opacityTarget], throwingEqualityListener, throwingEquality, ); store.subscribeSelector( (state) => state.values[opacityTarget], throwingSelectorCallback, ); store.subscribeSelector( (state) => state.values[opacityTarget], healthySelectorListener, ); store.subscribeSelector( (state) => state.canvas.zoom, unrelatedSelectorListener, ); expect(() => store.dispatch({ target: opacityTarget, type: "controls.setValue", value: 40, }), ).not.toThrow(); expect(store.getCommittedState().values[opacityTarget]).toBe(40); expect(throwingSelectorListener).not.toHaveBeenCalled(); expect(throwingEquality).toHaveBeenCalledOnce(); expect(throwingEqualityListener).not.toHaveBeenCalled(); expect(throwingSelectorCallback).toHaveBeenCalledOnce(); expect(healthySelectorListener).toHaveBeenCalledOnce(); expect(unrelatedSelectorListener).not.toHaveBeenCalled(); }); it("still propagates reducer-side errors before commit", () => { const reducerError = new Error("reducer failed"); const baseState = createState(); const initialState = { ...baseState, values: new Proxy(baseState.values, { get() { throw reducerError; }, }), }; const store = createToolcraftExternalStore(initialState); const listener = vi.fn(); store.subscribe(listener); expect(() => store.dispatch({ target: opacityTarget, type: "controls.setValue", value: 40, }), ).toThrow(reducerError); expect(store.getCommittedState()).toBe(initialState); expect(store.getState()).toBe(initialState); expect(listener).not.toHaveBeenCalled(); }); });