import { describe, test, expect, vi } from "vitest"; import { enableMapSet } from "immer"; import { toast } from "@webstudio-is/design-system"; import type { Instance, Instances, Prop, Props, DataSource, DataSources, Resource, Resources, } from "@webstudio-is/sdk"; import { encodeDataSourceVariable, collectionComponent, coreMetas, elementComponent, blockTemplateComponent, 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 { $instances } from "~/shared/sync/data-stores"; import { $dataSources, $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 { initBuilderApi } from "../builder-api"; 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": { 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("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("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"]); initBuilderApi(); const previousWarn = window.__webstudio__$__builderApi.toast.warn; const warn = vi.fn(); window.__webstudio__$__builderApi.toast.warn = warn; 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