import { afterAll, afterEach, describe, test, expect, vi } from "vitest"; import { enableMapSet } from "immer"; import { toast } from "@webstudio-is/design-system"; import type { Asset, Instance, Instances, Prop, Props, DataSource, DataSources, Resource, Resources, } from "@webstudio-is/sdk"; import { encodeDataSourceVariable, collectionComponent, coreMetas, elementComponent, blockTemplateComponent, blockComponent, portalComponent, } from "@webstudio-is/sdk"; import type { Project } from "@webstudio-is/project"; import * as baseComponentMetas from "@webstudio-is/sdk-components-react/metas"; import { componentMetas } from "@webstudio-is/sdk-components-registry/metas"; import { registerContainers } from "../sync/sync-stores"; import { $registeredComponentMetas } from "../nano-states"; import { $dataSources, $assets, $instances, $pages, $project, $props, $resources, } from "~/shared/sync/data-stores"; import { instanceText } from "./plugin-instance"; import { createDefaultPages } from "@webstudio-is/project-build"; import { selectInstance, selectInstances } from "~/shared/nano-states"; import { $allSelectedInstanceSelectors } from "~/shared/nano-states"; import { $selectedPageId } from "../nano-states/pages"; import * as instanceMutationUtils from "../instance-utils/mutation"; import { expectSlotTreeIntegrity, expectSlotsShareFragment, } from "../slot-test-utils"; import { pasteHandled } from "./copy-paste"; import { __testing__ as builderApiTesting } from "../builder-api"; import { $externalContentRoots } from "../external-content-mutations"; afterAll(builderApiTesting.useLocalApi()); afterEach(() => vi.restoreAllMocks()); const expectString = expect.any(String) as unknown as string; enableMapSet(); registerContainers(); $registeredComponentMetas.set( new Map(Object.entries({ ...baseComponentMetas, ...coreMetas })) ); $project.set({ id: "my-project" } as Project); $pages.set( createDefaultPages({ homePageId: "home-page", rootInstanceId: "body0", }) ); $selectedPageId.set("home-page"); const setPageRoot = (rootInstanceId: Instance["id"]) => { $pages.set( createDefaultPages({ homePageId: "home-page", rootInstanceId, }) ); $selectedPageId.set("home-page"); }; const createInstance = ( id: Instance["id"], component: string, children: Instance["children"] ): Instance => { return { type: "instance", id, component, children }; }; const getIdValuePair = (item: T) => [item.id, item] as const; const toMap = (list: T[]) => new Map(list.map(getIdValuePair)); const getMapDifference = >( left: Type, right: Type ): Type => { const leftSet = new Set(left.keys()); const rightSet = new Set(right.keys()); const difference = new Map() as Type; for (const [key, value] of left) { if (rightSet.has(key) === false) { difference.set(key, value); } } for (const [key, value] of right) { if (leftSet.has(key) === false) { difference.set(key, value); } } return difference; }; describe("copy and cut guards", () => { test("does not copy without a selected instance", () => { selectInstance(undefined); expect(instanceText.onCopy?.()).toBeUndefined(); }); test("does not cut without a selected instance", () => { selectInstance(undefined); expect(instanceText.onCut?.()).toBeUndefined(); }); test("does not copy or cut the selected root instance", () => { const instances: Instances = toMap([ createInstance("body0", "Body", [{ type: "id", value: "box1" }]), createInstance("box1", "Box", []), ] satisfies Instance[]); $instances.set(instances); selectInstance(["body0"]); expect(instanceText.onCopy?.()).toBeUndefined(); expect(instanceText.onCut?.()).toBeUndefined(); expect($instances.get()).toEqual(instances); }); test("copies multiple selected roots into one combined fragment", () => { $instances.set( toMap([ createInstance("body0", "Body", [ { type: "id", value: "box1" }, { type: "id", value: "box2" }, ]), createInstance("box1", "Box", []), createInstance("box2", "Box", []), ] satisfies Instance[]) ); selectInstances([ ["box1", "body0"], ["box2", "body0"], ]); const clipboardData = instanceText.onCopy?.(); expect(JSON.parse(clipboardData ?? "")).toMatchObject({ "@webstudio/instances/v0.1": { sourceOrigin: window.location.origin, rootInstanceIds: ["box1", "box2"], fragment: { children: [ { type: "id", value: "box1" }, { type: "id", value: "box2" }, ], instances: [ { id: "box1", type: "instance", component: "Box", children: [] }, { id: "box2", type: "instance", component: "Box", children: [] }, ], }, }, }); }); test("copies and pastes one rendered Collection Content Block with its resolved direct MDX source", async () => { setPageRoot("body"); $instances.set( toMap([ createInstance("body", "Body", [{ type: "id", value: "collection" }]), createInstance("collection", collectionComponent, [ { type: "id", value: "item" }, ]), createInstance("item", "Box", [{ type: "id", value: "block" }]), createInstance("block", blockComponent, [ { type: "id", value: "templates" }, { type: "id", value: "mdx-heading" }, ]), createInstance("templates", blockTemplateComponent, []), createInstance("mdx-heading", elementComponent, [ { type: "text", value: "Loaded from MDX" }, ]), ]) ); $dataSources.set( toMap([ { id: "item-source", scopeInstanceId: "collection", type: "parameter", name: "item", }, ]) ); $props.set( toMap([ { id: "collection-data", instanceId: "collection", name: "data", type: "json", value: [{ mdx: "post" }], }, { id: "collection-item", instanceId: "collection", name: "item", type: "parameter", value: "item-source", }, { id: "block-source", instanceId: "block", name: "src", type: "expression", value: `${encodeDataSourceVariable("item-source")}.mdx`, }, ] satisfies Prop[]) ); $assets.set( toMap([ { id: "post", projectId: "my-project", type: "file", format: "mdx", name: "post.mdx", size: 0, createdAt: "2026-01-01T00:00:00.000Z", meta: {}, }, ]) ); const renderScope = '["block","item","collection[0]","collection","body"]'; $externalContentRoots.set( new Map([ [ "post-root", { sourceBlockInstanceId: "block", sourceRenderScope: renderScope, blockInstanceId: "runtime-block", renderScope, instanceIds: new Set(), mutationRevision: 0, identity: { blockInstanceId: "block", assetId: "post", revision: "revision", contentRef: "post.mdx", format: "mdx", renderScope, }, }, ], ]) ); selectInstance(["item", "collection[0]", "collection", "body"]); const clipboardData = instanceText.onCopy?.(); const fragment = JSON.parse(clipboardData ?? "")[ "@webstudio/instance/v0.1" ]; expect(fragment.props).toContainEqual({ id: "block-source", instanceId: "block", name: "src", type: "asset", value: "post", }); expect(fragment.assets).toContainEqual( expect.objectContaining({ id: "post", format: "mdx" }) ); expect(fragment.assetPaths).toEqual({ post: "post.mdx" }); expect(fragment.instances).toEqual( expect.arrayContaining([ expect.objectContaining({ id: "block" }), expect.objectContaining({ id: "item" }), expect.objectContaining({ id: "templates" }), ]) ); expect(fragment.instances).not.toEqual( expect.arrayContaining([expect.objectContaining({ id: "mdx-heading" })]) ); const importAssets = vi .spyOn(builderApiTesting.api, "importAssets") .mockResolvedValue(new Map([["post", $assets.get().get("post")!]])); $instances.set( toMap([createInstance("target-body", "Body", [])] satisfies Instance[]) ); $props.set(new Map()); $dataSources.set(new Map()); setPageRoot("target-body"); selectInstance(["target-body"]); expect(await instanceText.onPaste?.(clipboardData ?? "")).toEqual( pasteHandled ); const pastedBlock = Array.from($instances.get().values()).find( (instance) => instance.component === blockComponent ); expect(pastedBlock).toBeDefined(); expect( Array.from($props.get().values()).find( (prop) => prop.instanceId === pastedBlock?.id && prop.name === "src" ) ).toEqual(expect.objectContaining({ type: "asset", value: "post" })); expect( Array.from($instances.get().values()).some( (instance) => instance.id === "mdx-heading" ) ).toBe(false); importAssets.mockRestore(); $externalContentRoots.set(new Map()); $props.set(new Map()); $assets.set(new Map()); setPageRoot("body0"); }); test("pastes multiple copied roots in order and selects pasted roots", async () => { $instances.set( toMap([ createInstance("body0", "Body", [ { type: "id", value: "box1" }, { type: "id", value: "box2" }, ]), createInstance("box1", "Box", [{ type: "id", value: "child" }]), createInstance("child", "Box", []), createInstance("box2", "Box", []), ] satisfies Instance[]) ); selectInstances([ ["box1", "body0"], ["box2", "body0"], ]); const clipboardData = instanceText.onCopy?.() ?? ""; selectInstance(["body0"]); const result = await instanceText.onPaste?.(clipboardData); expect(result).toEqual(pasteHandled); const bodyChildren = $instances.get().get("body0")?.children; const pastedRootIds = bodyChildren?.slice(2).map((child) => child.value); expect(bodyChildren).toEqual([ { type: "id", value: "box1" }, { type: "id", value: "box2" }, { type: "id", value: expect.any(String) }, { type: "id", value: expect.any(String) }, ]); expect(pastedRootIds?.[0]).not.toBe("box1"); expect(pastedRootIds?.[1]).not.toBe("box2"); expect($allSelectedInstanceSelectors.get()).toEqual([ [pastedRootIds?.[0], "body0"], [pastedRootIds?.[1], "body0"], ]); }); test("transfers assets from another deployment before inserting", async () => { $instances.set( toMap([createInstance("body0", "Body", [])] satisfies Instance[]) ); $assets.set(new Map()); selectInstance(["body0"]); const sourceAsset = { id: "source-asset", projectId: "source-project", name: "hero.png", type: "image", size: 128, format: "png", createdAt: "2026-01-01T00:00:00.000Z", description: null, meta: { width: 1200, height: 800 }, } satisfies Asset; const importedAsset = { ...sourceAsset, id: "imported-asset", projectId: "my-project", } satisfies Asset; const importAssets = vi .spyOn(builderApiTesting.api, "importAssets") .mockRejectedValueOnce(new Error("Source is unavailable")) .mockImplementation( async () => new Map([[sourceAsset.id, importedAsset]]) ); const clipboardData = JSON.stringify({ "@webstudio/instance/v0.1": { sourceOrigin: "https://source.example.com", instanceSelector: ["image", "source-body"], children: [{ type: "id", value: "image" }], instances: [createInstance("image", "Image", [])], assets: [sourceAsset], dataSources: [], resources: [], props: [ { id: "image-src", instanceId: "image", name: "src", type: "asset", value: sourceAsset.id, }, ], breakpoints: [], styleSourceSelections: [], styleSources: [], styles: [], }, }); expect(await instanceText.onPaste?.(clipboardData)).toEqual({ success: false, error: "Could not transfer assets from the source Webstudio deployment. Make sure it is reachable and try again.", }); expect($instances.get().get("body0")?.children).toEqual([]); expect(await instanceText.onPaste?.(clipboardData)).toEqual(pasteHandled); expect($instances.get().get("body0")?.children).toEqual([ { type: "id", value: expect.any(String) }, ]); expect(importAssets).toHaveBeenLastCalledWith("my-project", [ { asset: sourceAsset, url: "https://source.example.com/cgi/image/hero.png?format=raw", }, ]); expect(Array.from($props.get().values())).toEqual([ expect.objectContaining({ type: "asset", value: importedAsset.id }), ]); }); test("pastes a legacy invalid subtree with a warning", async () => { $instances.set( toMap([ createInstance("body0", "Body", [ { type: "id", value: "legacy-button" }, ]), { ...createInstance("legacy-button", elementComponent, [ { type: "id", value: "legacy-heading" }, ]), tag: "button", }, { ...createInstance("legacy-heading", elementComponent, []), tag: "h3", }, ] satisfies Instance[]) ); selectInstance(["legacy-button", "body0"]); const clipboardData = instanceText.onCopy?.() ?? ""; selectInstance(["body0"]); const warn = vi .spyOn(builderApiTesting.api.toast, "warn") .mockImplementation(() => {}); expect(await instanceText.onPaste?.(clipboardData)).toEqual(pasteHandled); expect($instances.get().get("body0")?.children).toHaveLength(2); expect(warn).toHaveBeenCalledWith( "Pasted with warning: Placing

element inside a