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, anotherItemMeta?: Partial ): Map => { 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; }; const getIdValuePair = (item: T) => [item.id, item] as const; const toMap = (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([ ["Button", { ...base, requiredAncestors: ["Form"] }], ["Checkbox", { ...base, requiredAncestors: ["Form", "Label"] }], ["Label", { ...base, requiredAncestors: ["Body"] }], ]); // button // label // checkbox const instances = new Map([ 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([ ["Button", { ...base, invalidAncestors: ["Button"] }], ["Form", { ...base, invalidAncestors: ["Button", "Form"] }], ]); // form // button const instances = new Map([ 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" }, }, ]); }); });