mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 22:14:29 +09:00
ref #4608 ## Description 1. What is this PR about (link the issue and add a short description) ## Steps for reproduction 1. click button 2. expect xyz ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview ## Before requesting a review - [ ] made a self-review - [ ] added inline comments where things may be not obvious (the "why", not "what") ## Before merging - [ ] tested locally and on preview environment (preview dev login: 0000) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests - [ ] if any new env variables are added, added them to `.env` file
1043 lines
36 KiB
TypeScript
1043 lines
36 KiB
TypeScript
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}`}></$.Box>
|
|
</$.Body>
|
|
);
|
|
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}`}></$.Box>
|
|
</$.Body>
|
|
);
|
|
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(
|
|
<ws.root ws:id={ROOT_INSTANCE_ID} vars={expression`${globalVariable}`}>
|
|
<$.Body ws:id="bodyId">
|
|
<$.Box ws:id="boxId" vars={expression`${boxVariable}`}></$.Box>
|
|
</$.Body>
|
|
</ws.root>
|
|
);
|
|
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(
|
|
<ws.root ws:id={ROOT_INSTANCE_ID} vars={expression`${globalVariable}`}>
|
|
<$.Body ws:id="bodyId" vars={expression`${bodyVariable}`}>
|
|
<$.Slot ws:id="slotId">
|
|
<$.Fragment ws:id="fragmentId">
|
|
<$.Box ws:id="boxId" vars={expression`${boxVariable}`}></$.Box>
|
|
</$.Fragment>
|
|
</$.Slot>
|
|
</$.Body>
|
|
</ws.root>
|
|
);
|
|
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`}</$.Text>
|
|
</$.Box>
|
|
</$.Body>
|
|
);
|
|
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`}</$.Text>
|
|
</$.Box>
|
|
</$.Body>
|
|
);
|
|
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`}></$.Text>
|
|
</$.Box>
|
|
</$.Body>
|
|
);
|
|
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}`}
|
|
</$.Text>
|
|
</$.Box>
|
|
</$.Body>
|
|
);
|
|
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}`}
|
|
</$.Text>
|
|
</$.Body>
|
|
);
|
|
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}
|
|
></$.Text>
|
|
</$.Box>
|
|
</$.Body>
|
|
);
|
|
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}
|
|
></$.Text>
|
|
</$.Box>
|
|
</$.Body>
|
|
);
|
|
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(
|
|
<ws.root ws:id={ROOT_INSTANCE_ID} data-vars={expression`${globalVariable}`}>
|
|
<$.Body ws:id="bodyId">
|
|
<$.Text ws:id="textId">{expression`globalVariable`}</$.Text>
|
|
</$.Body>
|
|
</ws.root>
|
|
);
|
|
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}`}</$.Text>
|
|
</$.Body>
|
|
);
|
|
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`}</$.Box>
|
|
</$.Fragment>
|
|
</$.Slot>
|
|
</$.Body>
|
|
);
|
|
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">
|
|
<ws.collection ws:id="collectionId" data={[]} item={collectionItem}>
|
|
<ws.collection
|
|
ws:id="nestedCollectionId"
|
|
data={expression`${collectionItem}`}
|
|
item={nestedCollectionItem}
|
|
>
|
|
<ws.element ws:id="divId" ws:tag="div">
|
|
{expression`${nestedCollectionItem}`}
|
|
</ws.element>
|
|
</ws.collection>
|
|
</ws.collection>
|
|
</$.Body>
|
|
);
|
|
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}`}
|
|
</$.Box>
|
|
</$.Body>
|
|
);
|
|
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}
|
|
></$.Box>
|
|
</$.Body>
|
|
);
|
|
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}`}</$.Text>
|
|
</$.Box>
|
|
</$.Body>
|
|
);
|
|
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}`}</$.Text>
|
|
</$.Box>
|
|
</$.Fragment>
|
|
</$.Slot>
|
|
</$.Body>
|
|
);
|
|
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(
|
|
<ws.root ws:id={ROOT_INSTANCE_ID} vars={expression`${globalVariable}`}>
|
|
<$.Body ws:id="homeBodyId">
|
|
<$.Text ws:id="homeTextId">{expression`${globalVariable}`}</$.Text>
|
|
</$.Body>
|
|
<$.Body ws:id="aboutBodyId">
|
|
<$.Text ws:id="aboutTextId">{expression`${globalVariable}`}</$.Text>
|
|
</$.Body>
|
|
</ws.root>
|
|
),
|
|
};
|
|
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(
|
|
<ws.root ws:id={ROOT_INSTANCE_ID} vars={expression`${globalVariable}`}>
|
|
<$.Body ws:id="bodyId">
|
|
<$.Slot ws:id="slotId">
|
|
<$.Fragment ws:id="fragmentId">
|
|
<$.Text ws:id="textId">{expression`${globalVariable}`}</$.Text>
|
|
</$.Fragment>
|
|
</$.Slot>
|
|
</$.Body>
|
|
</ws.root>
|
|
),
|
|
};
|
|
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}`}></$.Body>
|
|
),
|
|
};
|
|
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(
|
|
<ws.root ws:id={ROOT_INSTANCE_ID} vars={expression`${globalVariable}`}>
|
|
<$.Body ws:id="homeBodyId"></$.Body>
|
|
<$.Body ws:id="aboutBodyId"></$.Body>
|
|
</ws.root>
|
|
),
|
|
};
|
|
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}`}</$.Text>
|
|
</$.Box>
|
|
</$.Body>
|
|
),
|
|
};
|
|
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(
|
|
<ws.root ws:id={ROOT_INSTANCE_ID} vars={expression`${globalVariable}`}>
|
|
<$.Body ws:id="homeBodyId">
|
|
<$.Box ws:id="homeBoxId" data-prop={expression`${globalVariable}`} />
|
|
</$.Body>
|
|
<$.Body ws:id="aboutBodyId">
|
|
<$.Box ws:id="aboutBoxId" data-prop={expression`${globalVariable}`} />
|
|
</$.Body>
|
|
</ws.root>
|
|
),
|
|
};
|
|
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}`}></$.Body>
|
|
),
|
|
};
|
|
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}`}
|
|
></$.Body>
|
|
),
|
|
};
|
|
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);
|
|
});
|