Files
webstudio/apps/builder/app/shared/instance-utils.test.ts
T
Bogdan Chadkin 6037a039b7 Compute insert constraints for tree (#1954)
Fixes https://github.com/webstudio-is/webstudio-builder/issues/1950

Here added new utility to compute constraints for tree when insert embed
template, paste something or reparent. I previous implementation all
constraints were merged which gave us false positives like when paste
List>ListItem we got "requiredAncestors: [List]". Here tree layout is
considered to exclude such false positives.

For testing try to copy paste List>ListItem
2023-07-19 22:23:34 +03:00

320 lines
9.1 KiB
TypeScript

import { describe, test, expect } from "@jest/globals";
import type { WsComponentMeta } from "@webstudio-is/react-sdk";
import * as defaultMetas from "@webstudio-is/sdk-components-react/metas";
import type { Instance, Instances } from "@webstudio-is/project-build";
import {
computeInstancesConstraints,
findClosestDroppableComponentIndex,
findClosestDroppableTarget,
findClosestEditableInstanceSelector,
type InsertConstraints,
} from "./instance-utils";
const defaultMetasMap = new Map(Object.entries(defaultMetas));
const createFakeComponentMetas = (
itemMeta: Partial<WsComponentMeta>,
anotherItemMeta?: Partial<WsComponentMeta>
): Map<string, WsComponentMeta> => {
const base = {
label: "",
Icon: () => null,
};
const configs = {
Item: { ...base, type: "container", ...itemMeta },
AnotherItem: { ...base, type: "container", ...anotherItemMeta },
Bold: { ...base, type: "rich-text-child" },
Text: { ...base, type: "container" },
Form: { ...base, type: "container" },
Box: { ...base, type: "container" },
Div: { ...base, type: "container" },
Body: { ...base, type: "container" },
} as const;
return new Map(Object.entries(configs)) as Map<string, WsComponentMeta>;
};
const createInstancePair = (
id: Instance["id"],
component: string,
children: Instance["children"]
): [Instance["id"], Instance] => {
return [id, { type: "instance", id, component, children }];
};
const emptyInsertConstraints: InsertConstraints = {
requiredAncestors: new Set(),
invalidAncestors: new Set(),
};
describe("find closest editable instance selector", () => {
test("searches closest container", () => {
const instances: Instances = new Map([
createInstancePair("body", "Body", [{ type: "id", value: "box" }]),
createInstancePair("box", "Box", [
{ type: "text", value: "some text" },
{ type: "id", value: "bold" },
]),
createInstancePair("bold", "Bold", [
{ type: "text", value: "some-bold" },
]),
]);
expect(
findClosestEditableInstanceSelector(
["bold", "box", "body"],
instances,
createFakeComponentMetas({})
)
).toEqual(["box", "body"]);
expect(
findClosestEditableInstanceSelector(
["box", "body"],
instances,
createFakeComponentMetas({})
)
).toEqual(["box", "body"]);
});
test("skips when container has anything except rich-text-child or text", () => {
const instances: Instances = new Map([
createInstancePair("body", "Body", [{ type: "id", value: "box" }]),
createInstancePair("box", "Box", [
{ type: "text", value: "some text" },
{ type: "id", value: "bold" },
{ type: "id", value: "child-box" },
]),
createInstancePair("bold", "Bold", [
{ type: "text", value: "some-bold" },
]),
createInstancePair("child-box", "Box", [
{ type: "text", value: "child-box" },
]),
]);
expect(
findClosestEditableInstanceSelector(
["bold", "box", "body"],
instances,
createFakeComponentMetas({})
)
).toEqual(undefined);
});
test("considers empty container as editable", () => {
const instances: Instances = new Map([
createInstancePair("body", "Body", []),
]);
expect(
findClosestEditableInstanceSelector(
["body"],
instances,
createFakeComponentMetas({})
)
).toEqual(["body"]);
});
});
describe("compute instances constraints", () => {
const base = {
type: "container",
label: "",
icon: "",
} as const;
test("combine required ancestors excluding already resolved ones", () => {
const metas = new Map<string, WsComponentMeta>([
["Button", { ...base, requiredAncestors: ["Form"] }],
["Checkbox", { ...base, requiredAncestors: ["Form", "Label"] }],
["Label", { ...base, requiredAncestors: ["Body"] }],
]);
// button
// label
// checkbox
const instances = new Map<Instance["id"], Instance>([
createInstancePair("button", "Button", []),
createInstancePair("label", "Label", [{ type: "id", value: "checkbox" }]),
createInstancePair("checkbox", "Checkbox", []),
]);
expect(
computeInstancesConstraints(metas, instances, ["button", "label"])
).toEqual({
requiredAncestors: new Set(["Body", "Form"]),
invalidAncestors: new Set(),
});
});
test("combine invalid ancestors of all instances", () => {
const metas = new Map<string, WsComponentMeta>([
["Button", { ...base, invalidAncestors: ["Button"] }],
["Form", { ...base, invalidAncestors: ["Button", "Form"] }],
]);
// form
// button
const instances = new Map<Instance["id"], Instance>([
createInstancePair("form", "Form", [{ type: "id", value: "button" }]),
createInstancePair("button", "Button", []),
]);
expect(computeInstancesConstraints(metas, instances, ["form"])).toEqual({
requiredAncestors: new Set(),
invalidAncestors: new Set(["Button", "Form"]),
});
});
});
describe("find closest droppable component index", () => {
test("finds container", () => {
expect(
findClosestDroppableComponentIndex(
createFakeComponentMetas({}),
["Box", "Body"],
emptyInsertConstraints
)
).toEqual(0);
});
test("skips non containers", () => {
expect(
findClosestDroppableComponentIndex(
createFakeComponentMetas({}),
["Bold", "Italic", "Text", "Box", "Body"],
emptyInsertConstraints
)
).toEqual(2);
});
test("considers invalid ancestors", () => {
expect(
findClosestDroppableComponentIndex(
createFakeComponentMetas({}),
["Box", "Item", "Body"],
{
requiredAncestors: new Set(),
invalidAncestors: new Set(["Item"]),
}
)
).toEqual(2);
});
test("requires some ancestor", () => {
expect(
findClosestDroppableComponentIndex(
createFakeComponentMetas({}),
["Box", "Body"],
{
requiredAncestors: new Set(["Form"]),
invalidAncestors: new Set(),
}
)
).toEqual(-1);
expect(
findClosestDroppableComponentIndex(
createFakeComponentMetas({}),
["Box", "Form", "Body"],
{
requiredAncestors: new Set(["Form"]),
invalidAncestors: new Set(),
}
)
).toEqual(0);
});
test("considers both required and invalid ancestors", () => {
expect(
findClosestDroppableComponentIndex(
createFakeComponentMetas({}),
["Div", "Box", "Form", "Body"],
{
requiredAncestors: new Set(["Form"]),
invalidAncestors: new Set(["Box"]),
}
)
).toEqual(2);
expect(
findClosestDroppableComponentIndex(
createFakeComponentMetas({}),
["Div", "Form", "Box", "Body"],
{
requiredAncestors: new Set(["Form"]),
invalidAncestors: new Set(["Box"]),
}
)
).toEqual(-1);
});
});
describe("find closest droppable target", () => {
const createInstancePair = (
id: Instance["id"],
component: string,
children: Instance["children"]
): [Instance["id"], Instance] => {
return [id, { type: "instance", id, component, children }];
};
test("puts in the end if closest instance is container", () => {
const instances = new Map([
createInstancePair("body", "Body", [{ type: "id", value: "box" }]),
createInstancePair("box", "Box", [{ type: "id", value: "paragraph" }]),
createInstancePair("paragraph", "Paragraph", [
{ type: "id", value: "bold" },
]),
]);
expect(
findClosestDroppableTarget(
defaultMetasMap,
instances,
["box", "body"],
emptyInsertConstraints
)
).toEqual({
parentSelector: ["box", "body"],
position: "end",
});
expect(
findClosestDroppableTarget(
defaultMetasMap,
instances,
["not-existing", "body"],
emptyInsertConstraints
)
).toEqual(undefined);
});
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,
});
});
});