import * as React from "react"; import { afterEach, expect, it, vi } from "vitest"; import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { defineToolcraft } from "../../../schema/define-toolcraft"; import { createToolcraftState } from "../../../state/create-template-state"; import { ToolcraftDefaultsAuthoringProvider } from "../../app-shell/toolcraft-defaults-authoring"; import { SaveAppDefaults } from "./controls-panel-save-defaults"; import { createToolcraftSourceAssetCoordinator } from "../../../composition/source-asset-coordinator"; import { ToolcraftSourceAssetCoordinatorContext } from "../../app-shell/toolcraft-source-asset-context"; import type { ToolcraftDefaultResourceUpload } from "../../../source-assets/default-resource-capture"; import { createMemoryToolcraftBinaryAssetRepository } from "../../../source-assets/repository/memory-binary-asset-repository"; import { sliderRangeDefinition } from "../../../state/slider-range.test-support"; import { toolcraftReducer } from "../../../composition/public-state"; afterEach(cleanup); const state = createToolcraftState(defineToolcraft({ base: { identity: { id: "defaults-button", title: "Defaults" }, canvas: { enabled: false }, panels: { controls: { title: "Parameters", sections: [{ id: "params", controls: { title: { type: "text", label: "Title", target: "title", defaultValue: "Initial", applicability: { mode: "always" } }, } }] } } }, modules: [], }), { values: { title: "Current" } }); it("does not offer source writes without an authoring capability", () => { render( state} />); expect(screen.queryByRole("button", { name: "Save State as Default" })).toBeNull(); }); it("captures current parameters, prevents duplicate saves and confirms completed writes", async () => { let finish!: () => void; const save = vi.fn((_defaults: unknown) => new Promise((resolve) => { finish = resolve; })); render( state} />); fireEvent.click(screen.getByRole("button", { name: "Save State as Default" })); const pending = screen.getByRole("button", { name: "Saving Defaults…" }); expect((pending as HTMLButtonElement).disabled).toBe(true); expect(pending.getAttribute("aria-busy")).toBe("true"); expect(pending.querySelector('[data-slot="button-loader"]')).not.toBeNull(); fireEvent.click(pending); expect(save).toHaveBeenCalledOnce(); expect(save.mock.calls[0]?.[0]).toMatchObject({ state: { values: { title: "Current" } } }); finish(); await waitFor(() => expect(screen.getByRole("status").textContent).toBe("App defaults saved.")); }); it("keeps the current workspace and offers a retry after a failed write", async () => { const save = vi.fn(async () => { throw new Error("Defaults changed. Reload before saving."); }); render( state} />); fireEvent.click(screen.getByRole("button", { name: "Save State as Default" })); await waitFor(() => expect(screen.getByRole("alert").textContent).toContain("Defaults changed")); expect(state.values.title).toBe("Current"); expect((screen.getByRole("button", { name: "Save State as Default" }) as HTMLButtonElement).disabled).toBe(false); expect(screen.queryByRole("button", { name: "Saving Defaults…" })).toBeNull(); expect(document.querySelector('[data-slot="animated-loader"]')).toBeNull(); }); it("retries the complete slider value and range after a source write fails", async () => { const changed = toolcraftReducer(createToolcraftState(defineToolcraft(sliderRangeDefinition)), { type: "controls.editSlider", target: "shape.radius", value: 1000, }); const save = vi.fn().mockRejectedValueOnce(new Error("Write unavailable")).mockResolvedValueOnce(undefined); render( changed} />); fireEvent.click(screen.getByRole("button", { name: "Save State as Default" })); await waitFor(() => expect(screen.getByRole("alert").textContent).toContain("Write unavailable")); expect(changed.values["shape.radius"]).toBe(1000); expect(changed.controlRanges["shape.radius"]).toEqual({ min: 0, max: 1000 }); expect(document.querySelector('[data-slot="animated-loader"]')).toBeNull(); fireEvent.click(screen.getByRole("button", { name: "Save State as Default" })); await waitFor(() => expect(screen.getByRole("status").textContent).toBe("App defaults saved.")); expect(save).toHaveBeenCalledTimes(2); for (const call of save.mock.calls) expect(call[0]).toMatchObject({ state: { values: { "shape.radius": 1000 }, controlRanges: { "shape.radius": { min: 0, max: 1000 } }, } }); }); it("keeps the design-system loader active from resource capture through the final write", async () => { const coordinator = createToolcraftSourceAssetCoordinator({ getState: () => state, dispatch: () => {}, repository: createMemoryToolcraftBinaryAssetRepository(), }); let finishCapture!: (uploads: readonly ToolcraftDefaultResourceUpload[]) => void; let finishWrite!: () => void; coordinator.captureDefaultResources = vi.fn(() => new Promise(resolve => { finishCapture = resolve; })); const save = vi.fn(() => new Promise(resolve => { finishWrite = resolve; })); render( state} /> ); fireEvent.click(screen.getByRole("button", { name: "Save State as Default" })); const pending = screen.getByRole("button", { name: "Saving Defaults…" }); const indicator = pending.querySelector('[data-slot="animated-loader-indicator"]'); expect(indicator).not.toBeNull(); expect(save).not.toHaveBeenCalled(); fireEvent.click(pending); expect(coordinator.captureDefaultResources).toHaveBeenCalledOnce(); finishCapture([]); await waitFor(() => expect(save).toHaveBeenCalledOnce()); expect(pending.getAttribute("aria-busy")).toBe("true"); expect((pending as HTMLButtonElement).disabled).toBe(true); expect(pending.querySelector('[data-slot="animated-loader-indicator"]')).toBe(indicator); finishWrite(); await waitFor(() => expect(screen.queryByRole("button", { name: "Saving Defaults…" })).toBeNull()); expect(document.querySelector('[data-slot="animated-loader"]')).toBeNull(); await coordinator.dispose(); });