mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-07 04:23:40 +09:00
Fix https://github.com/webstudio-is/webstudio/issues/2516 Here added "duplicate" button to page settings and implemented logic based on recent instances slice utilities. ## Steps for reproduction 1. open pages 2. click on page 3. click "copy" button
1821 lines
49 KiB
TypeScript
1821 lines
49 KiB
TypeScript
import { enableMapSet } from "immer";
|
|
import { describe, test, expect, beforeEach } from "@jest/globals";
|
|
import {
|
|
encodeDataSourceVariable,
|
|
portalComponent,
|
|
collectionComponent,
|
|
type WsComponentMeta,
|
|
} from "@webstudio-is/react-sdk";
|
|
import * as defaultMetas from "@webstudio-is/sdk-components-react/metas";
|
|
import type {
|
|
Asset,
|
|
Instance,
|
|
Instances,
|
|
Page,
|
|
Prop,
|
|
StyleDecl,
|
|
StyleDeclKey,
|
|
} from "@webstudio-is/sdk";
|
|
import type { StyleProperty, StyleValue } from "@webstudio-is/css-engine";
|
|
import {
|
|
computeInstancesConstraints,
|
|
findClosestDroppableComponentIndex,
|
|
findClosestDroppableTarget,
|
|
findClosestEditableInstanceSelector,
|
|
insertTemplateData,
|
|
type InsertConstraints,
|
|
deleteInstance,
|
|
getInstancesSlice,
|
|
insertInstancesSliceCopy,
|
|
reparentInstance,
|
|
} from "./instance-utils";
|
|
import {
|
|
$assets,
|
|
$breakpoints,
|
|
$dataSources,
|
|
$instances,
|
|
$pages,
|
|
$project,
|
|
$props,
|
|
$styleSourceSelections,
|
|
$styleSources,
|
|
$styles,
|
|
assetsStore,
|
|
breakpointsStore,
|
|
dataSourcesStore,
|
|
instancesStore,
|
|
propsStore,
|
|
registeredComponentMetasStore,
|
|
styleSourceSelectionsStore,
|
|
styleSourcesStore,
|
|
stylesStore,
|
|
} from "./nano-states";
|
|
import { registerContainers } from "./sync";
|
|
import type { Project } from "@webstudio-is/project";
|
|
|
|
enableMapSet();
|
|
registerContainers();
|
|
|
|
const defaultMetasMap = new Map(Object.entries(defaultMetas));
|
|
|
|
const createFakeComponentMetas = (
|
|
itemMeta: Partial<WsComponentMeta>,
|
|
anotherItemMeta?: Partial<WsComponentMeta>
|
|
): Map<string, WsComponentMeta> => {
|
|
const base = {
|
|
label: "",
|
|
Icon: () => null,
|
|
};
|
|
const configs = {
|
|
Item: { ...base, type: "container", ...itemMeta },
|
|
AnotherItem: { ...base, type: "container", ...anotherItemMeta },
|
|
Bold: { ...base, type: "rich-text-child" },
|
|
Text: { ...base, type: "container" },
|
|
Form: { ...base, type: "container" },
|
|
Box: { ...base, type: "container" },
|
|
Div: { ...base, type: "container" },
|
|
Body: { ...base, type: "container" },
|
|
} as const;
|
|
return new Map(Object.entries(configs)) as Map<string, WsComponentMeta>;
|
|
};
|
|
|
|
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 createInstance = (
|
|
id: Instance["id"],
|
|
component: string,
|
|
children: Instance["children"]
|
|
): Instance => {
|
|
return { type: "instance", id, component, children };
|
|
};
|
|
|
|
const createInstancePair = (
|
|
id: Instance["id"],
|
|
component: string,
|
|
children: Instance["children"]
|
|
): [Instance["id"], Instance] => {
|
|
return [id, { type: "instance", id, component, children }];
|
|
};
|
|
|
|
const createStyleDecl = (
|
|
styleSourceId: string,
|
|
breakpointId: string,
|
|
property: StyleProperty,
|
|
value: StyleValue | string
|
|
): StyleDecl => ({
|
|
styleSourceId,
|
|
breakpointId,
|
|
property,
|
|
value: typeof value === "string" ? { type: "unparsed", value } : value,
|
|
});
|
|
|
|
const createStyleDeclPair = (
|
|
styleSourceId: string,
|
|
breakpointId: string,
|
|
property: StyleProperty,
|
|
value: StyleValue | string
|
|
): [StyleDeclKey, StyleDecl] => [
|
|
`${styleSourceId}:${breakpointId}:${property}:`,
|
|
createStyleDecl(styleSourceId, breakpointId, property, value),
|
|
];
|
|
|
|
const createProp = (instanceId: string, id: string, name: string): Prop => ({
|
|
id,
|
|
instanceId,
|
|
name,
|
|
type: "string",
|
|
value: id,
|
|
});
|
|
|
|
const createImageAsset = (id: string, name = "", projectId = ""): Asset => {
|
|
return {
|
|
id,
|
|
type: "image",
|
|
format: "",
|
|
name,
|
|
description: "",
|
|
projectId,
|
|
createdAt: "",
|
|
size: 0,
|
|
meta: { width: 0, height: 0 },
|
|
};
|
|
};
|
|
|
|
const createFontAsset = (id: string, family: string): Asset => {
|
|
return {
|
|
id,
|
|
type: "font",
|
|
format: "woff",
|
|
name: "",
|
|
description: "",
|
|
projectId: "",
|
|
createdAt: "",
|
|
size: 0,
|
|
meta: { style: "normal", family, variationAxes: {} },
|
|
};
|
|
};
|
|
|
|
const emptyInsertConstraints: InsertConstraints = {
|
|
requiredAncestors: new Set(),
|
|
invalidAncestors: new Set(),
|
|
};
|
|
|
|
describe("find closest editable instance selector", () => {
|
|
test("searches closest container", () => {
|
|
const instances: Instances = new Map([
|
|
createInstancePair("body", "Body", [{ type: "id", value: "box" }]),
|
|
createInstancePair("box", "Box", [
|
|
{ type: "text", value: "some text" },
|
|
{ type: "id", value: "bold" },
|
|
]),
|
|
createInstancePair("bold", "Bold", [
|
|
{ type: "text", value: "some-bold" },
|
|
]),
|
|
]);
|
|
expect(
|
|
findClosestEditableInstanceSelector(
|
|
["bold", "box", "body"],
|
|
instances,
|
|
createFakeComponentMetas({})
|
|
)
|
|
).toEqual(["box", "body"]);
|
|
expect(
|
|
findClosestEditableInstanceSelector(
|
|
["box", "body"],
|
|
instances,
|
|
createFakeComponentMetas({})
|
|
)
|
|
).toEqual(["box", "body"]);
|
|
});
|
|
|
|
test("skips when container has anything except rich-text-child or text", () => {
|
|
const instances: Instances = new Map([
|
|
createInstancePair("body", "Body", [{ type: "id", value: "box" }]),
|
|
createInstancePair("box", "Box", [
|
|
{ type: "text", value: "some text" },
|
|
{ type: "id", value: "bold" },
|
|
{ type: "id", value: "child-box" },
|
|
]),
|
|
createInstancePair("bold", "Bold", [
|
|
{ type: "text", value: "some-bold" },
|
|
]),
|
|
createInstancePair("child-box", "Box", [
|
|
{ type: "text", value: "child-box" },
|
|
]),
|
|
]);
|
|
expect(
|
|
findClosestEditableInstanceSelector(
|
|
["bold", "box", "body"],
|
|
instances,
|
|
createFakeComponentMetas({})
|
|
)
|
|
).toEqual(undefined);
|
|
});
|
|
|
|
test("considers empty container as editable", () => {
|
|
const instances: Instances = new Map([
|
|
createInstancePair("body", "Body", []),
|
|
]);
|
|
expect(
|
|
findClosestEditableInstanceSelector(
|
|
["body"],
|
|
instances,
|
|
createFakeComponentMetas({})
|
|
)
|
|
).toEqual(["body"]);
|
|
});
|
|
});
|
|
|
|
describe("compute instances constraints", () => {
|
|
const base = {
|
|
type: "container",
|
|
label: "",
|
|
icon: "",
|
|
} as const;
|
|
|
|
test("combine required ancestors excluding already resolved ones", () => {
|
|
const metas = new Map<string, WsComponentMeta>([
|
|
["Button", { ...base, requiredAncestors: ["Form"] }],
|
|
["Checkbox", { ...base, requiredAncestors: ["Form", "Label"] }],
|
|
["Label", { ...base, requiredAncestors: ["Body"] }],
|
|
]);
|
|
// button
|
|
// label
|
|
// checkbox
|
|
const instances = new Map<Instance["id"], Instance>([
|
|
createInstancePair("button", "Button", []),
|
|
createInstancePair("label", "Label", [{ type: "id", value: "checkbox" }]),
|
|
createInstancePair("checkbox", "Checkbox", []),
|
|
]);
|
|
expect(
|
|
computeInstancesConstraints(metas, instances, ["button", "label"])
|
|
).toEqual({
|
|
requiredAncestors: new Set(["Body", "Form"]),
|
|
invalidAncestors: new Set(),
|
|
});
|
|
});
|
|
|
|
test("combine invalid ancestors of all instances", () => {
|
|
const metas = new Map<string, WsComponentMeta>([
|
|
["Button", { ...base, invalidAncestors: ["Button"] }],
|
|
["Form", { ...base, invalidAncestors: ["Button", "Form"] }],
|
|
]);
|
|
// form
|
|
// button
|
|
const instances = new Map<Instance["id"], Instance>([
|
|
createInstancePair("form", "Form", [{ type: "id", value: "button" }]),
|
|
createInstancePair("button", "Button", []),
|
|
]);
|
|
expect(computeInstancesConstraints(metas, instances, ["form"])).toEqual({
|
|
requiredAncestors: new Set(),
|
|
invalidAncestors: new Set(["Button", "Form"]),
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("find closest droppable component index", () => {
|
|
test("finds container", () => {
|
|
expect(
|
|
findClosestDroppableComponentIndex(
|
|
createFakeComponentMetas({}),
|
|
["Box", "Body"],
|
|
emptyInsertConstraints
|
|
)
|
|
).toEqual(0);
|
|
});
|
|
|
|
test("skips non containers", () => {
|
|
expect(
|
|
findClosestDroppableComponentIndex(
|
|
createFakeComponentMetas({}),
|
|
["Bold", "Italic", "Text", "Box", "Body"],
|
|
emptyInsertConstraints
|
|
)
|
|
).toEqual(2);
|
|
});
|
|
|
|
test("considers invalid ancestors", () => {
|
|
expect(
|
|
findClosestDroppableComponentIndex(
|
|
createFakeComponentMetas({}),
|
|
["Box", "Item", "Body"],
|
|
{
|
|
requiredAncestors: new Set(),
|
|
invalidAncestors: new Set(["Item"]),
|
|
}
|
|
)
|
|
).toEqual(2);
|
|
});
|
|
|
|
test("requires some ancestor", () => {
|
|
expect(
|
|
findClosestDroppableComponentIndex(
|
|
createFakeComponentMetas({}),
|
|
["Box", "Body"],
|
|
{
|
|
requiredAncestors: new Set(["Form"]),
|
|
invalidAncestors: new Set(),
|
|
}
|
|
)
|
|
).toEqual(-1);
|
|
expect(
|
|
findClosestDroppableComponentIndex(
|
|
createFakeComponentMetas({}),
|
|
["Box", "Form", "Body"],
|
|
{
|
|
requiredAncestors: new Set(["Form"]),
|
|
invalidAncestors: new Set(),
|
|
}
|
|
)
|
|
).toEqual(0);
|
|
});
|
|
|
|
test("considers both required and invalid ancestors", () => {
|
|
expect(
|
|
findClosestDroppableComponentIndex(
|
|
createFakeComponentMetas({}),
|
|
["Div", "Box", "Form", "Body"],
|
|
{
|
|
requiredAncestors: new Set(["Form"]),
|
|
invalidAncestors: new Set(["Box"]),
|
|
}
|
|
)
|
|
).toEqual(2);
|
|
expect(
|
|
findClosestDroppableComponentIndex(
|
|
createFakeComponentMetas({}),
|
|
["Div", "Form", "Box", "Body"],
|
|
{
|
|
requiredAncestors: new Set(["Form"]),
|
|
invalidAncestors: new Set(["Box"]),
|
|
}
|
|
)
|
|
).toEqual(-1);
|
|
});
|
|
});
|
|
|
|
describe("find closest droppable target", () => {
|
|
const createInstancePair = (
|
|
id: Instance["id"],
|
|
component: string,
|
|
children: Instance["children"]
|
|
): [Instance["id"], Instance] => {
|
|
return [id, { type: "instance", id, component, children }];
|
|
};
|
|
|
|
test("puts in the end if closest instance is container", () => {
|
|
const instances = new Map([
|
|
createInstancePair("body", "Body", [{ type: "id", value: "box" }]),
|
|
createInstancePair("box", "Box", [{ type: "id", value: "paragraph" }]),
|
|
createInstancePair("paragraph", "Paragraph", [
|
|
{ type: "id", value: "bold" },
|
|
]),
|
|
]);
|
|
expect(
|
|
findClosestDroppableTarget(
|
|
defaultMetasMap,
|
|
instances,
|
|
["box", "body"],
|
|
emptyInsertConstraints
|
|
)
|
|
).toEqual({
|
|
parentSelector: ["box", "body"],
|
|
position: "end",
|
|
});
|
|
});
|
|
|
|
test("puts in the end of root instance", () => {
|
|
const instances = new Map([
|
|
createInstancePair("body", "Body", [{ type: "id", value: "box" }]),
|
|
createInstancePair("box", "Box", [{ type: "id", value: "paragraph" }]),
|
|
]);
|
|
expect(
|
|
findClosestDroppableTarget(
|
|
defaultMetasMap,
|
|
instances,
|
|
["body"],
|
|
emptyInsertConstraints
|
|
)
|
|
).toEqual({
|
|
parentSelector: ["body"],
|
|
position: "end",
|
|
});
|
|
});
|
|
|
|
test("finds closest container and puts after its child within selection", () => {
|
|
const instances = new Map([
|
|
createInstancePair("body", "Body", [{ type: "id", value: "paragraph" }]),
|
|
createInstancePair("paragraph", "Paragraph", [
|
|
{ type: "id", value: "bold" },
|
|
]),
|
|
createInstancePair("bold", "Bold", []),
|
|
]);
|
|
expect(
|
|
findClosestDroppableTarget(
|
|
defaultMetasMap,
|
|
instances,
|
|
["bold", "paragraph", "body"],
|
|
emptyInsertConstraints
|
|
)
|
|
).toEqual({
|
|
parentSelector: ["paragraph", "body"],
|
|
position: 1,
|
|
});
|
|
});
|
|
});
|
|
|
|
test("insert template data with only new style sources", () => {
|
|
instancesStore.set(new Map([createInstancePair("body", "Body", [])]));
|
|
styleSourceSelectionsStore.set(new Map());
|
|
styleSourcesStore.set(
|
|
new Map([["1", { type: "token", id: "1", name: "Zero" }]])
|
|
);
|
|
stylesStore.set(
|
|
new Map([
|
|
[
|
|
"1:base:color:",
|
|
{
|
|
breakpointId: "base",
|
|
styleSourceId: "1",
|
|
property: "color",
|
|
value: { type: "keyword", value: "red" },
|
|
},
|
|
],
|
|
])
|
|
);
|
|
insertTemplateData(
|
|
{
|
|
children: [{ type: "id", value: "box1" }],
|
|
instances: [
|
|
{ type: "instance", id: "box1", component: "Box", children: [] },
|
|
],
|
|
props: [],
|
|
dataSources: [],
|
|
styleSourceSelections: [{ instanceId: "box1", values: ["1", "2"] }],
|
|
styleSources: [
|
|
{ type: "token", id: "1", name: "One" },
|
|
{ type: "token", id: "2", name: "Two" },
|
|
],
|
|
styles: [
|
|
{
|
|
breakpointId: "base",
|
|
styleSourceId: "1",
|
|
property: "color",
|
|
value: { type: "keyword", value: "black" },
|
|
},
|
|
{
|
|
breakpointId: "base",
|
|
styleSourceId: "1",
|
|
property: "backgroundColor",
|
|
value: { type: "keyword", value: "purple" },
|
|
},
|
|
{
|
|
breakpointId: "base",
|
|
styleSourceId: "2",
|
|
property: "color",
|
|
value: { type: "keyword", value: "green" },
|
|
},
|
|
],
|
|
},
|
|
{ parentSelector: ["body"], position: "end" }
|
|
);
|
|
expect(styleSourceSelectionsStore.get()).toEqual(
|
|
new Map([["box1", { instanceId: "box1", values: ["1", "2"] }]])
|
|
);
|
|
expect(styleSourcesStore.get()).toEqual(
|
|
new Map([
|
|
["1", { type: "token", id: "1", name: "Zero" }],
|
|
["2", { type: "token", id: "2", name: "Two" }],
|
|
])
|
|
);
|
|
expect(stylesStore.get()).toEqual(
|
|
new Map([
|
|
[
|
|
"1:base:color:",
|
|
{
|
|
breakpointId: "base",
|
|
styleSourceId: "1",
|
|
property: "color",
|
|
value: { type: "keyword", value: "red" },
|
|
},
|
|
],
|
|
[
|
|
"2:base:color:",
|
|
{
|
|
breakpointId: "base",
|
|
styleSourceId: "2",
|
|
property: "color",
|
|
value: { type: "keyword", value: "green" },
|
|
},
|
|
],
|
|
])
|
|
);
|
|
});
|
|
|
|
describe("reparent instance", () => {
|
|
test("from collectioo item", () => {
|
|
// body
|
|
// list
|
|
// box
|
|
instancesStore.set(
|
|
new Map([
|
|
createInstancePair("body", "Body", [{ type: "id", value: "list" }]),
|
|
createInstancePair("list", collectionComponent, [
|
|
{ type: "id", value: "box" },
|
|
]),
|
|
createInstancePair("box", "Box", []),
|
|
])
|
|
);
|
|
registeredComponentMetasStore.set(createFakeComponentMetas({}));
|
|
reparentInstance(["box", "list[0]", "list", "body"], {
|
|
parentSelector: ["body"],
|
|
position: 1,
|
|
});
|
|
expect(instancesStore.get()).toEqual(
|
|
new Map([
|
|
createInstancePair("body", "Body", [
|
|
{ type: "id", value: "list" },
|
|
{ type: "id", value: "box" },
|
|
]),
|
|
createInstancePair("list", collectionComponent, []),
|
|
createInstancePair("box", "Box", []),
|
|
])
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("delete instance", () => {
|
|
test("delete instance with its children", () => {
|
|
// body
|
|
// box1
|
|
// box11
|
|
// box2
|
|
instancesStore.set(
|
|
new Map([
|
|
createInstancePair("body", "Body", [
|
|
{ type: "id", value: "box1" },
|
|
{ type: "id", value: "box2" },
|
|
]),
|
|
createInstancePair("box1", "Box", [{ type: "id", value: "box11" }]),
|
|
createInstancePair("box11", "Box", []),
|
|
createInstancePair("box2", "Box", []),
|
|
])
|
|
);
|
|
registeredComponentMetasStore.set(createFakeComponentMetas({}));
|
|
deleteInstance(["box1", "body"]);
|
|
expect(instancesStore.get()).toEqual(
|
|
new Map([
|
|
createInstancePair("body", "Body", [{ type: "id", value: "box2" }]),
|
|
createInstancePair("box2", "Box", []),
|
|
])
|
|
);
|
|
});
|
|
|
|
test("prevent deleting root instance", () => {
|
|
// body
|
|
// box1
|
|
instancesStore.set(
|
|
new Map([
|
|
createInstancePair("body", "Body", [{ type: "id", value: "box1" }]),
|
|
createInstancePair("box1", "Box", []),
|
|
])
|
|
);
|
|
registeredComponentMetasStore.set(createFakeComponentMetas({}));
|
|
deleteInstance(["body"]);
|
|
expect(instancesStore.get()).toEqual(
|
|
new Map([
|
|
createInstancePair("body", "Body", [{ type: "id", value: "box1" }]),
|
|
createInstancePair("box1", "Box", []),
|
|
])
|
|
);
|
|
});
|
|
|
|
test("delete instance from collection item", () => {
|
|
// body
|
|
// list
|
|
// box
|
|
instancesStore.set(
|
|
new Map([
|
|
createInstancePair("body", "Body", [{ type: "id", value: "list" }]),
|
|
createInstancePair("list", collectionComponent, [
|
|
{ type: "id", value: "box" },
|
|
]),
|
|
createInstancePair("box", "Box", []),
|
|
])
|
|
);
|
|
registeredComponentMetasStore.set(createFakeComponentMetas({}));
|
|
deleteInstance(["box", "list[0]", "list", "body"]);
|
|
expect(instancesStore.get()).toEqual(
|
|
new Map([
|
|
createInstancePair("body", "Body", [{ type: "id", value: "list" }]),
|
|
createInstancePair("list", collectionComponent, []),
|
|
])
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("get instances slice", () => {
|
|
test("collect the instance by id and all its descendants including portal instances", () => {
|
|
// body
|
|
// bodyChild1
|
|
// slot
|
|
// slotChild
|
|
// bodyChild2
|
|
instancesStore.set(
|
|
toMap([
|
|
createInstance("body", "Body", [
|
|
{ type: "id", value: "bodyChild1" },
|
|
{ type: "id", value: "bodyChild2" },
|
|
]),
|
|
createInstance("bodyChild1", "Box", [{ type: "id", value: "slot" }]),
|
|
createInstance("slot", "Slot", [{ type: "id", value: "slotChild" }]),
|
|
createInstance("slotChild", "Box", []),
|
|
createInstance("bodyChild2", "Box", []),
|
|
])
|
|
);
|
|
const { instances } = getInstancesSlice("bodyChild1");
|
|
|
|
expect(instances).toEqual([
|
|
createInstance("bodyChild1", "Box", [{ type: "id", value: "slot" }]),
|
|
createInstance("slot", "Slot", [{ type: "id", value: "slotChild" }]),
|
|
createInstance("slotChild", "Box", []),
|
|
]);
|
|
});
|
|
|
|
test("collect all styles and breakpoints bound to instances slice", () => {
|
|
// body
|
|
// box1
|
|
// box2
|
|
instancesStore.set(
|
|
toMap([
|
|
createInstance("body", "Body", [{ type: "id", value: "box1" }]),
|
|
createInstance("box1", "Box", [{ type: "id", value: "box2" }]),
|
|
createInstance("box2", "Box", []),
|
|
])
|
|
);
|
|
styleSourceSelectionsStore.set(
|
|
new Map([
|
|
["body", { instanceId: "box1", values: ["localBody", "token1"] }],
|
|
["box1", { instanceId: "box1", values: ["localBox1", "token2"] }],
|
|
["box2", { instanceId: "box2", values: ["localBox2", "token2"] }],
|
|
])
|
|
);
|
|
styleSourcesStore.set(
|
|
new Map([
|
|
["localBody", { id: "localBody", type: "local" }],
|
|
["localBox1", { id: "localBox1", type: "local" }],
|
|
["localBox2", { id: "localBox2", type: "local" }],
|
|
["token1", { id: "token1", type: "token", name: "token1" }],
|
|
["token2", { id: "token2", type: "token", name: "token2" }],
|
|
])
|
|
);
|
|
stylesStore.set(
|
|
new Map([
|
|
createStyleDeclPair("localBody1", "base", "color", "red"),
|
|
createStyleDeclPair("localBox1", "base", "color", "green"),
|
|
createStyleDeclPair("localBox2", "base", "color", "blue"),
|
|
createStyleDeclPair("token1", "base", "color", "yellow"),
|
|
createStyleDeclPair("token2", "base", "color", "orange"),
|
|
])
|
|
);
|
|
breakpointsStore.set(
|
|
new Map([
|
|
["base", { id: "base", label: "base" }],
|
|
["big", { id: "big", label: "big", minWidth: 768 }],
|
|
])
|
|
);
|
|
const { styleSources, styleSourceSelections, styles, breakpoints } =
|
|
getInstancesSlice("box1");
|
|
|
|
// exclude localBody and token1 bound to body
|
|
expect(styleSources).toEqual([
|
|
{ id: "localBox1", type: "local" },
|
|
{ id: "token2", type: "token", name: "token2" },
|
|
{ id: "localBox2", type: "local" },
|
|
]);
|
|
expect(styleSourceSelections).toEqual([
|
|
{ instanceId: "box1", values: ["localBox1", "token2"] },
|
|
{ instanceId: "box2", values: ["localBox2", "token2"] },
|
|
]);
|
|
expect(styles).toEqual([
|
|
createStyleDecl("localBox1", "base", "color", "green"),
|
|
createStyleDecl("localBox2", "base", "color", "blue"),
|
|
createStyleDecl("token2", "base", "color", "orange"),
|
|
]);
|
|
expect(breakpoints).toEqual([{ id: "base", label: "base" }]);
|
|
});
|
|
|
|
test("collect all props bound to instances slice", () => {
|
|
// body
|
|
// box1
|
|
// box2
|
|
instancesStore.set(
|
|
toMap([
|
|
createInstance("body", "Body", [{ type: "id", value: "box1" }]),
|
|
createInstance("box1", "Box", [{ type: "id", value: "box2" }]),
|
|
createInstance("box2", "Box", []),
|
|
])
|
|
);
|
|
propsStore.set(
|
|
toMap([
|
|
createProp("body", "bodyProp", "data-body"),
|
|
createProp("box1", "box1Prop", "data-box1"),
|
|
createProp("box2", "box2Prop", "data-box2"),
|
|
])
|
|
);
|
|
const { props } = getInstancesSlice("box1");
|
|
|
|
expect(props).toEqual([
|
|
createProp("box1", "box1Prop", "data-box1"),
|
|
createProp("box2", "box2Prop", "data-box2"),
|
|
]);
|
|
});
|
|
|
|
test("collect assets from props and styles withiin instances slice", () => {
|
|
// body
|
|
// box1
|
|
// box2
|
|
instancesStore.set(
|
|
toMap([
|
|
createInstance("body", "Body", [{ type: "id", value: "box1" }]),
|
|
createInstance("box1", "Box", [{ type: "id", value: "box2" }]),
|
|
createInstance("box2", "Box", []),
|
|
])
|
|
);
|
|
propsStore.set(
|
|
new Map([
|
|
[
|
|
"bodyProp",
|
|
{
|
|
id: "bodyProp",
|
|
instanceId: "body",
|
|
name: "data-body",
|
|
type: "asset",
|
|
value: "asset1",
|
|
},
|
|
],
|
|
[
|
|
"box1Prop",
|
|
{
|
|
id: "box1Prop",
|
|
instanceId: "box1",
|
|
name: "data-box1",
|
|
type: "asset",
|
|
value: "asset2",
|
|
},
|
|
],
|
|
])
|
|
);
|
|
styleSourceSelectionsStore.set(
|
|
new Map([
|
|
["body", { instanceId: "box1", values: ["localBody"] }],
|
|
["box1", { instanceId: "box1", values: ["localBox1"] }],
|
|
])
|
|
);
|
|
styleSourcesStore.set(
|
|
new Map([
|
|
["localBody", { id: "localBody", type: "local" }],
|
|
["localBox1", { id: "localBox1", type: "local" }],
|
|
])
|
|
);
|
|
stylesStore.set(
|
|
new Map([
|
|
createStyleDeclPair("localBody1", "base", "fontFamily", {
|
|
type: "fontFamily",
|
|
value: ["font1"],
|
|
}),
|
|
createStyleDeclPair("localBody1", "base", "backgroundImage", {
|
|
type: "image",
|
|
value: { type: "asset", value: "asset3" },
|
|
}),
|
|
createStyleDeclPair("localBox1", "base", "fontFamily", {
|
|
type: "fontFamily",
|
|
value: ["font2"],
|
|
}),
|
|
createStyleDeclPair("localBox1", "base", "color", {
|
|
type: "image",
|
|
value: { type: "asset", value: "asset4" },
|
|
}),
|
|
])
|
|
);
|
|
breakpointsStore.set(new Map([["base", { id: "base", label: "base" }]]));
|
|
assetsStore.set(
|
|
toMap([
|
|
createImageAsset("asset1"),
|
|
createImageAsset("asset2"),
|
|
createImageAsset("asset3"),
|
|
createImageAsset("asset4"),
|
|
createFontAsset("asset5", "font1"),
|
|
createFontAsset("asset6", "font2"),
|
|
])
|
|
);
|
|
const { assets } = getInstancesSlice("box1");
|
|
|
|
expect(assets).toEqual([
|
|
createImageAsset("asset2"),
|
|
createImageAsset("asset4"),
|
|
createFontAsset("asset6", "font2"),
|
|
]);
|
|
});
|
|
|
|
test("collect data sources used in expressions within instances", () => {
|
|
// body
|
|
// box1
|
|
// box2
|
|
instancesStore.set(
|
|
toMap([
|
|
createInstance("body", "Body", [{ type: "id", value: "box1" }]),
|
|
createInstance("box1", "Box", [{ type: "id", value: "box2" }]),
|
|
createInstance("box2", "Box", []),
|
|
])
|
|
);
|
|
dataSourcesStore.set(
|
|
toMap([
|
|
{
|
|
id: "box1$state",
|
|
scopeInstanceId: "box1",
|
|
type: "variable",
|
|
name: "state",
|
|
value: { type: "string", value: "initial" },
|
|
},
|
|
])
|
|
);
|
|
propsStore.set(
|
|
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$trueProp",
|
|
instanceId: "box2",
|
|
name: "bool-prop",
|
|
type: "expression",
|
|
value: `true`,
|
|
},
|
|
])
|
|
);
|
|
const { props, dataSources } = getInstancesSlice("box2");
|
|
|
|
expect(dataSources).toEqual([
|
|
{
|
|
id: "box1$state",
|
|
scopeInstanceId: "box1",
|
|
type: "variable",
|
|
name: "state",
|
|
value: { type: "string", value: "initial" },
|
|
},
|
|
]);
|
|
expect(props).toEqual([
|
|
{
|
|
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$trueProp",
|
|
instanceId: "box2",
|
|
name: "bool-prop",
|
|
type: "expression",
|
|
value: `true`,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("collect data sources used in actions within instances", () => {
|
|
// body
|
|
// box1
|
|
// box2
|
|
instancesStore.set(
|
|
toMap([
|
|
createInstance("body", "Body", [{ type: "id", value: "box1" }]),
|
|
createInstance("box1", "Box", [{ type: "id", value: "box2" }]),
|
|
createInstance("box2", "Box", []),
|
|
])
|
|
);
|
|
dataSourcesStore.set(
|
|
toMap([
|
|
{
|
|
id: "box1$state",
|
|
scopeInstanceId: "box1",
|
|
type: "variable",
|
|
name: "state",
|
|
value: { type: "string", value: "initial" },
|
|
},
|
|
{
|
|
id: "box2$state",
|
|
scopeInstanceId: "box2",
|
|
type: "variable",
|
|
name: "state",
|
|
value: { type: "string", value: "initial" },
|
|
},
|
|
])
|
|
);
|
|
propsStore.set(
|
|
toMap([
|
|
{
|
|
id: "box2$onChange1",
|
|
instanceId: "box2",
|
|
type: "action",
|
|
name: "onChange",
|
|
value: [
|
|
{
|
|
type: "execute",
|
|
args: ["value"],
|
|
code: `$ws$dataSource$box1$state = value`,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "box2$onChange2",
|
|
instanceId: "box2",
|
|
type: "action",
|
|
name: "onChange",
|
|
value: [
|
|
{
|
|
type: "execute",
|
|
args: ["value"],
|
|
code: `$ws$dataSource$box2$state = value`,
|
|
},
|
|
],
|
|
},
|
|
])
|
|
);
|
|
const { props, dataSources } = getInstancesSlice("box2");
|
|
|
|
expect(dataSources).toEqual([
|
|
{
|
|
id: "box1$state",
|
|
scopeInstanceId: "box1",
|
|
type: "variable",
|
|
name: "state",
|
|
value: { type: "string", value: "initial" },
|
|
},
|
|
{
|
|
id: "box2$state",
|
|
scopeInstanceId: "box2",
|
|
type: "variable",
|
|
name: "state",
|
|
value: { type: "string", value: "initial" },
|
|
},
|
|
]);
|
|
expect(props).toEqual([
|
|
{
|
|
id: "box2$onChange1",
|
|
instanceId: "box2",
|
|
type: "action",
|
|
name: "onChange",
|
|
value: [
|
|
{
|
|
args: ["value"],
|
|
code: "$ws$dataSource$box1$state = value",
|
|
type: "execute",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "box2$onChange2",
|
|
instanceId: "box2",
|
|
type: "action",
|
|
name: "onChange",
|
|
value: [
|
|
{
|
|
type: "execute",
|
|
args: ["value"],
|
|
code: `$ws$dataSource$box2$state = value`,
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("insert instances slice copy", () => {
|
|
const emptySlice = {
|
|
instances: [
|
|
{
|
|
id: "body",
|
|
type: "instance",
|
|
component: "Body",
|
|
children: [],
|
|
} satisfies Instance,
|
|
],
|
|
styleSourceSelections: [],
|
|
styleSources: [],
|
|
breakpoints: [],
|
|
styles: [],
|
|
dataSources: [],
|
|
props: [],
|
|
assets: [],
|
|
};
|
|
|
|
beforeEach(() => {
|
|
$pages.set({ homePage: { id: "" } as Page, pages: [] });
|
|
$project.set({ id: "current_project" } as Project);
|
|
$assets.set(new Map());
|
|
$breakpoints.set(new Map());
|
|
$styleSourceSelections.set(new Map());
|
|
$styleSources.set(new Map());
|
|
$styles.set(new Map());
|
|
$instances.set(new Map());
|
|
$props.set(new Map());
|
|
$dataSources.set(new Map());
|
|
});
|
|
|
|
test("insert assets with same ids if missing", () => {
|
|
insertInstancesSliceCopy({
|
|
slice: {
|
|
...emptySlice,
|
|
assets: [createImageAsset("asset1", "name", "another_project")],
|
|
},
|
|
availableDataSources: new Set(),
|
|
});
|
|
expect(Array.from($assets.get().values())).toEqual([
|
|
createImageAsset("asset1", "name", "current_project"),
|
|
]);
|
|
|
|
$assets.set(
|
|
toMap([createImageAsset("asset1", "changed_name", "current_project")])
|
|
);
|
|
insertInstancesSliceCopy({
|
|
slice: {
|
|
...emptySlice,
|
|
assets: [
|
|
createImageAsset("asset1", "name", "another_project"),
|
|
createImageAsset("asset2", "another_name", "another_project"),
|
|
],
|
|
},
|
|
availableDataSources: new Set(),
|
|
});
|
|
expect(Array.from($assets.get().values())).toEqual([
|
|
// preserve any user changes
|
|
createImageAsset("asset1", "changed_name", "current_project"),
|
|
// add new assets while preserving old ones
|
|
createImageAsset("asset2", "another_name", "current_project"),
|
|
]);
|
|
});
|
|
|
|
test("merge breakpoints with existing ones", () => {
|
|
$breakpoints.set(
|
|
toMap([
|
|
{ id: "existing_base", label: "base" },
|
|
{ id: "existing_small", label: "small", minWidth: 768 },
|
|
])
|
|
);
|
|
insertInstancesSliceCopy({
|
|
slice: {
|
|
...emptySlice,
|
|
breakpoints: [
|
|
{ id: "new_base", label: "Base" },
|
|
{
|
|
id: "new_small",
|
|
label: "Small",
|
|
minWidth: 768,
|
|
},
|
|
{
|
|
id: "new_large",
|
|
label: "Large",
|
|
minWidth: 1200,
|
|
},
|
|
],
|
|
},
|
|
availableDataSources: new Set(),
|
|
});
|
|
expect(Array.from($breakpoints.get().values())).toEqual([
|
|
{ id: "existing_base", label: "base" },
|
|
{ id: "existing_small", label: "small", minWidth: 768 },
|
|
{ id: "new_large", label: "Large", minWidth: 1200 },
|
|
]);
|
|
});
|
|
|
|
test("insert missing tokens and use merged breakpoint ids", () => {
|
|
$breakpoints.set(toMap([{ id: "base", label: "base" }]));
|
|
$styleSources.set(
|
|
toMap([{ id: "token1", type: "token", name: "oldLabel" }])
|
|
);
|
|
insertInstancesSliceCopy({
|
|
slice: {
|
|
...emptySlice,
|
|
breakpoints: [{ id: "new_base", label: "Base" }],
|
|
styleSources: [
|
|
{ id: "token1", type: "token", name: "newLabel" },
|
|
{ id: "token2", type: "token", name: "myToken" },
|
|
],
|
|
styles: [
|
|
{
|
|
styleSourceId: "token1",
|
|
breakpointId: "new_base",
|
|
property: "color",
|
|
value: { type: "keyword", value: "red" },
|
|
},
|
|
{
|
|
styleSourceId: "token2",
|
|
breakpointId: "new_base",
|
|
property: "color",
|
|
value: { type: "keyword", value: "green" },
|
|
},
|
|
],
|
|
},
|
|
availableDataSources: new Set(),
|
|
});
|
|
expect(Array.from($styleSources.get().values())).toEqual([
|
|
{ id: "token1", type: "token", name: "oldLabel" },
|
|
{ id: "token2", type: "token", name: "myToken" },
|
|
]);
|
|
expect(Array.from($styles.get().values())).toEqual([
|
|
{
|
|
styleSourceId: "token2",
|
|
breakpointId: "base",
|
|
property: "color",
|
|
value: { type: "keyword", value: "green" },
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("insert instances", () => {
|
|
insertInstancesSliceCopy({
|
|
slice: {
|
|
...emptySlice,
|
|
instances: [
|
|
{
|
|
type: "instance",
|
|
id: "box",
|
|
component: "Box",
|
|
children: [],
|
|
},
|
|
],
|
|
},
|
|
availableDataSources: new Set(),
|
|
});
|
|
expect(Array.from($instances.get().keys())).toEqual([
|
|
expect.not.stringMatching("box"),
|
|
]);
|
|
});
|
|
|
|
test("insert instances with portals", () => {
|
|
// portal
|
|
// fragment
|
|
// box
|
|
const instancesWithPortals: Instance[] = [
|
|
{
|
|
type: "instance",
|
|
id: "portal",
|
|
component: portalComponent,
|
|
children: [{ type: "id", value: "fragment" }],
|
|
},
|
|
{
|
|
type: "instance",
|
|
id: "fragment",
|
|
component: "Fragment",
|
|
children: [{ type: "id", value: "box" }],
|
|
},
|
|
{
|
|
type: "instance",
|
|
id: "box",
|
|
component: "Box",
|
|
children: [{ type: "text", value: "First" }],
|
|
},
|
|
];
|
|
|
|
insertInstancesSliceCopy({
|
|
slice: {
|
|
...emptySlice,
|
|
instances: instancesWithPortals,
|
|
},
|
|
availableDataSources: new Set(),
|
|
});
|
|
|
|
expect(Array.from($instances.get().values())).toEqual([
|
|
{
|
|
type: "instance",
|
|
id: "fragment",
|
|
component: "Fragment",
|
|
children: [{ type: "id", value: "box" }],
|
|
},
|
|
{
|
|
type: "instance",
|
|
id: "box",
|
|
component: "Box",
|
|
children: [{ type: "text", value: "First" }],
|
|
},
|
|
{
|
|
type: "instance",
|
|
id: expect.not.stringMatching("portal"),
|
|
component: portalComponent,
|
|
children: [{ type: "id", value: "fragment" }],
|
|
},
|
|
]);
|
|
|
|
// change portal content and make sure it does not break
|
|
// when stale portal with same id is inserted
|
|
const instancesWithDeletedBoxes = new Map($instances.get());
|
|
instancesWithDeletedBoxes.delete("box");
|
|
instancesWithDeletedBoxes.set("fragment", {
|
|
type: "instance",
|
|
id: "fragment",
|
|
component: "Fragment",
|
|
children: [],
|
|
});
|
|
$instances.set(instancesWithDeletedBoxes);
|
|
insertInstancesSliceCopy({
|
|
slice: {
|
|
...emptySlice,
|
|
instances: instancesWithPortals,
|
|
},
|
|
availableDataSources: new Set(),
|
|
});
|
|
|
|
expect(Array.from($instances.get().values())).toEqual([
|
|
{
|
|
type: "instance",
|
|
id: "fragment",
|
|
component: "Fragment",
|
|
children: [],
|
|
},
|
|
{
|
|
type: "instance",
|
|
id: expect.not.stringMatching("portal"),
|
|
component: portalComponent,
|
|
children: [{ type: "id", value: "fragment" }],
|
|
},
|
|
{
|
|
type: "instance",
|
|
id: expect.not.stringMatching("portal"),
|
|
component: portalComponent,
|
|
children: [{ type: "id", value: "fragment" }],
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("insert props with new ids", () => {
|
|
insertInstancesSliceCopy({
|
|
slice: {
|
|
...emptySlice,
|
|
props: [
|
|
{
|
|
id: "prop1",
|
|
instanceId: "body",
|
|
name: "myProp1",
|
|
type: "string",
|
|
value: "",
|
|
},
|
|
],
|
|
},
|
|
availableDataSources: new Set(),
|
|
});
|
|
expect(Array.from($props.get().values())).toEqual([
|
|
{
|
|
id: expect.not.stringMatching("prop1"),
|
|
instanceId: expect.not.stringMatching("body"),
|
|
name: "myProp1",
|
|
type: "string",
|
|
value: "",
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("insert props from portals with old ids", () => {
|
|
insertInstancesSliceCopy({
|
|
slice: {
|
|
...emptySlice,
|
|
// portal
|
|
// fragment
|
|
instances: [
|
|
{
|
|
type: "instance",
|
|
id: "portal",
|
|
component: portalComponent,
|
|
children: [{ type: "id", value: "fragment" }],
|
|
},
|
|
{
|
|
type: "instance",
|
|
id: "fragment",
|
|
component: "Fragment",
|
|
children: [],
|
|
},
|
|
],
|
|
props: [
|
|
{
|
|
id: "prop1",
|
|
instanceId: "fragment",
|
|
name: "myProp1",
|
|
type: "string",
|
|
value: "",
|
|
},
|
|
],
|
|
},
|
|
availableDataSources: new Set(),
|
|
});
|
|
expect(Array.from($props.get().values())).toEqual([
|
|
{
|
|
id: "prop1",
|
|
instanceId: "fragment",
|
|
name: "myProp1",
|
|
type: "string",
|
|
value: "",
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("insert data sources with new ids", () => {
|
|
insertInstancesSliceCopy({
|
|
slice: {
|
|
...emptySlice,
|
|
dataSources: [
|
|
{
|
|
id: "variableId",
|
|
scopeInstanceId: "body",
|
|
type: "variable",
|
|
name: "myVariable",
|
|
value: { type: "string", value: "" },
|
|
},
|
|
],
|
|
props: [
|
|
{
|
|
id: "expressionId",
|
|
instanceId: "body",
|
|
name: "myProp1",
|
|
type: "expression",
|
|
value: "$ws$dataSource$variableId",
|
|
},
|
|
{
|
|
id: "actionId",
|
|
instanceId: "body",
|
|
name: "myProp2",
|
|
type: "action",
|
|
value: [
|
|
{
|
|
type: "execute",
|
|
args: [],
|
|
code: `$ws$dataSource$variableId = ""`,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "parameterId",
|
|
instanceId: "body",
|
|
name: "myProp3",
|
|
type: "parameter",
|
|
value: "variableId",
|
|
},
|
|
],
|
|
},
|
|
availableDataSources: new Set(),
|
|
});
|
|
const [newVariableId] = $dataSources.get().keys();
|
|
expect(newVariableId).not.toEqual("variableId");
|
|
expect(Array.from($dataSources.get().values())).toEqual([
|
|
{
|
|
id: newVariableId,
|
|
scopeInstanceId: expect.not.stringMatching("body"),
|
|
type: "variable",
|
|
name: "myVariable",
|
|
value: { type: "string", value: "" },
|
|
},
|
|
]);
|
|
expect(Array.from($props.get().values())).toEqual([
|
|
{
|
|
id: expect.not.stringMatching("expressionId"),
|
|
instanceId: expect.not.stringMatching("body"),
|
|
name: "myProp1",
|
|
type: "expression",
|
|
value: encodeDataSourceVariable(newVariableId),
|
|
},
|
|
{
|
|
id: expect.not.stringMatching("actionId"),
|
|
instanceId: expect.not.stringMatching("body"),
|
|
name: "myProp2",
|
|
type: "action",
|
|
value: [
|
|
{
|
|
type: "execute",
|
|
args: [],
|
|
code: `${encodeDataSourceVariable(newVariableId)} = ""`,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: expect.not.stringMatching("parameterId"),
|
|
instanceId: expect.not.stringMatching("body"),
|
|
name: "myProp3",
|
|
type: "parameter",
|
|
value: newVariableId,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("inline data sources when not available in scope", () => {
|
|
insertInstancesSliceCopy({
|
|
slice: {
|
|
...emptySlice,
|
|
dataSources: [
|
|
{
|
|
id: "outsideVariableId",
|
|
scopeInstanceId: "outsideInstanceId",
|
|
type: "variable",
|
|
name: "myOutsideVariable",
|
|
value: { type: "string", value: "outside" },
|
|
},
|
|
{
|
|
id: "insideVariableId",
|
|
scopeInstanceId: "insideInstanceId",
|
|
type: "variable",
|
|
name: "myInsideVariable",
|
|
value: { type: "string", value: "inside" },
|
|
},
|
|
],
|
|
props: [
|
|
{
|
|
id: "expressionId",
|
|
instanceId: "body",
|
|
name: "myProp1",
|
|
type: "expression",
|
|
value:
|
|
"$ws$dataSource$outsideVariableId + $ws$dataSource$insideVariableId",
|
|
},
|
|
{
|
|
id: "actionId",
|
|
instanceId: "body",
|
|
name: "myProp2",
|
|
type: "action",
|
|
value: [
|
|
{
|
|
type: "execute",
|
|
args: [],
|
|
code: `$ws$dataSource$outsideVariableId = "outside"`,
|
|
},
|
|
{
|
|
type: "execute",
|
|
args: [],
|
|
code: `$ws$dataSource$insideVariableId = "inside"`,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
availableDataSources: new Set(["insideVariableId"]),
|
|
});
|
|
expect($dataSources.get()).toEqual(new Map());
|
|
expect(Array.from($props.get().values())).toEqual([
|
|
{
|
|
id: expect.not.stringMatching("expressionId"),
|
|
instanceId: expect.not.stringMatching("body"),
|
|
name: "myProp1",
|
|
type: "expression",
|
|
value: `"outside" + $ws$dataSource$insideVariableId`,
|
|
},
|
|
{
|
|
id: expect.not.stringMatching("actionId"),
|
|
instanceId: expect.not.stringMatching("body"),
|
|
name: "myProp2",
|
|
type: "action",
|
|
value: [
|
|
{
|
|
type: "execute",
|
|
args: [],
|
|
code: `$ws$dataSource$insideVariableId = "inside"`,
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("insert data sources from portals with old ids", () => {
|
|
insertInstancesSliceCopy({
|
|
slice: {
|
|
...emptySlice,
|
|
// portal
|
|
// fragment
|
|
instances: [
|
|
{
|
|
type: "instance",
|
|
id: "portal",
|
|
component: portalComponent,
|
|
children: [{ type: "id", value: "fragment" }],
|
|
},
|
|
{
|
|
type: "instance",
|
|
id: "fragment",
|
|
component: "Fragment",
|
|
children: [{ type: "id", value: "box" }],
|
|
},
|
|
],
|
|
dataSources: [
|
|
{
|
|
id: "variableId",
|
|
scopeInstanceId: "fragment",
|
|
type: "variable",
|
|
name: "myVariable",
|
|
value: { type: "string", value: "" },
|
|
},
|
|
],
|
|
props: [
|
|
{
|
|
id: "expressionId",
|
|
instanceId: "fragment",
|
|
name: "myProp1",
|
|
type: "expression",
|
|
value: "$ws$dataSource$variableId",
|
|
},
|
|
{
|
|
id: "actionId",
|
|
instanceId: "fragment",
|
|
name: "myProp2",
|
|
type: "action",
|
|
value: [
|
|
{
|
|
type: "execute",
|
|
args: [],
|
|
code: `$ws$dataSource$variableId = ""`,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "parameterId",
|
|
instanceId: "fragment",
|
|
name: "myProp3",
|
|
type: "parameter",
|
|
value: "variableId",
|
|
},
|
|
],
|
|
},
|
|
availableDataSources: new Set(),
|
|
});
|
|
expect(Array.from($dataSources.get().values())).toEqual([
|
|
{
|
|
id: "variableId",
|
|
scopeInstanceId: "fragment",
|
|
type: "variable",
|
|
name: "myVariable",
|
|
value: { type: "string", value: "" },
|
|
},
|
|
]);
|
|
expect(Array.from($props.get().values())).toEqual([
|
|
{
|
|
id: "expressionId",
|
|
instanceId: "fragment",
|
|
name: "myProp1",
|
|
type: "expression",
|
|
value: `$ws$dataSource$variableId`,
|
|
},
|
|
{
|
|
id: "actionId",
|
|
instanceId: "fragment",
|
|
name: "myProp2",
|
|
type: "action",
|
|
value: [
|
|
{
|
|
type: "execute",
|
|
args: [],
|
|
code: `$ws$dataSource$variableId = ""`,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "parameterId",
|
|
instanceId: "fragment",
|
|
name: "myProp3",
|
|
type: "parameter",
|
|
value: `variableId`,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("inline data sources from portals when not available in scope", () => {
|
|
insertInstancesSliceCopy({
|
|
slice: {
|
|
...emptySlice,
|
|
// portal
|
|
// fragment
|
|
instances: [
|
|
{
|
|
type: "instance",
|
|
id: "portal",
|
|
component: portalComponent,
|
|
children: [{ type: "id", value: "fragment" }],
|
|
},
|
|
{
|
|
type: "instance",
|
|
id: "fragment",
|
|
component: "Fragment",
|
|
children: [{ type: "id", value: "box" }],
|
|
},
|
|
],
|
|
dataSources: [
|
|
{
|
|
id: "outsideVariableId",
|
|
scopeInstanceId: "outsideInstanceId",
|
|
type: "variable",
|
|
name: "myOutsideVariable",
|
|
value: { type: "string", value: "outside" },
|
|
},
|
|
{
|
|
id: "insideVariableId",
|
|
scopeInstanceId: "insideInstanceId",
|
|
type: "variable",
|
|
name: "myInsideVariable",
|
|
value: { type: "string", value: "inside" },
|
|
},
|
|
],
|
|
props: [
|
|
{
|
|
id: "expressionId",
|
|
instanceId: "fragment",
|
|
name: "myProp1",
|
|
type: "expression",
|
|
value:
|
|
"$ws$dataSource$outsideVariableId + $ws$dataSource$insideVariableId",
|
|
},
|
|
{
|
|
id: "actionId",
|
|
instanceId: "fragment",
|
|
name: "myProp2",
|
|
type: "action",
|
|
value: [
|
|
{
|
|
type: "execute",
|
|
args: [],
|
|
code: `$ws$dataSource$outsideVariableId = "outside"`,
|
|
},
|
|
{
|
|
type: "execute",
|
|
args: [],
|
|
code: `$ws$dataSource$insideVariableId = "inside"`,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
availableDataSources: new Set(["insideVariableId"]),
|
|
});
|
|
expect($dataSources.get()).toEqual(new Map());
|
|
expect(Array.from($props.get().values())).toEqual([
|
|
{
|
|
id: "expressionId",
|
|
instanceId: "fragment",
|
|
name: "myProp1",
|
|
type: "expression",
|
|
value: `"outside" + $ws$dataSource$insideVariableId`,
|
|
},
|
|
{
|
|
id: "actionId",
|
|
instanceId: "fragment",
|
|
name: "myProp2",
|
|
type: "action",
|
|
value: [
|
|
{
|
|
type: "execute",
|
|
args: [],
|
|
code: `$ws$dataSource$insideVariableId = "inside"`,
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("insert local styles with new ids and use merged breakpoint ids", () => {
|
|
$breakpoints.set(toMap([{ id: "base", label: "base" }]));
|
|
insertInstancesSliceCopy({
|
|
slice: {
|
|
...emptySlice,
|
|
instances: [
|
|
{
|
|
type: "instance",
|
|
id: "box",
|
|
component: "Box",
|
|
children: [],
|
|
},
|
|
],
|
|
breakpoints: [{ id: "new_base", label: "Base" }],
|
|
styleSourceSelections: [
|
|
{ instanceId: "box", values: ["localId", "tokenId"] },
|
|
{ instanceId: "unknown", values: [] },
|
|
],
|
|
styleSources: [{ id: "localId", type: "local" }],
|
|
styles: [
|
|
{
|
|
styleSourceId: "localId",
|
|
breakpointId: "new_base",
|
|
property: "color",
|
|
value: { type: "keyword", value: "red" },
|
|
},
|
|
{
|
|
styleSourceId: "tokenId",
|
|
breakpointId: "new_base",
|
|
property: "color",
|
|
value: { type: "keyword", value: "green" },
|
|
},
|
|
],
|
|
},
|
|
availableDataSources: new Set(),
|
|
});
|
|
expect(Array.from($styleSourceSelections.get().values())).toEqual([
|
|
{
|
|
instanceId: expect.not.stringMatching("box"),
|
|
values: [expect.not.stringMatching("localId"), "tokenId"],
|
|
},
|
|
]);
|
|
expect(Array.from($styleSources.get().values())).toEqual([
|
|
{ id: expect.not.stringMatching("localId"), type: "local" },
|
|
]);
|
|
expect(Array.from($styles.get().values())).toEqual([
|
|
{
|
|
styleSourceId: expect.not.stringMatching("localId"),
|
|
breakpointId: "base",
|
|
property: "color",
|
|
value: { type: "keyword", value: "red" },
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("insert local styles from portal and use merged breakpoint ids", () => {
|
|
$breakpoints.set(toMap([{ id: "base", label: "base" }]));
|
|
insertInstancesSliceCopy({
|
|
slice: {
|
|
...emptySlice,
|
|
// portal
|
|
// fragment
|
|
instances: [
|
|
{
|
|
type: "instance",
|
|
id: "portal",
|
|
component: portalComponent,
|
|
children: [{ type: "id", value: "fragment" }],
|
|
},
|
|
{
|
|
type: "instance",
|
|
id: "fragment",
|
|
component: "Fragment",
|
|
children: [{ type: "id", value: "box" }],
|
|
},
|
|
],
|
|
breakpoints: [{ id: "new_base", label: "Base" }],
|
|
styleSourceSelections: [
|
|
{ instanceId: "fragment", values: ["localId", "tokenId"] },
|
|
{ instanceId: "unknown", values: [] },
|
|
],
|
|
styleSources: [{ id: "localId", type: "local" }],
|
|
styles: [
|
|
{
|
|
styleSourceId: "localId",
|
|
breakpointId: "new_base",
|
|
property: "color",
|
|
value: { type: "keyword", value: "red" },
|
|
},
|
|
{
|
|
styleSourceId: "tokenId",
|
|
breakpointId: "new_base",
|
|
property: "color",
|
|
value: { type: "keyword", value: "green" },
|
|
},
|
|
],
|
|
},
|
|
availableDataSources: new Set(),
|
|
});
|
|
expect(Array.from($styleSourceSelections.get().values())).toEqual([
|
|
{ instanceId: "fragment", values: ["localId", "tokenId"] },
|
|
]);
|
|
expect(Array.from($styleSources.get().values())).toEqual([
|
|
{ id: "localId", type: "local" },
|
|
]);
|
|
expect(Array.from($styles.get().values())).toEqual([
|
|
{
|
|
styleSourceId: "localId",
|
|
breakpointId: "base",
|
|
property: "color",
|
|
value: { type: "keyword", value: "red" },
|
|
},
|
|
]);
|
|
});
|
|
});
|