| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573 |
- import { API } from "@excalidraw/excalidraw/tests/helpers/api";
- import type { ObservedAppState } from "@excalidraw/excalidraw/types";
- import type { LinearElementEditor } from "@excalidraw/element";
- import type { SceneElementsMap } from "@excalidraw/element/types";
- import { AppStateDelta, Delta, ElementsDelta } from "../src/delta";
- describe("ElementsDelta", () => {
- describe("elements delta calculation", () => {
- it("should not throw when element gets removed but was already deleted", () => {
- const element = API.createElement({
- type: "rectangle",
- x: 100,
- y: 100,
- isDeleted: true,
- });
- const prevElements = new Map([[element.id, element]]);
- const nextElements = new Map();
- expect(() =>
- ElementsDelta.calculate(prevElements, nextElements),
- ).not.toThrow();
- });
- it("should not throw when adding element as already deleted", () => {
- const element = API.createElement({
- type: "rectangle",
- x: 100,
- y: 100,
- isDeleted: true,
- });
- const prevElements = new Map();
- const nextElements = new Map([[element.id, element]]);
- expect(() =>
- ElementsDelta.calculate(prevElements, nextElements),
- ).not.toThrow();
- });
- it("should create updated delta even when there is only version and versionNonce change", () => {
- const baseElement = API.createElement({
- type: "rectangle",
- x: 100,
- y: 100,
- strokeColor: "#000000",
- backgroundColor: "#ffffff",
- });
- const modifiedElement = {
- ...baseElement,
- version: baseElement.version + 1,
- versionNonce: baseElement.versionNonce + 1,
- };
- // Create maps for the delta calculation
- const prevElements = new Map([[baseElement.id, baseElement]]);
- const nextElements = new Map([[modifiedElement.id, modifiedElement]]);
- // Calculate the delta
- const delta = ElementsDelta.calculate(
- prevElements as SceneElementsMap,
- nextElements as SceneElementsMap,
- );
- expect(delta).toEqual(
- ElementsDelta.create(
- {},
- {},
- {
- [baseElement.id]: Delta.create(
- {
- version: baseElement.version,
- versionNonce: baseElement.versionNonce,
- },
- {
- version: baseElement.version + 1,
- versionNonce: baseElement.versionNonce + 1,
- },
- ),
- },
- ),
- );
- });
- });
- describe("squash", () => {
- it("should not squash when second delta is empty", () => {
- const updatedDelta = Delta.create(
- { x: 100, version: 1, versionNonce: 1 },
- { x: 200, version: 2, versionNonce: 2 },
- );
- const elementsDelta1 = ElementsDelta.create(
- {},
- {},
- { id1: updatedDelta },
- );
- const elementsDelta2 = ElementsDelta.empty();
- const elementsDelta = elementsDelta1.squash(elementsDelta2);
- expect(elementsDelta.isEmpty()).toBeFalsy();
- expect(elementsDelta).toBe(elementsDelta1);
- expect(elementsDelta.updated.id1).toBe(updatedDelta);
- });
- it("should squash mutually exclusive delta types", () => {
- const addedDelta = Delta.create(
- { x: 100, version: 1, versionNonce: 1, isDeleted: true },
- { x: 200, version: 2, versionNonce: 2, isDeleted: false },
- );
- const removedDelta = Delta.create(
- { x: 100, version: 1, versionNonce: 1, isDeleted: false },
- { x: 200, version: 2, versionNonce: 2, isDeleted: true },
- );
- const updatedDelta = Delta.create(
- { x: 100, version: 1, versionNonce: 1 },
- { x: 200, version: 2, versionNonce: 2 },
- );
- const elementsDelta1 = ElementsDelta.create(
- { id1: addedDelta },
- { id2: removedDelta },
- {},
- );
- const elementsDelta2 = ElementsDelta.create(
- {},
- {},
- { id3: updatedDelta },
- );
- const elementsDelta = elementsDelta1.squash(elementsDelta2);
- expect(elementsDelta.isEmpty()).toBeFalsy();
- expect(elementsDelta).toBe(elementsDelta1);
- expect(elementsDelta.added.id1).toBe(addedDelta);
- expect(elementsDelta.removed.id2).toBe(removedDelta);
- expect(elementsDelta.updated.id3).toBe(updatedDelta);
- });
- it("should squash the same delta types", () => {
- const elementsDelta1 = ElementsDelta.create(
- {
- id1: Delta.create(
- { x: 100, version: 1, versionNonce: 1, isDeleted: true },
- { x: 200, version: 2, versionNonce: 2, isDeleted: false },
- ),
- },
- {
- id2: Delta.create(
- { x: 100, version: 1, versionNonce: 1, isDeleted: false },
- { x: 200, version: 2, versionNonce: 2, isDeleted: true },
- ),
- },
- {
- id3: Delta.create(
- { x: 100, version: 1, versionNonce: 1 },
- { x: 200, version: 2, versionNonce: 2 },
- ),
- },
- );
- const elementsDelta2 = ElementsDelta.create(
- {
- id1: Delta.create(
- { y: 100, version: 2, versionNonce: 2, isDeleted: true },
- { y: 200, version: 3, versionNonce: 3, isDeleted: false },
- ),
- },
- {
- id2: Delta.create(
- { y: 100, version: 2, versionNonce: 2, isDeleted: false },
- { y: 200, version: 3, versionNonce: 3, isDeleted: true },
- ),
- },
- {
- id3: Delta.create(
- { y: 100, version: 2, versionNonce: 2 },
- { y: 200, version: 3, versionNonce: 3 },
- ),
- },
- );
- const elementsDelta = elementsDelta1.squash(elementsDelta2);
- expect(elementsDelta.isEmpty()).toBeFalsy();
- expect(elementsDelta).toBe(elementsDelta1);
- expect(elementsDelta.added.id1).toEqual(
- Delta.create(
- { x: 100, y: 100, version: 2, versionNonce: 2, isDeleted: true },
- { x: 200, y: 200, version: 3, versionNonce: 3, isDeleted: false },
- ),
- );
- expect(elementsDelta.removed.id2).toEqual(
- Delta.create(
- { x: 100, y: 100, version: 2, versionNonce: 2, isDeleted: false },
- { x: 200, y: 200, version: 3, versionNonce: 3, isDeleted: true },
- ),
- );
- expect(elementsDelta.updated.id3).toEqual(
- Delta.create(
- { x: 100, y: 100, version: 2, versionNonce: 2 },
- { x: 200, y: 200, version: 3, versionNonce: 3 },
- ),
- );
- });
- it("should squash different delta types ", () => {
- // id1: added -> updated => added
- // id2: removed -> added => added
- // id3: updated -> removed => removed
- const elementsDelta1 = ElementsDelta.create(
- {
- id1: Delta.create(
- { x: 100, version: 1, versionNonce: 1, isDeleted: true },
- { x: 101, version: 2, versionNonce: 2, isDeleted: false },
- ),
- },
- {
- id2: Delta.create(
- { x: 200, version: 1, versionNonce: 1, isDeleted: false },
- { x: 201, version: 2, versionNonce: 2, isDeleted: true },
- ),
- },
- {
- id3: Delta.create(
- { x: 300, version: 1, versionNonce: 1 },
- { x: 301, version: 2, versionNonce: 2 },
- ),
- },
- );
- const elementsDelta2 = ElementsDelta.create(
- {
- id2: Delta.create(
- { y: 200, version: 2, versionNonce: 2, isDeleted: true },
- { y: 201, version: 3, versionNonce: 3, isDeleted: false },
- ),
- },
- {
- id3: Delta.create(
- { y: 300, version: 2, versionNonce: 2, isDeleted: false },
- { y: 301, version: 3, versionNonce: 3, isDeleted: true },
- ),
- },
- {
- id1: Delta.create(
- { y: 100, version: 2, versionNonce: 2 },
- { y: 101, version: 3, versionNonce: 3 },
- ),
- },
- );
- const elementsDelta = elementsDelta1.squash(elementsDelta2);
- expect(elementsDelta.isEmpty()).toBeFalsy();
- expect(elementsDelta).toBe(elementsDelta1);
- expect(elementsDelta.added).toEqual({
- id1: Delta.create(
- { x: 100, y: 100, version: 2, versionNonce: 2, isDeleted: true },
- { x: 101, y: 101, version: 3, versionNonce: 3, isDeleted: false },
- ),
- id2: Delta.create(
- { x: 200, y: 200, version: 2, versionNonce: 2, isDeleted: true },
- { x: 201, y: 201, version: 3, versionNonce: 3, isDeleted: false },
- ),
- });
- expect(elementsDelta.removed).toEqual({
- id3: Delta.create(
- { x: 300, y: 300, version: 2, versionNonce: 2, isDeleted: false },
- { x: 301, y: 301, version: 3, versionNonce: 3, isDeleted: true },
- ),
- });
- expect(elementsDelta.updated).toEqual({});
- });
- it("should squash bound elements", () => {
- const elementsDelta1 = ElementsDelta.create(
- {},
- {},
- {
- id1: Delta.create(
- {
- version: 1,
- versionNonce: 1,
- boundElements: [{ id: "t1", type: "text" }],
- },
- {
- version: 2,
- versionNonce: 2,
- boundElements: [{ id: "t2", type: "text" }],
- },
- ),
- },
- );
- const elementsDelta2 = ElementsDelta.create(
- {},
- {},
- {
- id1: Delta.create(
- {
- version: 2,
- versionNonce: 2,
- boundElements: [{ id: "a1", type: "arrow" }],
- },
- {
- version: 3,
- versionNonce: 3,
- boundElements: [{ id: "a2", type: "arrow" }],
- },
- ),
- },
- );
- const elementsDelta = elementsDelta1.squash(elementsDelta2);
- expect(elementsDelta.updated.id1.deleted.boundElements).toEqual([
- { id: "t1", type: "text" },
- { id: "a1", type: "arrow" },
- ]);
- expect(elementsDelta.updated.id1.inserted.boundElements).toEqual([
- { id: "t2", type: "text" },
- { id: "a2", type: "arrow" },
- ]);
- });
- });
- });
- describe("AppStateDelta", () => {
- describe("ensure stable delta properties order", () => {
- it("should maintain stable order for root properties", () => {
- const name = "untitled scene";
- const selectedLinearElement = {
- elementId: "id1" as LinearElementEditor["elementId"],
- isEditing: false,
- };
- const commonAppState = {
- viewBackgroundColor: "#ffffff",
- selectedElementIds: {},
- selectedGroupIds: {},
- editingGroupId: null,
- croppingElementId: null,
- editingLinearElementId: null,
- selectedLinearElementIsEditing: null,
- lockedMultiSelections: {},
- activeLockedId: null,
- };
- const prevAppState1: ObservedAppState = {
- ...commonAppState,
- name: "",
- selectedLinearElement: null,
- };
- const nextAppState1: ObservedAppState = {
- ...commonAppState,
- name,
- selectedLinearElement,
- };
- const prevAppState2: ObservedAppState = {
- selectedLinearElement: null,
- name: "",
- ...commonAppState,
- };
- const nextAppState2: ObservedAppState = {
- selectedLinearElement,
- name,
- ...commonAppState,
- };
- const delta1 = AppStateDelta.calculate(prevAppState1, nextAppState1);
- const delta2 = AppStateDelta.calculate(prevAppState2, nextAppState2);
- expect(JSON.stringify(delta1)).toBe(JSON.stringify(delta2));
- });
- it("should maintain stable order for selectedElementIds", () => {
- const commonAppState = {
- name: "",
- viewBackgroundColor: "#ffffff",
- selectedGroupIds: {},
- editingGroupId: null,
- croppingElementId: null,
- selectedLinearElement: null,
- activeLockedId: null,
- lockedMultiSelections: {},
- };
- const prevAppState1: ObservedAppState = {
- ...commonAppState,
- selectedElementIds: { id5: true, id2: true, id4: true },
- };
- const nextAppState1: ObservedAppState = {
- ...commonAppState,
- selectedElementIds: {
- id1: true,
- id2: true,
- id3: true,
- },
- };
- const prevAppState2: ObservedAppState = {
- ...commonAppState,
- selectedElementIds: { id4: true, id2: true, id5: true },
- };
- const nextAppState2: ObservedAppState = {
- ...commonAppState,
- selectedElementIds: {
- id3: true,
- id2: true,
- id1: true,
- },
- };
- const delta1 = AppStateDelta.calculate(prevAppState1, nextAppState1);
- const delta2 = AppStateDelta.calculate(prevAppState2, nextAppState2);
- expect(JSON.stringify(delta1)).toBe(JSON.stringify(delta2));
- });
- it("should maintain stable order for selectedGroupIds", () => {
- const commonAppState = {
- name: "",
- viewBackgroundColor: "#ffffff",
- selectedElementIds: {},
- editingGroupId: null,
- croppingElementId: null,
- selectedLinearElement: null,
- activeLockedId: null,
- lockedMultiSelections: {},
- };
- const prevAppState1: ObservedAppState = {
- ...commonAppState,
- selectedGroupIds: { id5: false, id2: true, id4: true, id0: true },
- };
- const nextAppState1: ObservedAppState = {
- ...commonAppState,
- selectedGroupIds: {
- id0: true,
- id1: true,
- id2: false,
- id3: true,
- },
- };
- const prevAppState2: ObservedAppState = {
- ...commonAppState,
- selectedGroupIds: { id0: true, id4: true, id2: true, id5: false },
- };
- const nextAppState2: ObservedAppState = {
- ...commonAppState,
- selectedGroupIds: {
- id3: true,
- id2: false,
- id1: true,
- id0: true,
- },
- };
- const delta1 = AppStateDelta.calculate(prevAppState1, nextAppState1);
- const delta2 = AppStateDelta.calculate(prevAppState2, nextAppState2);
- expect(JSON.stringify(delta1)).toBe(JSON.stringify(delta2));
- });
- });
- describe("squash", () => {
- it("should not squash when second delta is empty", () => {
- const delta = Delta.create(
- { name: "untitled scene" },
- { name: "titled scene" },
- );
- const appStateDelta1 = AppStateDelta.create(delta);
- const appStateDelta2 = AppStateDelta.empty();
- const appStateDelta = appStateDelta1.squash(appStateDelta2);
- expect(appStateDelta.isEmpty()).toBeFalsy();
- expect(appStateDelta).toBe(appStateDelta1);
- expect(appStateDelta.delta).toBe(delta);
- });
- it("should squash exclusive properties", () => {
- const delta1 = Delta.create(
- { name: "untitled scene" },
- { name: "titled scene" },
- );
- const delta2 = Delta.create(
- { viewBackgroundColor: "#ffffff" },
- { viewBackgroundColor: "#000000" },
- );
- const appStateDelta1 = AppStateDelta.create(delta1);
- const appStateDelta2 = AppStateDelta.create(delta2);
- const appStateDelta = appStateDelta1.squash(appStateDelta2);
- expect(appStateDelta.isEmpty()).toBeFalsy();
- expect(appStateDelta).toBe(appStateDelta1);
- expect(appStateDelta.delta).toEqual(
- Delta.create(
- { name: "untitled scene", viewBackgroundColor: "#ffffff" },
- { name: "titled scene", viewBackgroundColor: "#000000" },
- ),
- );
- });
- it("should squash selectedElementIds, selectedGroupIds and lockedMultiSelections", () => {
- const delta1 = Delta.create<Partial<ObservedAppState>>(
- {
- name: "untitled scene",
- selectedElementIds: { id1: true },
- selectedGroupIds: {},
- lockedMultiSelections: { g1: true },
- },
- {
- name: "titled scene",
- selectedElementIds: { id2: true },
- selectedGroupIds: { g1: true },
- lockedMultiSelections: {},
- },
- );
- const delta2 = Delta.create<Partial<ObservedAppState>>(
- {
- selectedElementIds: { id3: true },
- selectedGroupIds: { g1: true },
- lockedMultiSelections: {},
- },
- {
- selectedElementIds: { id2: true },
- selectedGroupIds: { g2: true, g3: true },
- lockedMultiSelections: { g3: true },
- },
- );
- const appStateDelta1 = AppStateDelta.create(delta1);
- const appStateDelta2 = AppStateDelta.create(delta2);
- const appStateDelta = appStateDelta1.squash(appStateDelta2);
- expect(appStateDelta.isEmpty()).toBeFalsy();
- expect(appStateDelta).toBe(appStateDelta1);
- expect(appStateDelta.delta).toEqual(
- Delta.create<Partial<ObservedAppState>>(
- {
- name: "untitled scene",
- selectedElementIds: { id1: true, id3: true },
- selectedGroupIds: { g1: true },
- lockedMultiSelections: { g1: true },
- },
- {
- name: "titled scene",
- selectedElementIds: { id2: true },
- selectedGroupIds: { g1: true, g2: true, g3: true },
- lockedMultiSelections: { g3: true },
- },
- ),
- );
- });
- });
- });
|