mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-10-30 16:47:54 +09:00
- Add Token class and token() helper in template package for JSX token support - Refactor insertFragmentWithConflictResolution to detectFragmentTokenConflicts - Refactor insertPageWithConflictResolution to detectPageTokenConflicts - Move dialog display to callers for better testability - Add conflictResolution parameter to page-utils - Add comprehensive tests for token functionality and conflict detection - Fix bug: remove .values() call on arrays in breakpoint handling
555 lines
16 KiB
TypeScript
555 lines
16 KiB
TypeScript
import { describe, test, expect, vi, beforeAll } from "vitest";
|
|
import { enableMapSet } from "immer";
|
|
import type {
|
|
Instance,
|
|
Instances,
|
|
Prop,
|
|
Props,
|
|
DataSource,
|
|
DataSources,
|
|
} from "@webstudio-is/sdk";
|
|
import {
|
|
encodeDataSourceVariable,
|
|
collectionComponent,
|
|
coreMetas,
|
|
portalComponent,
|
|
} from "@webstudio-is/sdk";
|
|
import type { Project } from "@webstudio-is/project";
|
|
import * as baseComponentMetas from "@webstudio-is/sdk-components-react/metas";
|
|
import { registerContainers } from "../sync/sync-stores";
|
|
import {
|
|
$instances,
|
|
$dataSources,
|
|
$pages,
|
|
$project,
|
|
$props,
|
|
$registeredComponentMetas,
|
|
} from "../nano-states";
|
|
import { instanceText } from "./plugin-instance";
|
|
import { createDefaultPages } from "@webstudio-is/project-build";
|
|
import { $awareness, selectInstance } from "../awareness";
|
|
import * as instanceUtils from "../instance-utils";
|
|
|
|
const expectString = expect.any(String) as unknown as string;
|
|
|
|
enableMapSet();
|
|
registerContainers();
|
|
|
|
// Mock detectFragmentTokenConflicts to always return no conflicts
|
|
beforeAll(() => {
|
|
vi.spyOn(instanceUtils, "detectFragmentTokenConflicts").mockReturnValue([]);
|
|
});
|
|
|
|
$registeredComponentMetas.set(
|
|
new Map(Object.entries({ ...baseComponentMetas, ...coreMetas }))
|
|
);
|
|
$project.set({ id: "my-project" } as Project);
|
|
$pages.set(
|
|
createDefaultPages({
|
|
homePageId: "home-page",
|
|
rootInstanceId: "body0",
|
|
})
|
|
);
|
|
$awareness.set({ pageId: "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", 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", [])])
|
|
);
|
|
});
|
|
});
|
|
|
|
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 () => {
|
|
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 () => {
|
|
$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(["box", "body"]);
|
|
const clipboardData = instanceText.onCopy?.() ?? "";
|
|
selectInstance(["portal", "body"]);
|
|
await instanceText.onPaste?.(clipboardData);
|
|
expect($instances.get()).toEqual(instances);
|
|
});
|
|
|
|
test("insert portal into its sibling", async () => {
|
|
$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("insert into portal fragment when portal is a target", async () => {
|
|
$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 [boxId, fragmentId] = getMapDifference(
|
|
prevInstances,
|
|
$instances.get()
|
|
).keys();
|
|
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
|
|
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", []),
|
|
])
|
|
);
|
|
});
|