Files
webstudio/apps/builder/app/shared/copy-paste/plugin-instance.test.ts
T
Bogdan Chadkin 685ca24552 Support actions copy paste (#1976)
Here added action copy paste support.

When all action dependencies are scoped to copied instance new variable
nams are ussed.

When some any dependency is not scoped execution is removed from copied
prop.
2023-07-28 01:53:58 +03:00

319 lines
8.4 KiB
TypeScript

import { describe, test, expect } from "@jest/globals";
import { enableMapSet } from "immer";
import type {
Instance,
Instances,
Prop,
Props,
DataSource,
DataSources,
Page,
} from "@webstudio-is/project-build";
import type { Project } from "@webstudio-is/project";
import { encodeDataSourceVariable } from "@webstudio-is/react-sdk";
import * as baseComponentMetas from "@webstudio-is/sdk-components-react/metas";
import { registerContainers } from "../sync";
import {
dataSourcesStore,
instancesStore,
pagesStore,
projectStore,
propsStore,
registeredComponentMetasStore,
selectedInstanceSelectorStore,
selectedPageIdStore,
} from "../nano-states";
import { onCopy, onPaste } from "./plugin-instance";
enableMapSet();
registerContainers();
registeredComponentMetasStore.set(new Map(Object.entries(baseComponentMetas)));
projectStore.set({ id: "my-project" } as Project);
pagesStore.set({
homePage: { id: "home-page", rootInstanceId: "body0" } as Page,
pages: [],
});
selectedPageIdStore.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("paste target", () => {
// 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", () => {
instancesStore.set(instances);
selectedInstanceSelectorStore.set(["box1", "body0"]);
const clipboardData = onCopy() ?? "";
selectedInstanceSelectorStore.set(["box2", "body0"]);
onPaste(clipboardData);
const instancesDifference = getMapDifference(
instances,
instancesStore.get()
);
const [newBox1] = instancesDifference.keys();
expect(instancesStore.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", () => {
instancesStore.set(instances);
selectedInstanceSelectorStore.set(["box1", "body0"]);
onPaste(onCopy() ?? "");
const instancesDifference = getMapDifference(
instances,
instancesStore.get()
);
const [newBox1] = instancesDifference.keys();
expect(instancesStore.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", [])])
);
});
});
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" },
},
{
id: "box2$stateInitial",
scopeInstanceId: "box2",
type: "expression",
name: "stateInitial",
code: `$ws$dataSource$box1$state === 'initial'`,
},
] satisfies DataSource[]);
const props: Props = toMap([
{
id: "box1$state",
instanceId: "box1",
type: "dataSource",
name: "state",
value: "box1$state",
},
{
id: "box2$state",
instanceId: "box2",
type: "dataSource",
name: "state",
value: "box1$state",
},
{
id: "box2$show",
instanceId: "box2",
type: "dataSource",
name: "show",
value: "box2$stateInitial",
},
{
id: "box2$onChange",
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", () => {
instancesStore.set(instances);
propsStore.set(props);
dataSourcesStore.set(dataSources);
selectedInstanceSelectorStore.set(["box1", "body0"]);
const clipboardData = onCopy() ?? "";
selectedInstanceSelectorStore.set(["body0"]);
onPaste(clipboardData);
const instancesDifference = getMapDifference(
instances,
instancesStore.get()
);
const [newBox1, newBox2] = instancesDifference.keys();
const dataSourcesDifference = getMapDifference(
dataSources,
dataSourcesStore.get()
);
const [newDataSource1, newDataSource2] = dataSourcesDifference.keys();
expect(dataSourcesDifference).toEqual(
toMap([
{
...dataSources.get("box1$state"),
id: newDataSource1,
scopeInstanceId: newBox1,
},
{
...dataSources.get("box2$stateInitial"),
id: newDataSource2,
scopeInstanceId: newBox2,
code: `${encodeDataSourceVariable(newDataSource1)} === 'initial'`,
},
])
);
const propsDifference = getMapDifference(props, propsStore.get());
const [newProp1, newProp2, newProp3, newProp4] = propsDifference.keys();
expect(propsDifference).toEqual(
toMap([
{
...props.get("box1$state"),
id: newProp1,
instanceId: newBox1,
value: newDataSource1,
},
{
...props.get("box2$state"),
id: newProp2,
instanceId: newBox2,
value: newDataSource1,
},
{
...props.get("box2$show"),
id: newProp3,
instanceId: newBox2,
value: newDataSource2,
},
{
...props.get("box2$onChange"),
id: newProp4,
instanceId: newBox2,
value: [
{
type: "execute",
args: ["value"],
code: `${encodeDataSourceVariable(newDataSource1)} = value`,
},
],
},
])
);
});
test("are inlined into props when not scoped to copied instances or depends on not scoped data source", () => {
instancesStore.set(instances);
propsStore.set(props);
dataSourcesStore.set(dataSources);
selectedInstanceSelectorStore.set(["box2", "box1", "body0"]);
const clipboardData = onCopy() ?? "";
selectedInstanceSelectorStore.set(["body0"]);
onPaste(clipboardData);
const instancesDifference = getMapDifference(
instances,
instancesStore.get()
);
const [newBox2] = instancesDifference.keys();
const dataSourcesDifference = getMapDifference(
dataSources,
dataSourcesStore.get()
);
expect(dataSourcesDifference).toEqual(new Map());
const propsDifference = getMapDifference(props, propsStore.get());
const [newProp1, newProp2, newProp3] = propsDifference.keys();
expect(propsDifference).toEqual(
toMap([
{
id: newProp1,
instanceId: newBox2,
type: "string",
name: "state",
value: "initial",
},
{
id: newProp2,
instanceId: newBox2,
type: "boolean",
name: "show",
value: true,
},
{
id: newProp3,
instanceId: newBox2,
type: "action",
name: "onChange",
value: [],
},
])
);
});
test.todo(
"preserve data sources not scoped to new instances within same scope"
);
});