import * as React from "react"; import { act, cleanup, render } from "@testing-library/react"; import { afterEach, describe, expect, it } from "vitest"; import type { ToolcraftArtifactExportOwner } from "../../export/artifact-export-owner"; import { defineToolcraft } from "../../schema/define-toolcraft"; import { createToolcraftState } from "../../state/create-template-state"; import { createToolcraftExternalStore } from "../../composition/public-state"; import { ToolcraftExportProvider, useToolcraftExportOwner, } from "./toolcraft-export-context"; import { ToolcraftRoot } from "./toolcraft-root"; afterEach(cleanup); const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft fixture", }, canvas: { enabled: true }, panels: {}, }, modules: [], }); const createStore = () => createToolcraftExternalStore(createToolcraftState(schema)); function deferred() { let resolve!: () => void; const promise = new Promise((done) => { resolve = done; }); return { promise, resolve }; } function Probe({ capture, }: { capture: (owner: ToolcraftArtifactExportOwner) => void; }) { capture(useToolcraftExportOwner()); return null; } describe("root-scoped export owner", () => { it("shares one admission slot between callers of the same store", async () => { const store = createStore(); const owners: ToolcraftArtifactExportOwner[] = []; const capture = (owner: ToolcraftArtifactExportOwner) => owners.push(owner); render( <> , ); expect(owners[0]).toBe(owners[1]); const gate = deferred(); const first = owners[0]!.start(() => gate.promise); expect(owners[1]!.start(() => undefined)).toEqual({ status: "busy" }); gate.resolve(); if (first.status !== "started") throw new Error("Expected admission"); await expect(first.completion).resolves.toMatchObject({ status: "completed", }); }); it("does not share jobs across different stores", async () => { const owners: ToolcraftArtifactExportOwner[] = []; render( <> owners.push(owner)} /> owners.push(owner)} /> , ); expect(owners[0]).not.toBe(owners[1]); const gate = deferred(); const jobs = owners.map((owner) => owner.start(() => gate.promise)); expect(jobs.every((job) => job.status === "started")).toBe(true); gate.resolve(); await Promise.all( jobs.map((job) => job.status === "started" ? job.completion : undefined, ), ); }); it("survives panel hiding and remount without cancelling or replacing the owner", async () => { const store = createStore(); let owner!: ToolcraftArtifactExportOwner; const capture = (next: ToolcraftArtifactExportOwner) => { owner = next; }; const view = render( , ); const original = owner; const gate = deferred(); const job = owner.start(() => gate.promise); view.rerender( {null}, ); await Promise.resolve(); expect(original.getStatus().phase).toBe("running"); view.rerender( , ); expect(owner).toBe(original); gate.resolve(); if (job.status !== "started") throw new Error("Expected admission"); await expect(job.completion).resolves.toMatchObject({ status: "completed", }); }); it("retires the previous store without publishing its late result to the replacement", async () => { let owner!: ToolcraftArtifactExportOwner; const capture = (next: ToolcraftArtifactExportOwner) => { owner = next; }; const view = render( , ); const previous = owner; const gate = deferred(); let published = false; const oldJob = previous.start(async ({ signal }) => { await gate.promise; signal.throwIfAborted(); published = true; }); view.rerender( , ); expect(owner).not.toBe(previous); const newJob = owner.start(() => "new store"); gate.resolve(); if (oldJob.status !== "started" || newJob.status !== "started") throw new Error("Expected admission"); await expect(oldJob.completion).resolves.toEqual({ status: "cancelled" }); await expect(newJob.completion).resolves.toEqual({ status: "completed", value: "new store", }); expect(published).toBe(false); expect(previous.getStatus().phase).toBe("disposed"); expect(owner.getStatus().phase).toBe("idle"); }); it("reconnects a retired store only after its previous export has settled", async () => { const store = createStore(); let owner!: ToolcraftArtifactExportOwner; const capture = (next: ToolcraftArtifactExportOwner) => { owner = next; }; const first = render( , ); const retired = owner; const gate = deferred(); const job = retired.start(() => gate.promise); first.unmount(); await Promise.resolve(); expect(retired.getStatus().phase).toBe("disposing"); render( , ); expect(owner).toBe(retired); expect(() => owner.start(() => "too early")).toThrow("disposed"); if (job.status !== "started") throw new Error("Expected admission"); await act(async () => { gate.resolve(); await expect(job.completion).resolves.toEqual({ status: "cancelled" }); }); expect(owner).not.toBe(retired); expect(owner.getStatus().phase).toBe("idle"); const retry = owner.start(() => "ready"); if (retry.status !== "started") throw new Error("Expected retry admission"); await expect(retry.completion).resolves.toEqual({ status: "completed", value: "ready", }); }); it("keeps the reconnected StrictMode owner usable", async () => { let owner!: ToolcraftArtifactExportOwner; render( { owner = next; }} /> , ); await Promise.resolve(); expect(owner.getStatus().phase).toBe("idle"); const job = owner.start(() => "ready"); if (job.status !== "started") throw new Error("Expected admission"); await expect(job.completion).resolves.toEqual({ status: "completed", value: "ready", }); }); it("cancels on real root teardown even if a renderer settles immediately afterward", async () => { let owner!: ToolcraftArtifactExportOwner; const view = render( { owner = next; }} /> , ); const gate = deferred(); let downloaded = false; const job = owner.start(async ({ signal }) => { await gate.promise; signal.throwIfAborted(); downloaded = true; }); view.unmount(); gate.resolve(); if (job.status !== "started") throw new Error("Expected admission"); await expect(job.completion).resolves.toEqual({ status: "cancelled" }); expect(downloaded).toBe(false); expect(owner.getStatus().phase).toBe("disposed"); }); });