import { expect, test, vi } from "vitest"; import { $, ActionValue, expression, Parameter, renderData, ResourceValue, Variable, ws, } from "@webstudio-is/template"; import { encodeDataVariableId, ROOT_INSTANCE_ID, SYSTEM_VARIABLE_ID, } from "@webstudio-is/sdk"; import { createDefaultPages } from "@webstudio-is/project-build"; import { computeExpression, decodeDataVariableName, encodeDataVariableName, findUnsetVariableNames, restoreExpressionVariables, rebindTreeVariablesMutable, unsetExpressionVariables, deleteVariableMutable, findAvailableVariables, findVariableUsagesByInstance, } from "./data-variables"; test("encode data variable name when necessary", () => { expect(encodeDataVariableName("formState")).toEqual("formState"); expect(encodeDataVariableName("Collection Item")).toEqual( "Collection$32$Item" ); expect(encodeDataVariableName("$my$Variable")).toEqual("$36$my$36$Variable"); }); test("dencode data variable name", () => { expect(decodeDataVariableName(encodeDataVariableName("formState"))).toEqual( "formState" ); expect( decodeDataVariableName(encodeDataVariableName("Collection Item")) ).toEqual("Collection Item"); }); test("dencode data variable name with dollar sign", () => { expect( decodeDataVariableName(encodeDataVariableName("$my$Variable")) ).toEqual("$my$Variable"); expect(decodeDataVariableName("$my$Variable")).toEqual("$my$Variable"); }); test("find available variables", () => { const bodyVariable = new Variable("bodyVariable", ""); const boxVariable = new Variable("boxVariable", ""); const data = renderData( <$.Body ws:id="bodyId" vars={expression`${bodyVariable}`}> <$.Box ws:id="boxId" vars={expression`${boxVariable}`}> ); expect( findAvailableVariables({ ...data, startingInstanceId: "boxId" }) ).toEqual([ expect.objectContaining({ name: "system", id: SYSTEM_VARIABLE_ID }), expect.objectContaining({ name: "bodyVariable" }), expect.objectContaining({ name: "boxVariable" }), ]); }); test("find masked variables", () => { const bodyVariable = new Variable("myVariable", ""); const boxVariable = new Variable("myVariable", ""); const data = renderData( <$.Body ws:id="bodyId" vars={expression`${bodyVariable}`}> <$.Box ws:id="boxId" vars={expression`${boxVariable}`}> ); expect( findAvailableVariables({ ...data, startingInstanceId: "boxId" }) ).toEqual([ expect.objectContaining({ name: "system", id: SYSTEM_VARIABLE_ID }), expect.objectContaining({ scopeInstanceId: "boxId", name: "myVariable" }), ]); }); test("find global variables", () => { const globalVariable = new Variable("globalVariable", ""); const boxVariable = new Variable("boxVariable", ""); const data = renderData( <$.Body ws:id="bodyId"> <$.Box ws:id="boxId" vars={expression`${boxVariable}`}> ); data.instances.delete(ROOT_INSTANCE_ID); expect( findAvailableVariables({ ...data, startingInstanceId: "boxId" }) ).toEqual([ expect.objectContaining({ name: "system", id: SYSTEM_VARIABLE_ID }), expect.objectContaining({ name: "globalVariable" }), expect.objectContaining({ name: "boxVariable" }), ]); }); test("find global variables in slots", () => { const globalVariable = new Variable("globalVariable", ""); const bodyVariable = new Variable("bodyVariable", ""); const boxVariable = new Variable("boxVariable", ""); const data = renderData( <$.Body ws:id="bodyId" vars={expression`${bodyVariable}`}> <$.Slot ws:id="slotId"> <$.Fragment ws:id="fragmentId"> <$.Box ws:id="boxId" vars={expression`${boxVariable}`}> ); data.instances.delete(ROOT_INSTANCE_ID); expect( findAvailableVariables({ ...data, startingInstanceId: "boxId" }) ).toEqual([ expect.objectContaining({ name: "system", id: SYSTEM_VARIABLE_ID }), expect.objectContaining({ name: "globalVariable" }), expect.objectContaining({ name: "boxVariable" }), ]); }); test("unset expression variables", () => { expect( unsetExpressionVariables({ expression: `$ws$dataSource$myId + arbitaryVariable`, unsetNameById: new Map([["myId", "My Variable"]]), }) ).toEqual("My$32$Variable + arbitaryVariable"); }); test("ignore not existing variables in expressions", () => { expect( unsetExpressionVariables({ expression: `$ws$dataSource$myId + arbitaryVariable`, unsetNameById: new Map(), }) ).toEqual("$ws$dataSource$myId + arbitaryVariable"); }); test("restore expression variables", () => { expect( restoreExpressionVariables({ expression: `My$32$Variable + missingVariable`, maskedIdByName: new Map([["My Variable", "myId"]]), }) ).toEqual("$ws$dataSource$myId + missingVariable"); }); test("compute expression with decoded ids", () => { expect( computeExpression("$ws$dataSource$myId", new Map([["myId", "value"]])) ).toEqual("value"); }); test("compute expression with decoded names", () => { expect( computeExpression("My$32$Name", new Map([["My Name", "value"]])) ).toEqual("value"); }); test("compute expression when invalid syntax", () => { // prevent error message in test report const spy = vi.spyOn(console, "error"); spy.mockImplementationOnce(() => {}); expect(computeExpression("https://github.com", new Map())).toEqual(undefined); expect(spy).toHaveBeenCalledOnce(); spy.mockRestore(); }); test("compute expression with nested field of undefined without error", () => { const spy = vi.spyOn(console, "error"); const variables = new Map([["myVariable", undefined]]); expect(computeExpression("myVariable.field", variables)).toEqual(undefined); expect(spy).not.toHaveBeenCalled(); spy.mockRestore(); }); test("compute literal expression when variable is json object", () => { const jsonObject = { hello: "world", subObject: { world: "hello" } }; const variables = new Map([["jsonVariable", jsonObject]]); expect(computeExpression("`${jsonVariable}`", variables)).toEqual( `{"hello":"world","subObject":{"world":"hello"}}` ); expect(computeExpression("`${jsonVariable.subObject}`", variables)).toEqual( `{"world":"hello"}` ); }); test("compute literal expression when object is frozen", () => { const jsonObject = Object.freeze({ hello: "world", subObject: { world: "hello" }, }); const variables = new Map([["jsonVariable", jsonObject]]); expect(computeExpression("`${jsonVariable.subObject}`", variables)).toEqual( `{"world":"hello"}` ); }); test("compute unset variables as undefined", () => { expect(computeExpression(`a`, new Map())).toEqual(undefined); expect(computeExpression("`${a}`", new Map())).toEqual("undefined"); }); test("find unset variable names", () => { const resourceVariable = new ResourceValue("resourceVariable", { url: expression`six`, method: "post", searchParams: [{ name: "filter", value: expression`seven` }], headers: [{ name: "auth", value: expression`eight` }], body: expression`nine`, }); const resourceProp = new ResourceValue("resourceProp", { url: expression`ten`, method: "post", searchParams: [{ name: "filter", value: expression`eleven` }], headers: [{ name: "auth", value: expression`twelve` }], body: expression`thirteen`, }); const data = renderData( <$.Body ws:id="body" data-prop={expression`two`}> <$.Box ws:id="box" data-prop={expression`three`}> <$.Text ws:id="text" data-variables={expression`${resourceVariable}`} data-resource={resourceProp} data-action={new ActionValue(["five"], expression`four + five`)} >{expression`one`} ); expect( findUnsetVariableNames({ startingInstanceId: "body", ...data }) ).toEqual([ "one", "two", "three", "four", "six", "eight", "seven", "nine", "ten", "twelve", "eleven", "thirteen", ]); }); test("restore tree variables in children", () => { const bodyVariable = new Variable("one", "one value of body"); const boxVariable = new Variable("one", "one value of box"); const data = renderData( <$.Body ws:id="bodyId" data-vars={expression`${bodyVariable}`}> <$.Box ws:id="boxId" data-vars={expression`${boxVariable}`}> <$.Text ws:id="textId">{expression`one`} ); rebindTreeVariablesMutable({ startingInstanceId: "boxId", pages: createDefaultPages({ rootInstanceId: "bodyId" }), ...data, }); expect(Array.from(data.dataSources.values())).toEqual([ expect.objectContaining({ scopeInstanceId: "bodyId" }), expect.objectContaining({ scopeInstanceId: "boxId" }), ]); const [_bodyVariableId, boxVariableId] = data.dataSources.keys(); const boxIdentifier = encodeDataVariableId(boxVariableId); expect(data.instances.get("textId")?.children).toEqual([ { type: "expression", value: boxIdentifier }, ]); }); test("restore tree variables in props", () => { const oneBody = new Variable("one", "one value of body"); const oneBox = new Variable("one", "one value of box"); const twoBox = new Variable("two", "two value of box"); const data = renderData( <$.Body ws:id="bodyId" data-body-vars={expression`${oneBody}`}> <$.Box ws:id="boxId" data-box-vars={expression`${oneBox} ${twoBox}`} data-one={expression`one`} data-action={new ActionValue(["one"], expression`one + two + three`)} > <$.Text ws:id="text" data-two={expression`one + two + three`}> ); rebindTreeVariablesMutable({ startingInstanceId: "boxId", pages: createDefaultPages({ rootInstanceId: "bodyId" }), ...data, }); const [_bodyVariableId, boxOneVariableId, boxTwoVariableId] = data.dataSources.keys(); const boxOneIdentifier = encodeDataVariableId(boxOneVariableId); const boxTwoIdentifier = encodeDataVariableId(boxTwoVariableId); expect(Array.from(data.dataSources.values())).toEqual([ expect.objectContaining({ name: "one", scopeInstanceId: "bodyId" }), expect.objectContaining({ name: "one", scopeInstanceId: "boxId" }), expect.objectContaining({ name: "two", scopeInstanceId: "boxId" }), ]); expect(Array.from(data.props.values())).toEqual([ expect.objectContaining({ name: "data-body-vars" }), expect.objectContaining({ name: "data-box-vars" }), expect.objectContaining({ name: "data-one", value: boxOneIdentifier }), expect.objectContaining({ name: "data-action", value: [ { type: "execute", args: ["one"], code: `one + ${boxTwoIdentifier} + three`, }, ], }), expect.objectContaining({ name: "data-two", value: `${boxOneIdentifier} + ${boxTwoIdentifier} + three`, }), ]); }); test("rebind tree variables in props and children", () => { const bodyVariable = new Variable("one", "one value of body"); const boxVariable = new Variable("one", "one value of box"); const data = renderData( <$.Body ws:id="bodyId" data-body-vars={expression`${bodyVariable}`}> <$.Box ws:id="boxId" data-box-vars={expression`${boxVariable}`}> <$.Text ws:id="textId" data-text-vars={expression`${bodyVariable}`} data-action={new ActionValue([], expression`${bodyVariable}`)} > {expression`${bodyVariable}`} ); rebindTreeVariablesMutable({ startingInstanceId: "boxId", pages: createDefaultPages({ rootInstanceId: "bodyId" }), ...data, }); expect(Array.from(data.dataSources.values())).toEqual([ expect.objectContaining({ scopeInstanceId: "bodyId" }), expect.objectContaining({ scopeInstanceId: "boxId" }), ]); const [_bodyVariableId, boxVariableId] = data.dataSources.keys(); const boxIdentifier = encodeDataVariableId(boxVariableId); expect(Array.from(data.props.values())).toEqual([ expect.objectContaining({ name: "data-body-vars" }), expect.objectContaining({ name: "data-box-vars", value: boxIdentifier }), expect.objectContaining({ name: "data-text-vars", value: boxIdentifier }), expect.objectContaining({ name: "data-action", value: [{ type: "execute", args: [], code: boxIdentifier }], }), ]); expect(data.instances.get("textId")?.children).toEqual([ { type: "expression", value: boxIdentifier }, ]); }); test("preserve nested variables with the same name when rebind", () => { const bodyVariable = new Variable("one", "one value of body"); const textVariable = new Variable("one", "one value of box"); const data = renderData( <$.Body ws:id="bodyId" data-body-vars={expression`${bodyVariable}`}> <$.Text ws:id="textId" data-text-vars={expression`${textVariable}`}> {expression`${textVariable}`} ); rebindTreeVariablesMutable({ startingInstanceId: "bodyId", pages: createDefaultPages({ rootInstanceId: "bodyId" }), ...data, }); expect(Array.from(data.dataSources.values())).toEqual([ expect.objectContaining({ scopeInstanceId: "bodyId" }), expect.objectContaining({ scopeInstanceId: "textId" }), ]); const [_bodyVariableId, textVariableId] = data.dataSources.keys(); const textIdentifier = encodeDataVariableId(textVariableId); expect(data.instances.get("textId")?.children).toEqual([ { type: "expression", value: textIdentifier }, ]); }); test("restore tree variables in resources", () => { const bodyVariable = new Variable("one", "one value of body"); const boxVariable = new Variable("one", "one value of box"); const resourceVariable = new ResourceValue("resourceVariable", { url: expression`one + 1`, method: "post", searchParams: [{ name: "filter", value: expression`one + 1` }], headers: [{ name: "auth", value: expression`one + 1` }], body: expression`one + 1`, }); const resourceProp = new ResourceValue("resourceProp", { url: expression`one + 2`, method: "post", searchParams: [{ name: "filter", value: expression`one + 2` }], headers: [{ name: "auth", value: expression`one + 2` }], body: expression`one + 2`, }); const data = renderData( <$.Body ws:id="bodyId" data-vars={expression`${bodyVariable}`}> <$.Box ws:id="boxId" data-vars={expression`${boxVariable}`}> <$.Text ws:id="text" data-vars={expression`${resourceVariable}`} data-resource={resourceProp} > ); rebindTreeVariablesMutable({ startingInstanceId: "boxId", pages: createDefaultPages({ rootInstanceId: "bodyId" }), ...data, }); expect(Array.from(data.dataSources.values())).toEqual([ expect.objectContaining({ scopeInstanceId: "bodyId" }), expect.objectContaining({ scopeInstanceId: "boxId" }), expect.objectContaining({ type: "resource" }), ]); const [_bodyVariableId, boxVariableId] = data.dataSources.keys(); const boxIdentifier = encodeDataVariableId(boxVariableId); expect(Array.from(data.resources.values())).toEqual([ expect.objectContaining({ url: `${boxIdentifier} + 1`, method: "post", searchParams: [{ name: "filter", value: `${boxIdentifier} + 1` }], headers: [{ name: "auth", value: `${boxIdentifier} + 1` }], body: `${boxIdentifier} + 1`, }), expect.objectContaining({ url: `${boxIdentifier} + 2`, method: "post", searchParams: [{ name: "filter", value: `${boxIdentifier} + 2` }], headers: [{ name: "auth", value: `${boxIdentifier} + 2` }], body: `${boxIdentifier} + 2`, }), ]); }); test("rebind tree variables in resources", () => { const bodyVariable = new Variable("one", "one value of body"); const boxVariable = new Variable("one", "one value of box"); const resourceVariable = new ResourceValue("resourceVariable", { url: expression`${bodyVariable}`, method: "post", searchParams: [{ name: "filter", value: expression`${bodyVariable}` }], headers: [{ name: "auth", value: expression`${bodyVariable}` }], body: expression`${bodyVariable}`, }); const resourceProp = new ResourceValue("resourceProp", { url: expression`${bodyVariable}`, method: "post", searchParams: [{ name: "filter", value: expression`${bodyVariable}` }], headers: [{ name: "auth", value: expression`${bodyVariable}` }], body: expression`${bodyVariable}`, }); const data = renderData( <$.Body ws:id="bodyId" data-body-vars={expression`${bodyVariable}`}> <$.Box ws:id="boxId" data-box-vars={expression`${boxVariable}`}> <$.Text ws:id="textId" data-text-vars={expression`${resourceVariable}`} data-action={resourceProp} > ); rebindTreeVariablesMutable({ startingInstanceId: "boxId", pages: createDefaultPages({ rootInstanceId: "bodyId" }), ...data, }); expect(Array.from(data.dataSources.values())).toEqual([ expect.objectContaining({ scopeInstanceId: "bodyId" }), expect.objectContaining({ scopeInstanceId: "boxId" }), expect.objectContaining({ type: "resource" }), ]); const [_bodyVariableId, boxVariableId] = data.dataSources.keys(); const boxIdentifier = encodeDataVariableId(boxVariableId); expect(Array.from(data.resources.values())).toEqual([ expect.objectContaining({ url: boxIdentifier, method: "post", searchParams: [{ name: "filter", value: boxIdentifier }], headers: [{ name: "auth", value: boxIdentifier }], body: boxIdentifier, }), expect.objectContaining({ url: boxIdentifier, method: "post", searchParams: [{ name: "filter", value: boxIdentifier }], headers: [{ name: "auth", value: boxIdentifier }], body: boxIdentifier, }), ]); }); test("rebind global variables in resources", () => { const globalVariable = new Variable("globalVariable", ""); const data = renderData( <$.Body ws:id="bodyId"> <$.Text ws:id="textId">{expression`globalVariable`} ); data.instances.delete(ROOT_INSTANCE_ID); rebindTreeVariablesMutable({ startingInstanceId: ROOT_INSTANCE_ID, pages: createDefaultPages({ rootInstanceId: "bodyId" }), ...data, }); expect(Array.from(data.dataSources.values())).toEqual([ expect.objectContaining({ scopeInstanceId: ROOT_INSTANCE_ID }), ]); const [globalVariableId] = data.dataSources.keys(); const globalIdentifier = encodeDataVariableId(globalVariableId); expect(data.instances.get("textId")?.children).toEqual([ { type: "expression", value: globalIdentifier }, ]); }); test("preserve other variables when rebind", () => { const bodyVariable = new Variable("globalVariable", ""); const textVariable = new Variable("textVariable", ""); const data = renderData( <$.Body ws:id="bodyId" data-vars={expression`${bodyVariable}`}> <$.Text ws:id="textId">{expression`${textVariable}`} ); rebindTreeVariablesMutable({ startingInstanceId: "bodyId", pages: createDefaultPages({ rootInstanceId: "bodyId" }), ...data, }); expect(Array.from(data.dataSources.values())).toEqual([ expect.objectContaining({ scopeInstanceId: "bodyId" }), expect.objectContaining({ scopeInstanceId: "textId" }), ]); const [_globalVariableId, textVariableId] = data.dataSources.keys(); const textIdentifier = encodeDataVariableId(textVariableId); expect(data.instances.get("textId")?.children).toEqual([ { type: "expression", value: textIdentifier }, ]); }); test("prevent rebinding tree variables from slots", () => { const bodyVariable = new Variable("myVariable", "one value of body"); const data = renderData( <$.Body ws:id="bodyId" data-body-vars={expression`${bodyVariable}`}> <$.Slot ws:id="slotId"> <$.Fragment ws:id="fragmentId"> <$.Box ws:id="boxId">{expression`myVariable`} ); rebindTreeVariablesMutable({ startingInstanceId: "boxId", pages: createDefaultPages({ rootInstanceId: "bodyId" }), ...data, }); expect(data.instances.get("boxId")?.children).toEqual([ { type: "expression", value: "myVariable" }, ]); }); test("prevent rebinding with nested collection item", () => { const collectionItem = new Parameter("collectionITem"); const nestedCollectionItem = new Parameter("collectionITem"); const data = renderData( <$.Body ws:id="bodyId"> {expression`${nestedCollectionItem}`} ); expect(Array.from(data.dataSources.values())).toEqual([ expect.objectContaining({ scopeInstanceId: "collectionId" }), expect.objectContaining({ scopeInstanceId: "nestedCollectionId" }), ]); const [collectionItemId, nestedCollectionItemId] = data.dataSources.keys(); rebindTreeVariablesMutable({ startingInstanceId: "bodyId", pages: createDefaultPages({ rootInstanceId: "bodyId" }), ...data, }); expect(Array.from(data.props.values())).toEqual([ expect.objectContaining({ instanceId: "collectionId", name: "data", }), expect.objectContaining({ instanceId: "collectionId", name: "item", value: collectionItemId, }), expect.objectContaining({ instanceId: "nestedCollectionId", name: "data", value: encodeDataVariableId(collectionItemId), }), expect.objectContaining({ instanceId: "nestedCollectionId", name: "item", value: nestedCollectionItemId, }), ]); expect(data.instances.get("divId")?.children).toEqual([ { type: "expression", value: encodeDataVariableId(nestedCollectionItemId) }, ]); }); test("delete variable and unset it in expressions", () => { const bodyVariable = new Variable("bodyVariable", "one value of body"); const data = renderData( <$.Body ws:id="bodyId" data-body-vars={expression`${bodyVariable}`}> <$.Box ws:id="boxId" data-action={new ActionValue([], expression`${bodyVariable}`)} > {expression`${bodyVariable}`} ); expect(Array.from(data.dataSources.values())).toEqual([ expect.objectContaining({ scopeInstanceId: "bodyId" }), ]); const [bodyVariableId] = data.dataSources.keys(); deleteVariableMutable(data, bodyVariableId); expect(Array.from(data.props.values())).toEqual([ expect.objectContaining({ name: "data-body-vars", value: "bodyVariable" }), expect.objectContaining({ name: "data-action", value: [{ type: "execute", args: [], code: "bodyVariable" }], }), ]); expect(data.instances.get("boxId")?.children).toEqual([ { type: "expression", value: "bodyVariable" }, ]); }); test("delete variable and unset it in resources", () => { const bodyVariable = new Variable("bodyVariable", "one value of body"); const resourceVariable = new ResourceValue("resourceVariable", { url: expression`${bodyVariable}`, method: "post", searchParams: [{ name: "filter", value: expression`${bodyVariable}` }], headers: [{ name: "auth", value: expression`${bodyVariable}` }], body: expression`${bodyVariable}`, }); const resourceProp = new ResourceValue("resourceProp", { url: expression`${bodyVariable}`, method: "post", searchParams: [{ name: "filter", value: expression`${bodyVariable}` }], headers: [{ name: "auth", value: expression`${bodyVariable}` }], body: expression`${bodyVariable}`, }); const data = renderData( <$.Body ws:id="bodyId" data-body-vars={expression`${bodyVariable}`}> <$.Box ws:id="boxId" data-box-vars={expression`${resourceVariable}`} data-resource={resourceProp} > ); expect(Array.from(data.dataSources.values())).toEqual([ expect.objectContaining({ scopeInstanceId: "bodyId" }), expect.objectContaining({ scopeInstanceId: "boxId" }), ]); const [bodyVariableId] = data.dataSources.keys(); deleteVariableMutable(data, bodyVariableId); expect(Array.from(data.resources.values())).toEqual([ expect.objectContaining({ url: "bodyVariable", method: "post", searchParams: [{ name: "filter", value: "bodyVariable" }], headers: [{ name: "auth", value: "bodyVariable" }], body: "bodyVariable", }), expect.objectContaining({ url: "bodyVariable", method: "post", searchParams: [{ name: "filter", value: "bodyVariable" }], headers: [{ name: "auth", value: "bodyVariable" }], body: "bodyVariable", }), ]); }); test("rebind expressions with parent variable when delete variable on child", () => { const bodyVariable = new Variable("myVariable", "one value of body"); const boxVariable = new Variable("myVariable", "one value of body"); const data = renderData( <$.Body ws:id="bodyId" data-body-vars={expression`${bodyVariable}`}> <$.Box ws:id="boxId" data-box-vars={expression`${boxVariable}`}> <$.Text ws:id="textId">{expression`${boxVariable}`} ); expect(Array.from(data.dataSources.values())).toEqual([ expect.objectContaining({ scopeInstanceId: "bodyId" }), expect.objectContaining({ scopeInstanceId: "boxId" }), ]); const [bodyVariableId, boxVariableId] = data.dataSources.keys(); deleteVariableMutable(data, boxVariableId); const bodyIdentifier = encodeDataVariableId(bodyVariableId); expect(data.instances.get("textId")?.children).toEqual([ { type: "expression", value: bodyIdentifier }, ]); }); test("prevent rebinding with variables outside of slot content scope", () => { const bodyVariable = new Variable("myVariable", "one value of body"); const boxVariable = new Variable("myVariable", "one value of body"); const data = renderData( <$.Body ws:id="bodyId" data-body-vars={expression`${bodyVariable}`}> <$.Slot> <$.Fragment> <$.Box ws:id="boxId" data-box-vars={expression`${boxVariable}`}> <$.Text ws:id="textId">{expression`${boxVariable}`} ); expect(Array.from(data.dataSources.values())).toEqual([ expect.objectContaining({ scopeInstanceId: "bodyId" }), expect.objectContaining({ scopeInstanceId: "boxId" }), ]); const [_bodyVariableId, boxVariableId] = data.dataSources.keys(); deleteVariableMutable(data, boxVariableId); expect(data.instances.get("textId")?.children).toEqual([ { type: "expression", value: "myVariable" }, ]); }); test("unset global variables on all pages when delete", () => { const globalVariable = new Variable("globalVariable", ""); const pages = createDefaultPages({ rootInstanceId: "homeBodyId" }); pages.pages.push({ id: "", name: "", path: "", title: "", meta: {}, rootInstanceId: "aboutBodyId", }); const data = { pages, ...renderData( <$.Body ws:id="homeBodyId"> <$.Text ws:id="homeTextId">{expression`${globalVariable}`} <$.Body ws:id="aboutBodyId"> <$.Text ws:id="aboutTextId">{expression`${globalVariable}`} ), }; data.instances.delete(ROOT_INSTANCE_ID); expect(data.dataSources.size).toEqual(1); const [globalVariableId] = data.dataSources.keys(); deleteVariableMutable(data, globalVariableId); expect(data.instances.get("homeTextId")?.children).toEqual([ { type: "expression", value: "globalVariable" }, ]); expect(data.instances.get("aboutTextId")?.children).toEqual([ { type: "expression", value: "globalVariable" }, ]); }); test("unset global variables in slots when delete", () => { const globalVariable = new Variable("globalVariable", ""); const data = { pages: createDefaultPages({ rootInstanceId: "bodyId" }), ...renderData( <$.Body ws:id="bodyId"> <$.Slot ws:id="slotId"> <$.Fragment ws:id="fragmentId"> <$.Text ws:id="textId">{expression`${globalVariable}`} ), }; data.instances.delete(ROOT_INSTANCE_ID); expect(data.dataSources.size).toEqual(1); const [globalVariableId] = data.dataSources.keys(); deleteVariableMutable(data, globalVariableId); expect(data.instances.get("textId")?.children).toEqual([ { type: "expression", value: "globalVariable" }, ]); }); test("unset body variables in page meta when delete", () => { const bodyVariable = new Variable("bodyVariable", ""); const data = { pages: createDefaultPages({ rootInstanceId: "bodyId" }), ...renderData( <$.Body ws:id="bodyId" vars={expression`${bodyVariable}`}> ), }; expect(data.dataSources.size).toEqual(1); const [bodyVariableId] = data.dataSources.keys(); const bodyIdentifier = encodeDataVariableId(bodyVariableId); data.pages.homePage.title = bodyIdentifier; data.pages.homePage.meta = { description: bodyIdentifier, excludePageFromSearch: bodyIdentifier, socialImageUrl: bodyIdentifier, language: bodyIdentifier, status: bodyIdentifier, redirect: bodyIdentifier, custom: [{ property: "auth", content: bodyIdentifier }], }; deleteVariableMutable(data, bodyVariableId); expect(data.pages.homePage.title).toEqual(`bodyVariable`); expect(data.pages.homePage.meta.description).toEqual(`bodyVariable`); expect(data.pages.homePage.meta.excludePageFromSearch).toEqual( `bodyVariable` ); expect(data.pages.homePage.meta.socialImageUrl).toEqual(`bodyVariable`); expect(data.pages.homePage.meta.language).toEqual(`bodyVariable`); expect(data.pages.homePage.meta.status).toEqual(`bodyVariable`); expect(data.pages.homePage.meta.redirect).toEqual(`bodyVariable`); expect(data.pages.homePage.meta.custom?.[0].content).toEqual(`bodyVariable`); }); test("unset global variables in all pages meta when delete", () => { const globalVariable = new Variable("globalVariable", ""); const data = { pages: createDefaultPages({ rootInstanceId: "homeBodyId" }), ...renderData( <$.Body ws:id="homeBodyId"> <$.Body ws:id="aboutBodyId"> ), }; data.instances.delete(ROOT_INSTANCE_ID); data.pages.pages.push({ id: "", name: "", path: "", title: "", meta: {}, rootInstanceId: "aboutBodyId", }); expect(data.dataSources.size).toEqual(1); const [globalVariableId] = data.dataSources.keys(); const globalIdentifier = encodeDataVariableId(globalVariableId); for (const page of [data.pages.homePage, ...data.pages.pages]) { page.title = globalIdentifier; page.meta = { description: globalIdentifier, excludePageFromSearch: globalIdentifier, socialImageUrl: globalIdentifier, language: globalIdentifier, status: globalIdentifier, redirect: globalIdentifier, custom: [{ property: "auth", content: globalIdentifier }], }; } deleteVariableMutable(data, globalVariableId); for (const page of [data.pages.homePage, ...data.pages.pages]) { expect(page.title).toEqual(`globalVariable`); expect(page.meta.description).toEqual(`globalVariable`); expect(page.meta.excludePageFromSearch).toEqual(`globalVariable`); expect(page.meta.socialImageUrl).toEqual(`globalVariable`); expect(page.meta.language).toEqual(`globalVariable`); expect(page.meta.status).toEqual(`globalVariable`); expect(page.meta.redirect).toEqual(`globalVariable`); expect(page.meta.custom?.[0].content).toEqual(`globalVariable`); } }); test("find variable usages by instance", () => { const bodyVariable = new Variable("bodyVariable", ""); const boxVariable = new Variable("boxVariable", ""); const data = { pages: createDefaultPages({ rootInstanceId: "bodyId" }), ...renderData( <$.Body ws:id="bodyId" vars={expression`${bodyVariable}`} data-prop={expression`${bodyVariable} + "test"`} > <$.Box ws:id="boxId" vars={expression`${boxVariable}`} data-prop={expression`${boxVariable} + ${bodyVariable}`} > <$.Text ws:id="textId">{expression`${bodyVariable}`} ), }; const bodyVariableId = [...data.dataSources.values()].find( (ds) => ds.name === "bodyVariable" )?.id; const boxVariableId = [...data.dataSources.values()].find( (ds) => ds.name === "boxVariable" )?.id; const usagesByInstance = findVariableUsagesByInstance({ ...data, startingInstanceId: "bodyId", }); expect(usagesByInstance.get(bodyVariableId!)).toEqual( new Set(["bodyId", "boxId", "textId"]) ); expect(usagesByInstance.get(boxVariableId!)).toEqual(new Set(["boxId"])); }); test("find variable usages from root includes all pages", () => { const globalVariable = new Variable("globalVariable", ""); const data = { pages: createDefaultPages({ rootInstanceId: "homeBodyId" }), ...renderData( <$.Body ws:id="homeBodyId"> <$.Box ws:id="homeBoxId" data-prop={expression`${globalVariable}`} /> <$.Body ws:id="aboutBodyId"> <$.Box ws:id="aboutBoxId" data-prop={expression`${globalVariable}`} /> ), }; data.instances.delete(ROOT_INSTANCE_ID); data.pages.pages.push({ id: "aboutPage", name: "About", path: "/about", title: "", meta: {}, rootInstanceId: "aboutBodyId", }); const [globalVariableId] = data.dataSources.keys(); const usagesByInstance = findVariableUsagesByInstance({ ...data, startingInstanceId: ROOT_INSTANCE_ID, }); // Global variable is used in expressions on both boxes and the root (where it's defined) expect(usagesByInstance.get(globalVariableId)).toEqual( new Set([ROOT_INSTANCE_ID, "homeBoxId", "aboutBoxId"]) ); }); test("find variable usages in page meta", () => { const bodyVariable = new Variable("bodyVariable", ""); const data = { pages: createDefaultPages({ rootInstanceId: "bodyId" }), ...renderData( <$.Body ws:id="bodyId" vars={expression`${bodyVariable}`}> ), }; const [bodyVariableId] = data.dataSources.keys(); const bodyIdentifier = encodeDataVariableId(bodyVariableId); data.pages.homePage.title = bodyIdentifier; data.pages.homePage.meta = { description: bodyIdentifier, excludePageFromSearch: bodyIdentifier, socialImageUrl: bodyIdentifier, }; const usagesByInstance = findVariableUsagesByInstance({ ...data, startingInstanceId: "bodyId", }); // Page meta expressions are attributed to the page's root instance expect(usagesByInstance.get(bodyVariableId)).toEqual(new Set(["bodyId"])); }); test("find variable usages counts unique instances not expressions", () => { const bodyVariable = new Variable("bodyVariable", ""); const data = { pages: createDefaultPages({ rootInstanceId: "bodyId" }), ...renderData( <$.Body ws:id="bodyId" vars={expression`${bodyVariable}`} data-prop1={expression`${bodyVariable}`} data-prop2={expression`${bodyVariable}`} data-prop3={expression`${bodyVariable}`} > ), }; const [bodyVariableId] = data.dataSources.keys(); const usagesByInstance = findVariableUsagesByInstance({ ...data, startingInstanceId: "bodyId", }); // Even though bodyVariable is used 4 times (in 4 different expressions), // it should only appear once in the set since it's all on the same instance expect(usagesByInstance.get(bodyVariableId)).toEqual(new Set(["bodyId"])); expect(usagesByInstance.get(bodyVariableId)?.size).toBe(1); });