Files
webstudio/apps/builder/app/shared/copy-paste/plugin-instance.test.ts
Oleg Isonen 6e10ae3809 fix: Fix Builder paste transfer data handling and publish sync edge cases (#5861)
- Fix publish sync for empty JSON data variables
- Retry transient asset downloads once with shared sync-client backoff
- Fix Builder paste handling for Webstudio transfer data
- Add transfer/data-envelope terminology and colocated runtime
data-format modules
- Rename page clipboard runtime operation to page transfer semantics
- Report malformed Webstudio/Webflow paste data through paste results
instead of inserting it as text
- Add browser-level E2E coverage for instance, page, folder, template,
generic plugin, malformed data, native input, and content-mode paste
flows

## Details

- Allows empty JSON data variables to sync/publish correctly.
- Adds asset download retry coverage and uses shared backoff logic.
- Introduces runtime `data-formats` modules for data envelopes, instance
transfer data, and page transfer data.
- Removes stale clipboard aliases and `validateTemplateStructure`.
- Keeps clipboard as a Builder UI transport concern while
runtime/protocol use transfer-data terminology.
- Changes paste plugins to return expressive success/error results so
callers can show toast errors and stop fallback insertion.
- Prevents malformed Webstudio transfer payloads from falling through to
text/HTML/markdown insertion.
- Handles malformed Webflow-owned paste data without throwing.
- Adds shared copy-paste fragment insertion helpers to reduce duplicated
breakpoint-warning/fragment-data logic.
2026-07-10 13:51:55 +02:00

1677 lines
53 KiB
TypeScript

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";
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 = <T extends { id: string }>(item: T) =>
[item.id, item] as const;
const toMap = <T extends { id: string }>(list: T[]) =>
new Map(list.map(getIdValuePair));
const getMapDifference = <Type extends Map<unknown, unknown>>(
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("sanitizes multi-root clipboard root ids before paste", async () => {
$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 = JSON.parse(instanceText.onCopy?.() ?? "");
clipboardData["@webstudio/instances/v0.1"].rootInstanceIds = [
"box1",
"child",
"missing",
"box1",
];
selectInstance(["body0"]);
expect(await instanceText.onPaste?.(JSON.stringify(clipboardData))).toEqual(
pasteHandled
);
const bodyChildren = $instances.get().get("body0")?.children;
expect(bodyChildren).toEqual([
{ type: "id", value: "box1" },
{ type: "id", value: "box2" },
{ type: "id", value: expect.any(String) },
]);
clipboardData["@webstudio/instances/v0.1"].rootInstanceIds = ["missing"];
expect(await instanceText.onPaste?.(JSON.stringify(clipboardData))).toEqual(
pasteHandled
);
expect($instances.get().get("body0")?.children).toEqual(bodyChildren);
});
test("writes only copyable roots when multi-copy skips selected roots", () => {
$instances.set(
toMap([
createInstance("body0", "Body", [
{ type: "id", value: "box1" },
{ type: "id", value: "template" },
]),
createInstance("box1", "Box", []),
createInstance("template", blockTemplateComponent, []),
] satisfies Instance[])
);
selectInstances([
["template", "body0"],
["box1", "body0"],
]);
const clipboardData = instanceText.onCopy?.();
expect(JSON.parse(clipboardData ?? "")).toMatchObject({
"@webstudio/instances/v0.1": {
rootInstanceIds: ["box1"],
},
});
});
test("does not copy when no multi-selected root is copyable", () => {
const instances: Instances = toMap([
createInstance("body0", "Body", [
{ type: "id", value: "template1" },
{ type: "id", value: "template2" },
]),
createInstance("template1", blockTemplateComponent, []),
createInstance("template2", blockTemplateComponent, []),
] satisfies Instance[]);
$instances.set(instances);
selectInstances([
["template1", "body0"],
["template2", "body0"],
]);
expect(instanceText.onCopy?.()).toBeUndefined();
expect($instances.get()).toEqual(instances);
});
test("cuts multiple selected roots into one combined fragment and removes them", () => {
$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.onCut?.();
expect(JSON.parse(clipboardData ?? "")).toMatchObject({
"@webstudio/instances/v0.1": {
rootInstanceIds: ["box1", "box2"],
},
});
expect($instances.get().get("body0")?.children).toEqual([]);
expect($instances.get().has("box1")).toBe(false);
expect($instances.get().has("box2")).toBe(false);
expect($allSelectedInstanceSelectors.get()).toEqual([]);
});
test("cuts only copyable roots when multi-cut skips selected roots", () => {
$instances.set(
toMap([
createInstance("body0", "Body", [
{ type: "id", value: "box1" },
{ type: "id", value: "template" },
]),
createInstance("box1", "Box", []),
createInstance("template", blockTemplateComponent, []),
] satisfies Instance[])
);
selectInstances([
["template", "body0"],
["box1", "body0"],
]);
const clipboardData = instanceText.onCut?.();
expect(JSON.parse(clipboardData ?? "")).toMatchObject({
"@webstudio/instances/v0.1": {
rootInstanceIds: ["box1"],
},
});
expect($instances.get().get("body0")?.children).toEqual([
{ type: "id", value: "template" },
]);
expect($instances.get().has("box1")).toBe(false);
expect($instances.get().has("template")).toBe(true);
expect($allSelectedInstanceSelectors.get()).toEqual([]);
});
test("does not cut when no multi-selected root is copyable", () => {
const instances: Instances = toMap([
createInstance("body0", "Body", [
{ type: "id", value: "template1" },
{ type: "id", value: "template2" },
]),
createInstance("template1", blockTemplateComponent, []),
createInstance("template2", blockTemplateComponent, []),
] satisfies Instance[]);
$instances.set(instances);
selectInstances([
["template1", "body0"],
["template2", "body0"],
]);
expect(instanceText.onCut?.()).toBeUndefined();
expect($instances.get()).toEqual(instances);
expect($allSelectedInstanceSelectors.get()).toEqual([
["template1", "body0"],
["template2", "body0"],
]);
});
});
describe("paste target", () => {
test("pastes radix navigation menu fragment with viewport wrapper", async () => {
const previousMetas = $registeredComponentMetas.get();
$registeredComponentMetas.set(componentMetas);
$instances.set(toMap([createInstance("body0", "Body", [])]));
selectInstance(["body0"]);
const clipboardData = JSON.stringify({
"@webstudio/instance/v0.1": {
instanceSelector: ["nav", "source-parent"],
children: [{ type: "id", value: "nav" }],
instances: [
createInstance(
"nav",
"@webstudio-is/sdk-components-react-radix:NavigationMenu",
[
{ type: "id", value: "list" },
{ type: "id", value: "viewport-container" },
]
),
createInstance(
"list",
"@webstudio-is/sdk-components-react-radix:NavigationMenuList",
[{ type: "id", value: "item" }]
),
createInstance(
"item",
"@webstudio-is/sdk-components-react-radix:NavigationMenuItem",
[
{ type: "id", value: "trigger" },
{ type: "id", value: "content" },
]
),
createInstance(
"trigger",
"@webstudio-is/sdk-components-react-radix:NavigationMenuTrigger",
[{ type: "id", value: "button" }]
),
createInstance("button", "Button", [{ type: "id", value: "text" }]),
createInstance("text", "Text", [{ type: "text", value: "About" }]),
createInstance(
"content",
"@webstudio-is/sdk-components-react-radix:NavigationMenuContent",
[{ type: "id", value: "content-box" }]
),
createInstance("content-box", "Box", [
{ type: "id", value: "content-link" },
]),
createInstance("content-link", "Link", [
{ type: "text", value: "Documentation" },
]),
{
...createInstance("viewport-container", "Box", [
{ type: "id", value: "viewport" },
]),
label: "Viewport Container",
},
createInstance(
"viewport",
"@webstudio-is/sdk-components-react-radix:NavigationMenuViewport",
[]
),
],
assets: [],
dataSources: [],
resources: [],
props: [],
breakpoints: [],
styleSourceSelections: [],
styleSources: [],
styles: [],
},
});
const warn = vi.spyOn(toast, "warn").mockImplementation(() => "");
const result = await instanceText.onPaste?.(clipboardData);
expect(result).toEqual(pasteHandled);
expect(warn).not.toHaveBeenCalled();
const [pastedNavId] =
$instances
.get()
.get("body0")
?.children.map((child) => child.value) ?? [];
expect($instances.get().get(pastedNavId ?? "")?.component).toBe(
"@webstudio-is/sdk-components-react-radix:NavigationMenu"
);
warn.mockRestore();
$registeredComponentMetas.set(previousMetas);
});
// body0
// box1
// box2
const instances: Instances = toMap([
createInstance("body0", "Body", [
{ type: "id", value: "box1" },
{ type: "id", value: "box2" },
]),
createInstance("box1", "Box", []),
createInstance("box2", "Box", []),
] satisfies Instance[]);
test("is inside selected instance", async () => {
$instances.set(instances);
selectInstance(["box1", "body0"]);
const clipboardData = instanceText.onCopy?.() ?? "";
selectInstance(["box2", "body0"]);
await instanceText.onPaste?.(clipboardData);
const instancesDifference = getMapDifference(instances, $instances.get());
const [newBox1] = instancesDifference.keys();
expect($instances.get().get("box2")).toEqual(
createInstance("box2", "Box", [{ type: "id", value: newBox1 }])
);
expect(instancesDifference).toEqual(
toMap([createInstance(newBox1, "Box", [])])
);
});
test("is after selected instance when same as copied", async () => {
$instances.set(instances);
selectInstance(["box1", "body0"]);
await instanceText.onPaste?.(instanceText.onCopy?.() ?? "");
const instancesDifference = getMapDifference(instances, $instances.get());
const [newBox1] = instancesDifference.keys();
expect($instances.get().get("body0")).toEqual(
createInstance("body0", "Body", [
{ type: "id", value: "box1" },
{ type: "id", value: newBox1 },
{ type: "id", value: "box2" },
])
);
expect(instancesDifference).toEqual(
toMap([createInstance(newBox1, "Box", [])])
);
});
test("uses multi-selection paste target after the last selected sibling for old single-root clipboard data", async () => {
$instances.set(
toMap([
createInstance("body0", "Body", [
{ type: "id", value: "source" },
{ type: "id", value: "box1" },
{ type: "id", value: "box2" },
{ type: "id", value: "tail" },
]),
createInstance("source", "Box", []),
createInstance("box1", "Box", []),
createInstance("box2", "Box", []),
createInstance("tail", "Box", []),
] satisfies Instance[])
);
selectInstance(["source", "body0"]);
const clipboardData = instanceText.onCopy?.() ?? "";
selectInstances([
["box1", "body0"],
["box2", "body0"],
]);
await instanceText.onPaste?.(clipboardData);
const bodyChildren = $instances.get().get("body0")?.children;
expect(bodyChildren).toEqual([
{ type: "id", value: "source" },
{ type: "id", value: "box1" },
{ type: "id", value: "box2" },
{ type: "id", value: expect.any(String) },
{ type: "id", value: "tail" },
]);
const pastedId = bodyChildren?.[3]?.value;
expect(pastedId).not.toBe("source");
expect($allSelectedInstanceSelectors.get()).toEqual([[pastedId, "body0"]]);
});
test("uses multi-selection paste target after the last selected sibling for multi-root clipboard data", async () => {
$instances.set(
toMap([
createInstance("body0", "Body", [
{ type: "id", value: "source1" },
{ type: "id", value: "source2" },
{ type: "id", value: "box1" },
{ type: "id", value: "box2" },
{ type: "id", value: "tail" },
]),
createInstance("source1", "Box", []),
createInstance("source2", "Box", []),
createInstance("box1", "Box", []),
createInstance("box2", "Box", []),
createInstance("tail", "Box", []),
] satisfies Instance[])
);
selectInstances([
["source1", "body0"],
["source2", "body0"],
]);
const clipboardData = instanceText.onCopy?.() ?? "";
selectInstances([
["box1", "body0"],
["box2", "body0"],
]);
await instanceText.onPaste?.(clipboardData);
const bodyChildren = $instances.get().get("body0")?.children;
expect(bodyChildren).toEqual([
{ type: "id", value: "source1" },
{ type: "id", value: "source2" },
{ type: "id", value: "box1" },
{ type: "id", value: "box2" },
{ type: "id", value: expect.any(String) },
{ type: "id", value: expect.any(String) },
{ type: "id", value: "tail" },
]);
});
test("uses lowest common ancestor paste target for mixed-parent multi-selection", async () => {
$instances.set(
toMap([
createInstance("body0", "Body", [
{ type: "id", value: "source" },
{ type: "id", value: "section" },
{ type: "id", value: "footer" },
]),
createInstance("source", "Box", []),
createInstance("section", "Box", [
{ type: "id", value: "group" },
{ type: "id", value: "aside" },
{ type: "id", value: "tail" },
]),
createInstance("group", "Box", [{ type: "id", value: "box" }]),
createInstance("box", "Box", []),
createInstance("aside", "Box", []),
createInstance("tail", "Box", []),
createInstance("footer", "Box", []),
] satisfies Instance[])
);
selectInstance(["source", "body0"]);
const clipboardData = instanceText.onCopy?.() ?? "";
selectInstances([
["box", "group", "section", "body0"],
["aside", "section", "body0"],
]);
await instanceText.onPaste?.(clipboardData);
expect($instances.get().get("section")?.children).toEqual([
{ type: "id", value: "group" },
{ type: "id", value: "aside" },
{ type: "id", value: "tail" },
{ type: "id", value: expect.any(String) },
]);
expect($instances.get().get("body0")?.children).toEqual([
{ type: "id", value: "source" },
{ type: "id", value: "section" },
{ type: "id", value: "footer" },
]);
});
test("pastes into shared slot content", async () => {
$instances.set(
toMap([
createInstance("body0", "Body", [
{ type: "id", value: "source" },
{ type: "id", value: "slot1" },
{ type: "id", value: "slot2" },
]),
createInstance("source", "Box", []),
createInstance("slot1", "Slot", [{ type: "id", value: "fragment" }]),
createInstance("slot2", "Slot", [{ type: "id", value: "fragment" }]),
createInstance("fragment", "Fragment", [{ type: "id", value: "box" }]),
createInstance("box", "Box", []),
] satisfies Instance[])
);
selectInstance(["source", "body0"]);
const clipboardData = instanceText.onCopy?.() ?? "";
selectInstance(["slot1", "body0"]);
await instanceText.onPaste?.(clipboardData);
expectSlotsShareFragment($instances.get(), ["slot1", "slot2"]);
expect($instances.get().get("slot1")?.children).toEqual([
{ type: "id", value: "fragment" },
]);
expect($instances.get().get("slot2")?.children).toEqual([
{ type: "id", value: "fragment" },
]);
expect($instances.get().get("fragment")?.children).toEqual([
{ type: "id", value: "box" },
{ type: "id", value: expect.any(String) },
]);
});
test("pastes into legacy shared slot content", async () => {
$instances.set(
toMap([
createInstance("body0", "Body", [
{ type: "id", value: "source" },
{ type: "id", value: "slot1" },
{ type: "id", value: "slot2" },
]),
createInstance("source", "Box", []),
createInstance("slot1", "Slot", [{ type: "id", value: "box" }]),
createInstance("slot2", "Slot", [{ type: "id", value: "box" }]),
createInstance("box", "Box", []),
] satisfies Instance[])
);
selectInstance(["source", "body0"]);
const clipboardData = instanceText.onCopy?.() ?? "";
selectInstance(["slot1", "body0"]);
await instanceText.onPaste?.(clipboardData);
const fragmentId = expectSlotsShareFragment($instances.get(), [
"slot1",
"slot2",
]);
expect($instances.get().get("slot1")?.children).toEqual([
{ type: "id", value: fragmentId },
]);
expect($instances.get().get("slot2")?.children).toEqual([
{ type: "id", value: fragmentId },
]);
expect($instances.get().get(fragmentId ?? "")?.children).toEqual([
{ type: "id", value: "box" },
{ type: "id", value: expect.any(String) },
]);
});
test("pastes after selected shared slot child in shared slot content", async () => {
$instances.set(
toMap([
createInstance("body0", "Body", [
{ type: "id", value: "slot1" },
{ type: "id", value: "slot2" },
]),
createInstance("slot1", "Slot", [{ type: "id", value: "fragment" }]),
createInstance("slot2", "Slot", [{ type: "id", value: "fragment" }]),
createInstance("fragment", "Fragment", [{ type: "id", value: "box" }]),
createInstance("box", "Box", []),
] satisfies Instance[])
);
selectInstance(["box", "fragment", "slot1", "body0"]);
await instanceText.onPaste?.(instanceText.onCopy?.() ?? "");
expect($instances.get().get("slot1")?.children).toEqual([
{ type: "id", value: "fragment" },
]);
expect($instances.get().get("slot2")?.children).toEqual([
{ type: "id", value: "fragment" },
]);
expect($instances.get().get("fragment")?.children).toEqual([
{ type: "id", value: "box" },
{ type: "id", value: expect.any(String) },
]);
});
test("pastes after selected legacy shared slot child in shared slot content", async () => {
$instances.set(
toMap([
createInstance("body0", "Body", [
{ type: "id", value: "slot1" },
{ type: "id", value: "slot2" },
]),
createInstance("slot1", "Slot", [
{ type: "id", value: "box" },
{ type: "id", value: "heading" },
]),
createInstance("slot2", "Slot", [
{ type: "id", value: "box" },
{ type: "id", value: "heading" },
]),
createInstance("box", "Box", []),
createInstance("heading", "Heading", []),
] satisfies Instance[])
);
selectInstance(["box", "slot1", "body0"]);
await instanceText.onPaste?.(instanceText.onCopy?.() ?? "");
const fragmentId = expectSlotsShareFragment($instances.get(), [
"slot1",
"slot2",
]);
expect($instances.get().get(fragmentId ?? "")?.children).toEqual([
{ type: "id", value: "box" },
{ type: "id", value: expect.any(String) },
{ type: "id", value: "heading" },
]);
expectSlotTreeIntegrity($instances.get());
});
test("pastes after selected nested shared slot child in shared slot content", async () => {
$instances.set(
toMap([
createInstance("body0", "Body", [
{ type: "id", value: "slot1" },
{ type: "id", value: "slot2" },
]),
createInstance("slot1", "Slot", [{ type: "id", value: "fragment" }]),
createInstance("slot2", "Slot", [{ type: "id", value: "fragment" }]),
createInstance("fragment", "Fragment", [{ type: "id", value: "div" }]),
createInstance("div", "Box", [{ type: "id", value: "box" }]),
createInstance("box", "Box", []),
] satisfies Instance[])
);
selectInstance(["box", "div", "fragment", "slot1", "body0"]);
await instanceText.onPaste?.(instanceText.onCopy?.() ?? "");
expect($instances.get().get("slot1")?.children).toEqual([
{ type: "id", value: "fragment" },
]);
expect($instances.get().get("slot2")?.children).toEqual([
{ type: "id", value: "fragment" },
]);
expect($instances.get().get("fragment")?.children).toEqual([
{ type: "id", value: "div" },
]);
expect($instances.get().get("div")?.children).toEqual([
{ type: "id", value: "box" },
{ type: "id", value: expect.any(String) },
]);
});
test("pastes into nested shared slot container", async () => {
$instances.set(
toMap([
createInstance("body0", "Body", [
{ type: "id", value: "source" },
{ type: "id", value: "slot1" },
{ type: "id", value: "slot2" },
]),
createInstance("source", "Box", []),
createInstance("slot1", "Slot", [{ type: "id", value: "fragment" }]),
createInstance("slot2", "Slot", [{ type: "id", value: "fragment" }]),
createInstance("fragment", "Fragment", [{ type: "id", value: "div" }]),
createInstance("div", "Box", []),
] satisfies Instance[])
);
selectInstance(["source", "body0"]);
const clipboardData = instanceText.onCopy?.() ?? "";
selectInstance(["div", "fragment", "slot1", "body0"]);
await instanceText.onPaste?.(clipboardData);
expect($instances.get().get("slot1")?.children).toEqual([
{ type: "id", value: "fragment" },
]);
expect($instances.get().get("slot2")?.children).toEqual([
{ type: "id", value: "fragment" },
]);
expect($instances.get().get("fragment")?.children).toEqual([
{ type: "id", value: "div" },
]);
expect($instances.get().get("div")?.children).toEqual([
{ type: "id", value: expect.any(String) },
]);
});
test("copies shared slot child and pastes independent copy outside", async () => {
$instances.set(
toMap([
createInstance("body0", "Body", [
{ type: "id", value: "slot1" },
{ type: "id", value: "slot2" },
{ type: "id", value: "target" },
]),
createInstance("slot1", "Slot", [{ type: "id", value: "fragment" }]),
createInstance("slot2", "Slot", [{ type: "id", value: "fragment" }]),
createInstance("fragment", "Fragment", [{ type: "id", value: "box" }]),
createInstance("box", "Box", []),
createInstance("target", "Box", []),
] satisfies Instance[])
);
selectInstance(["box", "fragment", "slot1", "body0"]);
const clipboardData = instanceText.onCopy?.() ?? "";
selectInstance(["target", "body0"]);
await instanceText.onPaste?.(clipboardData);
expectSlotsShareFragment($instances.get(), ["slot1", "slot2"]);
const pastedBoxId = $instances.get().get("target")?.children[0]?.value;
expect(pastedBoxId).toEqual(expect.any(String));
expect(pastedBoxId).not.toBe("box");
expect($instances.get().get("slot1")?.children).toEqual([
{ type: "id", value: "fragment" },
]);
expect($instances.get().get("slot2")?.children).toEqual([
{ type: "id", value: "fragment" },
]);
expect($instances.get().get("fragment")?.children).toEqual([
{ type: "id", value: "box" },
]);
expect($instances.get().get(pastedBoxId ?? "")?.children).toEqual([]);
});
test("copies legacy shared slot child and pastes independent copy outside", async () => {
$instances.set(
toMap([
createInstance("body0", "Body", [
{ type: "id", value: "slot1" },
{ type: "id", value: "slot2" },
{ type: "id", value: "target" },
]),
createInstance("slot1", "Slot", [
{ type: "id", value: "box" },
{ type: "id", value: "heading" },
]),
createInstance("slot2", "Slot", [
{ type: "id", value: "box" },
{ type: "id", value: "heading" },
]),
createInstance("box", "Box", []),
createInstance("heading", "Heading", []),
createInstance("target", "Box", []),
] satisfies Instance[])
);
selectInstance(["box", "slot1", "body0"]);
const clipboardData = instanceText.onCopy?.() ?? "";
selectInstance(["target", "body0"]);
await instanceText.onPaste?.(clipboardData);
const pastedBoxId = $instances.get().get("target")?.children[0]?.value;
expect(pastedBoxId).toEqual(expect.any(String));
expect(pastedBoxId).not.toBe("box");
expect($instances.get().get(pastedBoxId ?? "")?.component).toBe("Box");
selectInstance(["box", "slot1", "body0"]);
instanceMutationUtils.convertInstance(elementComponent, "section");
expectSlotsShareFragment($instances.get(), ["slot1", "slot2"]);
expect($instances.get().get("box")?.component).toBe(elementComponent);
expect($instances.get().get("box")?.tag).toBe("section");
expect($instances.get().get(pastedBoxId ?? "")?.component).toBe("Box");
});
test("copies nested shared slot child with scoped data and pastes independent copy outside", async () => {
const dataSources: DataSources = toMap([
{
id: "boxVariable",
scopeInstanceId: "box",
type: "variable",
name: "Box Variable",
value: { type: "string", value: "value" },
},
] satisfies DataSource[]);
const props: Props = toMap([
{
id: "boxExpressionProp",
instanceId: "box",
name: "children",
type: "expression",
value: "$ws$dataSource$boxVariable",
},
{
id: "boxResourceProp",
instanceId: "box",
name: "resource",
type: "resource",
value: "boxResource",
},
] satisfies Prop[]);
const resources: Resources = toMap([
{
id: "boxResource",
name: "Box Resource",
method: "get",
url: "$ws$dataSource$boxVariable",
headers: [
{
name: "authorization",
value: "$ws$dataSource$boxVariable",
},
],
searchParams: [
{
name: "query",
value: "$ws$dataSource$boxVariable",
},
],
},
] satisfies Resource[]);
$instances.set(
toMap([
createInstance("body0", "Body", [
{ type: "id", value: "slot1" },
{ type: "id", value: "slot2" },
{ type: "id", value: "target" },
]),
createInstance("slot1", "Slot", [{ type: "id", value: "fragment" }]),
createInstance("slot2", "Slot", [{ type: "id", value: "fragment" }]),
createInstance("fragment", "Fragment", [{ type: "id", value: "div" }]),
createInstance("div", "Box", [{ type: "id", value: "box" }]),
createInstance("box", "Box", []),
createInstance("target", "Box", []),
] satisfies Instance[])
);
$dataSources.set(dataSources);
$props.set(props);
$resources.set(resources);
selectInstance(["box", "div", "fragment", "slot1", "body0"]);
const clipboardData = instanceText.onCopy?.() ?? "";
selectInstance(["target", "body0"]);
await instanceText.onPaste?.(clipboardData);
const pastedBoxId = $instances.get().get("target")?.children[0]?.value;
const dataSourcesDifference = getMapDifference(
dataSources,
$dataSources.get()
);
const [newDataSourceId] = dataSourcesDifference.keys();
const propsDifference = getMapDifference(props, $props.get());
const resourcesDifference = getMapDifference(resources, $resources.get());
const [newResourceId] = resourcesDifference.keys();
expect(pastedBoxId).toEqual(expect.any(String));
expect(pastedBoxId).not.toBe("box");
expect(dataSourcesDifference).toEqual(
toMap([
{
...dataSources.get("boxVariable"),
id: newDataSourceId,
scopeInstanceId: pastedBoxId,
},
])
);
expect(Array.from(propsDifference.values())).toEqual(
expect.arrayContaining([
{
id: expectString,
instanceId: pastedBoxId,
name: "children",
type: "expression",
value: encodeDataSourceVariable(newDataSourceId),
},
{
id: expectString,
instanceId: pastedBoxId,
name: "resource",
type: "resource",
value: newResourceId,
},
])
);
expect(resourcesDifference).toEqual(
toMap([
{
id: newResourceId,
name: "Box Resource",
method: "get",
url: encodeDataSourceVariable(newDataSourceId),
headers: [
{
name: "authorization",
value: encodeDataSourceVariable(newDataSourceId),
},
],
searchParams: [
{
name: "query",
value: encodeDataSourceVariable(newDataSourceId),
},
],
},
])
);
expect($instances.get().get("fragment")?.children).toEqual([
{ type: "id", value: "div" },
]);
expect($instances.get().get("div")?.children).toEqual([
{ type: "id", value: "box" },
]);
});
test("cuts shared slot child from all slot occurrences", () => {
$instances.set(
toMap([
createInstance("body0", "Body", [
{ type: "id", value: "slot1" },
{ type: "id", value: "slot2" },
]),
createInstance("slot1", "Slot", [{ type: "id", value: "fragment" }]),
createInstance("slot2", "Slot", [{ type: "id", value: "fragment" }]),
createInstance("fragment", "Fragment", [{ type: "id", value: "box" }]),
createInstance("box", "Box", []),
] satisfies Instance[])
);
selectInstance(["box", "fragment", "slot1", "body0"]);
const clipboardData = instanceText.onCut?.();
expect(JSON.parse(clipboardData ?? "")).toMatchObject({
"@webstudio/instance/v0.1": {
instanceSelector: ["box", "fragment", "slot1", "body0"],
children: [{ type: "id", value: "box" }],
},
});
expectSlotsShareFragment($instances.get(), ["slot1", "slot2"]);
expect($instances.get().get("slot1")?.children).toEqual([
{ type: "id", value: "fragment" },
]);
expect($instances.get().get("slot2")?.children).toEqual([
{ type: "id", value: "fragment" },
]);
expect($instances.get().get("fragment")?.children).toEqual([]);
expect($instances.get().has("box")).toBe(false);
});
test("cuts legacy shared slot child from all slot occurrences", () => {
$instances.set(
toMap([
createInstance("body0", "Body", [
{ type: "id", value: "slot1" },
{ type: "id", value: "slot2" },
]),
createInstance("slot1", "Slot", [
{ type: "id", value: "box" },
{ type: "id", value: "heading" },
]),
createInstance("slot2", "Slot", [
{ type: "id", value: "box" },
{ type: "id", value: "heading" },
]),
createInstance("box", "Box", []),
createInstance("heading", "Heading", []),
] satisfies Instance[])
);
selectInstance(["box", "slot1", "body0"]);
const clipboardData = instanceText.onCut?.();
expect(JSON.parse(clipboardData ?? "")).toMatchObject({
"@webstudio/instance/v0.1": {
instanceSelector: ["box", "slot1", "body0"],
children: [{ type: "id", value: "box" }],
},
});
const fragmentId = expectSlotsShareFragment($instances.get(), [
"slot1",
"slot2",
]);
expect($instances.get().get("slot1")?.children).toEqual([
{ type: "id", value: fragmentId },
]);
expect($instances.get().get("slot2")?.children).toEqual([
{ type: "id", value: fragmentId },
]);
expect($instances.get().get(fragmentId ?? "")?.children).toEqual([
{ type: "id", value: "heading" },
]);
expect($instances.get().has("box")).toBe(false);
});
test("cuts nested shared slot child from all slot occurrences", () => {
$instances.set(
toMap([
createInstance("body0", "Body", [
{ type: "id", value: "slot1" },
{ type: "id", value: "slot2" },
]),
createInstance("slot1", "Slot", [{ type: "id", value: "fragment" }]),
createInstance("slot2", "Slot", [{ type: "id", value: "fragment" }]),
createInstance("fragment", "Fragment", [{ type: "id", value: "div" }]),
createInstance("div", "Box", [{ type: "id", value: "box" }]),
createInstance("box", "Box", []),
] satisfies Instance[])
);
selectInstance(["box", "div", "fragment", "slot1", "body0"]);
const clipboardData = instanceText.onCut?.();
expect(JSON.parse(clipboardData ?? "")).toMatchObject({
"@webstudio/instance/v0.1": {
instanceSelector: ["box", "div", "fragment", "slot1", "body0"],
children: [{ type: "id", value: "box" }],
},
});
expect($instances.get().get("slot1")?.children).toEqual([
{ type: "id", value: "fragment" },
]);
expect($instances.get().get("slot2")?.children).toEqual([
{ type: "id", value: "fragment" },
]);
expect($instances.get().get("fragment")?.children).toEqual([
{ type: "id", value: "div" },
]);
expect($instances.get().get("div")?.children).toEqual([]);
expect($instances.get().has("box")).toBe(false);
});
test("cuts shared slot child and preserves shared siblings", () => {
$instances.set(
toMap([
createInstance("body0", "Body", [
{ type: "id", value: "slot1" },
{ type: "id", value: "slot2" },
]),
createInstance("slot1", "Slot", [{ type: "id", value: "fragment" }]),
createInstance("slot2", "Slot", [{ type: "id", value: "fragment" }]),
createInstance("fragment", "Fragment", [
{ type: "id", value: "box" },
{ type: "id", value: "heading" },
]),
createInstance("box", "Box", []),
createInstance("heading", "Heading", []),
] satisfies Instance[])
);
selectInstance(["box", "fragment", "slot1", "body0"]);
const clipboardData = instanceText.onCut?.();
expect(JSON.parse(clipboardData ?? "")).toMatchObject({
"@webstudio/instance/v0.1": {
instanceSelector: ["box", "fragment", "slot1", "body0"],
children: [{ type: "id", value: "box" }],
},
});
expect($instances.get().get("slot1")?.children).toEqual([
{ type: "id", value: "fragment" },
]);
expect($instances.get().get("slot2")?.children).toEqual([
{ type: "id", value: "fragment" },
]);
expect($instances.get().get("fragment")?.children).toEqual([
{ type: "id", value: "heading" },
]);
expect($instances.get().has("box")).toBe(false);
});
});
describe("data sources", () => {
// body0
// box1
// box2
const instances: Instances = toMap([
createInstance("body0", "Body", [{ type: "id", value: "box1" }]),
createInstance("box1", "Box", [{ type: "id", value: "box2" }]),
createInstance("box2", "Box", []),
] satisfies Instance[]);
const dataSources: DataSources = toMap([
{
id: "box1$state",
scopeInstanceId: "box1",
type: "variable",
name: "state",
value: { type: "string", value: "initial" },
},
] satisfies DataSource[]);
const props: Props = toMap([
{
id: "box1$stateProp",
instanceId: "box1",
name: "state",
type: "expression",
value: "$ws$dataSource$box1$state",
},
{
id: "box2$stateProp",
instanceId: "box2",
name: "state",
type: "expression",
value: "$ws$dataSource$box1$state",
},
{
id: "box2$showProp",
instanceId: "box2",
name: "show",
type: "expression",
value: `$ws$dataSource$box1$state === 'initial'`,
},
{
id: "box2$onChangeProp",
instanceId: "box2",
type: "action",
name: "onChange",
value: [
{
type: "execute",
args: ["value"],
code: `$ws$dataSource$box1$state = value`,
},
],
},
] satisfies Prop[]);
test("are copy pasted when scoped to copied instances", async () => {
$instances.set(instances);
$dataSources.set(dataSources);
$props.set(props);
selectInstance(["box1", "body0"]);
const clipboardData = instanceText.onCopy?.() ?? "";
selectInstance(["body0"]);
await instanceText.onPaste?.(clipboardData);
const instancesDifference = getMapDifference(instances, $instances.get());
const [newBox1, newBox2] = instancesDifference.keys();
const dataSourcesDifference = getMapDifference(
dataSources,
$dataSources.get()
);
const [newDataSource1] = dataSourcesDifference.keys();
expect(dataSourcesDifference).toEqual(
toMap([
{
...dataSources.get("box1$state"),
id: newDataSource1,
scopeInstanceId: newBox1,
},
])
);
const propsDifference = getMapDifference(props, $props.get());
const [newProp1, newProp2, newProp3, newProp4] = propsDifference.keys();
expect(propsDifference).toEqual(
toMap([
{
id: newProp1,
instanceId: newBox1,
name: "state",
type: "expression",
value: encodeDataSourceVariable(newDataSource1),
},
{
id: newProp2,
instanceId: newBox2,
name: "state",
type: "expression",
value: encodeDataSourceVariable(newDataSource1),
},
{
id: newProp3,
instanceId: newBox2,
name: "show",
type: "expression",
value: `${encodeDataSourceVariable(newDataSource1)} === 'initial'`,
},
{
id: newProp4,
instanceId: newBox2,
name: "onChange",
type: "action",
value: [
{
type: "execute",
args: ["value"],
code: `${encodeDataSourceVariable(newDataSource1)} = value`,
},
],
},
])
);
});
test("preserve data sources outside of scope when pasted within their scope", async () => {
$instances.set(instances);
$props.set(props);
$dataSources.set(dataSources);
selectInstance(["box2", "box1", "body0"]);
const clipboardData = instanceText.onCopy?.() ?? "";
selectInstance(["box1", "body0"]);
await instanceText.onPaste?.(clipboardData);
const instancesDifference = getMapDifference(instances, $instances.get());
const [newBox2] = instancesDifference.keys();
const dataSourcesDifference = getMapDifference(
dataSources,
$dataSources.get()
);
expect(dataSourcesDifference).toEqual(new Map());
const propsDifference = getMapDifference(props, $props.get());
const [newProp1, newProp2, newProp3] = propsDifference.keys();
expect(propsDifference).toEqual(
toMap([
{
id: newProp1,
instanceId: newBox2,
name: "state",
type: "expression",
value: `$ws$dataSource$box1$state`,
},
{
id: newProp2,
instanceId: newBox2,
name: "show",
type: "expression",
value: `$ws$dataSource$box1$state === 'initial'`,
},
{
id: newProp3,
instanceId: newBox2,
type: "action",
name: "onChange",
value: [
{
args: ["value"],
code: "$ws$dataSource$box1$state = value",
type: "execute",
},
],
},
])
);
});
test("copy parameter prop with new data source", async () => {
setPageRoot("body");
const instances: Instances = toMap([
createInstance("body", "Body", [{ type: "id", value: "list" }]),
createInstance("list", collectionComponent, []),
] satisfies Instance[]);
const dataSources: DataSources = toMap([
{
id: "itemDataSource",
scopeInstanceId: "list",
type: "parameter",
name: "item",
},
] satisfies DataSource[]);
const props: Props = toMap([
{
id: "dataProp",
instanceId: "list",
name: "data",
type: "json",
value: [],
},
{
id: "itemProp",
instanceId: "list",
name: "item",
type: "parameter",
value: "itemDataSource",
},
] satisfies Prop[]);
$instances.set(instances);
$props.set(props);
$dataSources.set(dataSources);
selectInstance(["list", "body"]);
const clipboardData = instanceText.onCopy?.() ?? "";
selectInstance(["body"]);
await instanceText.onPaste?.(clipboardData);
const instancesDifference = getMapDifference(instances, $instances.get());
const [collectionId] = instancesDifference.keys();
const dataSourcesDifference = getMapDifference(
dataSources,
$dataSources.get()
);
const [itemDataSourceId] = dataSourcesDifference.keys();
expect(dataSourcesDifference).toEqual(
new Map([
[
itemDataSourceId,
{
id: itemDataSourceId,
scopeInstanceId: collectionId,
type: "parameter",
name: "item",
},
],
])
);
const propsDifference = getMapDifference(props, $props.get());
const [dataPropId, itemPropId] = propsDifference.keys();
expect(propsDifference).toEqual(
toMap([
{
id: dataPropId,
instanceId: collectionId,
name: "data",
type: "json",
value: [],
},
{
id: itemPropId,
instanceId: collectionId,
name: "item",
type: "parameter",
value: itemDataSourceId,
},
])
);
});
});
test("when paste into copied instance insert after it", async () => {
setPageRoot("body");
$instances.set(
toMap([
createInstance("body", "Body", [{ type: "id", value: "box" }]),
createInstance("box", "Box", []),
])
);
selectInstance(["box", "body"]);
const clipboardData = instanceText.onCopy?.() ?? "";
await instanceText.onPaste?.(clipboardData);
expect($instances.get()).toEqual(
toMap([
createInstance("body", "Body", [
{ type: "id", value: "box" },
{ type: "id", value: expectString },
]),
createInstance("box", "Box", []),
createInstance(expectString, "Box", []),
])
);
});
test("prevent pasting portal into own descendants", async () => {
const instances = toMap([
createInstance("body", "Body", [{ type: "id", value: "portal" }]),
createInstance("portal", portalComponent, [
{ type: "id", value: "fragment" },
]),
createInstance("fragment", "Fragment", [{ type: "id", value: "box" }]),
createInstance("box", "Box", []),
]);
$instances.set(instances);
selectInstance(["portal", "body"]);
const clipboardData = instanceText.onCopy?.() ?? "";
selectInstance(["fragment", "portal", "body"]);
await instanceText.onPaste?.(clipboardData);
expect($instances.get()).toEqual(instances);
});
test("prevent pasting portal into copy of it", async () => {
const instances = toMap([
createInstance("body", "Body", [
{ type: "id", value: "portal1" },
{ type: "id", value: "portal2" },
]),
createInstance("portal1", portalComponent, [
{ type: "id", value: "fragment" },
]),
createInstance("portal2", portalComponent, [
{ type: "id", value: "fragment" },
]),
createInstance("fragment", "Fragment", []),
]);
$instances.set(instances);
selectInstance(["portal1", "body"]);
const clipboardData = instanceText.onCopy?.() ?? "";
selectInstance(["portal2", "body"]);
await instanceText.onPaste?.(clipboardData);
expect($instances.get()).toEqual(instances);
});
test("insert portal into its sibling", async () => {
setPageRoot("body");
$instances.set(
toMap([
createInstance("body", "Body", [
{ type: "id", value: "portal" },
{ type: "id", value: "sibling" },
]),
createInstance("portal", portalComponent, [
{ type: "id", value: "fragment" },
]),
createInstance("fragment", "Fragment", []),
createInstance("sibling", "Box", []),
])
);
selectInstance(["portal", "body"]);
const clipboardData = instanceText.onCopy?.() ?? "";
selectInstance(["sibling", "body"]);
await instanceText.onPaste?.(clipboardData);
expect($instances.get()).toEqual(
toMap([
createInstance("body", "Body", [
{ type: "id", value: "portal" },
{ type: "id", value: "sibling" },
]),
createInstance("portal", portalComponent, [
{ type: "id", value: "fragment" },
]),
createInstance("fragment", "Fragment", []),
createInstance("sibling", "Box", [{ type: "id", value: expectString }]),
createInstance(expectString, portalComponent, [
{ type: "id", value: "fragment" },
]),
])
);
});
test("inserts multi-root clipboard into descendant of copied non-portal root", async () => {
setPageRoot("body");
$instances.set(
toMap([
createInstance("body", "Body", [
{ type: "id", value: "portal" },
{ type: "id", value: "box" },
]),
createInstance("portal", portalComponent, [
{ type: "id", value: "fragment" },
]),
createInstance("fragment", "Fragment", []),
createInstance("box", "Box", [{ type: "id", value: "target" }]),
createInstance("target", "Box", []),
])
);
selectInstances([
["portal", "body"],
["box", "body"],
]);
const clipboardData = instanceText.onCopy?.() ?? "";
selectInstance(["target", "box", "body"]);
await instanceText.onPaste?.(clipboardData);
const targetChildren = $instances.get().get("target")?.children;
const [pastedPortalId, pastedBoxId] =
targetChildren?.map((child) => (child.type === "id" ? child.value : "")) ??
[];
expect(targetChildren).toEqual([
{ type: "id", value: expectString },
{ type: "id", value: expectString },
]);
expect($instances.get().get(pastedPortalId ?? "")?.component).toBe(
portalComponent
);
expect($instances.get().get(pastedBoxId ?? "")?.component).toBe("Box");
});
test("insert into portal fragment when portal is a target", async () => {
setPageRoot("body");
$instances.set(
toMap([
createInstance("body", "Body", [
{ type: "id", value: "portal" },
{ type: "id", value: "box" },
]),
createInstance("portal", portalComponent, []),
createInstance("box", "Box", []),
])
);
selectInstance(["box", "body"]);
const clipboardData = instanceText.onCopy?.() ?? "";
selectInstance(["portal", "body"]);
// fragment not exists
const prevInstances = $instances.get();
await instanceText.onPaste?.(clipboardData);
const newInstances = getMapDifference(prevInstances, $instances.get());
const boxId = Array.from(newInstances.values()).find(
(instance) => instance.component === "Box"
)?.id;
const fragmentId = Array.from(newInstances.values()).find(
(instance) => instance.component === "Fragment"
)?.id;
if (boxId === undefined || fragmentId === undefined) {
throw Error("Expected pasted Box and Fragment instances");
}
expect($instances.get()).toEqual(
toMap([
createInstance("body", "Body", [
{ type: "id", value: "portal" },
{ type: "id", value: "box" },
]),
createInstance("portal", portalComponent, [
{ type: "id", value: fragmentId },
]),
createInstance(fragmentId, "Fragment", [{ type: "id", value: boxId }]),
createInstance("box", "Box", []),
createInstance(boxId, "Box", []),
])
);
// fragment already exists
selectInstance(["portal", "body"]);
await instanceText.onPaste?.(clipboardData);
expect($instances.get()).toEqual(
toMap([
createInstance("body", "Body", [
{ type: "id", value: "portal" },
{ type: "id", value: "box" },
]),
createInstance("portal", portalComponent, [
{ type: "id", value: fragmentId },
]),
createInstance(fragmentId, "Fragment", [
{ type: "id", value: boxId },
{ type: "id", value: expectString },
]),
createInstance("box", "Box", []),
createInstance(boxId, "Box", []),
createInstance(expectString, "Box", []),
])
);
});