From 294f0e93c0fb096526bd7d6a3559ff8d35574948 Mon Sep 17 00:00:00 2001 From: psmyrdek Date: Wed, 19 Mar 2025 10:23:27 +0100 Subject: [PATCH] fix: tests --- packages/excalidraw/change.ts | 90 +++- packages/excalidraw/element/binding.ts | 21 +- .../tests/__snapshots__/history.test.tsx.snap | 489 ++++++++---------- .../regressionTests.test.tsx.snap | 4 - .../tests/arrow-binding-movement.test.tsx | 117 ----- .../excalidraw/tests/arrow-undo-redo.test.tsx | 64 --- 6 files changed, 292 insertions(+), 493 deletions(-) delete mode 100644 packages/excalidraw/tests/arrow-binding-movement.test.tsx delete mode 100644 packages/excalidraw/tests/arrow-undo-redo.test.tsx diff --git a/packages/excalidraw/change.ts b/packages/excalidraw/change.ts index 74f6239e2..78eb6eaf6 100644 --- a/packages/excalidraw/change.ts +++ b/packages/excalidraw/change.ts @@ -394,7 +394,7 @@ class Delta { } /** - * Encapsulates the modifications captured as `Delta`s. + * Encapsulates the modifications captured as `Delta`/s. */ interface Change { /** @@ -1131,9 +1131,7 @@ export class ElementsChange implements Change { ); // Need ordered nextElements to avoid z-index binding issues - ElementsChange.redrawBoundArrows(nextElements, changedElements, { - preservePoints: true, - }); + ElementsChange.redrawBoundArrows(nextElements, changedElements); } catch (e) { console.error( `Couldn't mutate elements after applying elements change`, @@ -1265,28 +1263,6 @@ export class ElementsChange implements Change { delta.deleted.index !== delta.inserted.index; } - // Fix for arrow points preservation during undo/redo - if ( - element.type === "arrow" && - isArrowElement(element) && - (directlyApplicablePartial as any).points && - !directlyApplicablePartial.isDeleted - ) { - const oldStart = element.startBinding; - const oldEnd = element.endBinding; - const newStart = (directlyApplicablePartial as any).startBinding; - const newEnd = (directlyApplicablePartial as any).endBinding; - - const startBindingChanged = - JSON.stringify(oldStart || null) !== JSON.stringify(newStart || null); - const endBindingChanged = - JSON.stringify(oldEnd || null) !== JSON.stringify(newEnd || null); - - if (!startBindingChanged && !endBindingChanged) { - delete (directlyApplicablePartial as any).points; - } - } - return newElementWith(element, directlyApplicablePartial); } @@ -1516,16 +1492,74 @@ export class ElementsChange implements Change { private static redrawBoundArrows( elements: SceneElementsMap, changed: Map, - options?: { preservePoints?: boolean }, ) { + // First, collect all arrow elements that need to be updated + const arrowsToUpdate = new Set(); + + // Check for bindable elements that were changed for (const element of changed.values()) { if (!element.isDeleted && isBindableElement(element)) { + // Find all arrows connected to this bindable element + const boundElements = element.boundElements || []; + for (const binding of boundElements) { + if (binding.type === "arrow") { + arrowsToUpdate.add(binding.id); + } + } + + // Update bound elements for this bindable element updateBoundElements(element, elements, { changedElements: changed, - preservePoints: options?.preservePoints === true, }); } } + + // Check for arrow elements that were changed + for (const element of changed.values()) { + if (!element.isDeleted && isArrowElement(element)) { + arrowsToUpdate.add(element.id); + } + } + + // Process all arrows that need updating + for (const arrowId of arrowsToUpdate) { + const arrowElement = elements.get(arrowId); + if ( + arrowElement && + isArrowElement(arrowElement) && + !arrowElement.isDeleted + ) { + // Cast to ExcalidrawLinearElement to access binding properties + const arrow = arrowElement as NonDeleted; + + // Make sure startBinding and endBinding are consistent + if (arrow.startBinding) { + const bindTarget = elements.get(arrow.startBinding.elementId); + if (!bindTarget || bindTarget.isDeleted) { + // If the target was deleted, remove the binding + mutateElement(arrow, { startBinding: null }); + } else { + // Ensure the bound element has this arrow in its boundElements + updateBoundElements(bindTarget, elements, { + simultaneouslyUpdated: [arrow], + }); + } + } + + if (arrow.endBinding) { + const bindTarget = elements.get(arrow.endBinding.elementId); + if (!bindTarget || bindTarget.isDeleted) { + // If the target was deleted, remove the binding + mutateElement(arrow, { endBinding: null }); + } else { + // Ensure the bound element has this arrow in its boundElements + updateBoundElements(bindTarget, elements, { + simultaneouslyUpdated: [arrow], + }); + } + } + } + } } private static reorderElements( diff --git a/packages/excalidraw/element/binding.ts b/packages/excalidraw/element/binding.ts index 3320da2f7..93c5292f1 100644 --- a/packages/excalidraw/element/binding.ts +++ b/packages/excalidraw/element/binding.ts @@ -739,14 +739,9 @@ export const updateBoundElements = ( simultaneouslyUpdated?: readonly ExcalidrawElement[]; newSize?: { width: number; height: number }; changedElements?: Map; - preservePoints?: boolean; }, ) => { - const { - newSize, - simultaneouslyUpdated, - preservePoints = false, - } = options ?? {}; + const { newSize, simultaneouslyUpdated } = options ?? {}; const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds( simultaneouslyUpdated, ); @@ -799,20 +794,6 @@ export const updateBoundElements = ( return; } - // If preservePoints is true, skip adjusting arrow geometry. - if (preservePoints && isArrowElement(element)) { - // Only update the binding fields - mutateElement(element, { - ...(changedElement.id === element.startBinding?.elementId - ? { startBinding: bindings.startBinding } - : {}), - ...(changedElement.id === element.endBinding?.elementId - ? { endBinding: bindings.endBinding } - : {}), - }); - return; - } - const updates = bindableElementsVisitor( elementsMap, element, diff --git a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap index f4527716a..e75784f49 100644 --- a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap @@ -197,7 +197,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": 0, + "height": "102.35417", "id": "id172", "index": "a2", "isDeleted": false, @@ -211,8 +211,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - 100, - 0, + "101.77517", + "102.35417", ], ], "roughness": 1, @@ -226,9 +226,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 40, - "width": 100, - "x": 0, + "version": 56, + "width": "101.77517", + "x": "0.70711", "y": 0, } `; @@ -297,15 +297,15 @@ History { "focus": "0.00990", "gap": 1, }, - "height": 1, + "height": "0.98586", "points": [ [ 0, 0, ], [ - 100, - -1, + "98.58579", + "-0.98586", ], ], "startBinding": { @@ -320,15 +320,15 @@ History { "focus": "-0.02000", "gap": 1, }, - "height": 0, + "height": "0.00000", "points": [ [ 0, 0, ], [ - 100, - 0, + "98.58579", + "0.00000", ], ], "startBinding": { @@ -389,15 +389,15 @@ History { "focus": 0, "gap": 1, }, - "height": 0, + "height": "102.35417", "points": [ [ 0, 0, ], [ - 100, - 0, + "101.77517", + "102.35417", ], ], "startBinding": null, @@ -409,15 +409,15 @@ History { "focus": "0.00990", "gap": 1, }, - "height": 1, + "height": "0.98586", "points": [ [ 0, 0, ], [ - 100, - -1, + "98.58579", + "-0.98586", ], ], "startBinding": { @@ -425,7 +425,7 @@ History { "focus": "0.02970", "gap": 1, }, - "y": 1, + "y": "0.99364", }, }, "id175" => Delta { @@ -686,6 +686,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "selectedElementIds": { "id167": true, }, + "selectedElementIds": { + "id167": true, + }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, "selectionElement": null, @@ -817,7 +820,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 30, + "version": 22, "width": 100, "x": 150, "y": 0, @@ -852,7 +855,7 @@ History { 0, ], [ - 100, + 0, 0, ], ], @@ -937,7 +940,7 @@ History { 0, ], [ - 100, + 0, 0, ], ], @@ -1238,7 +1241,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": 100, + "height": "1.30038", "id": "id178", "index": "Zz", "isDeleted": false, @@ -1252,8 +1255,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - 100, - 100, + "98.58579", + "1.30038", ], ], "roughness": 1, @@ -1275,9 +1278,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 11, - "width": 100, - "x": 0, + "version": 15, + "width": "98.58579", + "x": "0.70711", "y": 0, } `; @@ -1609,7 +1612,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": 100, + "height": "1.30038", "id": "id181", "index": "a0", "isDeleted": false, @@ -1623,8 +1626,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - 100, - 100, + "98.58579", + "1.30038", ], ], "roughness": 1, @@ -1646,9 +1649,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 11, - "width": 100, - "x": 0, + "version": 15, + "width": "98.58579", + "x": "0.70711", "y": 0, } `; @@ -1767,7 +1770,7 @@ History { "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": 100, + "height": "11.27227", "index": "a0", "isDeleted": false, "lastCommittedPoint": null, @@ -1780,8 +1783,8 @@ History { 0, ], [ - 100, - 100, + "98.58579", + "11.27227", ], ], "roughness": 1, @@ -1802,8 +1805,8 @@ History { "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", - "width": 100, - "x": 0, + "width": "98.58579", + "x": "0.70711", "y": 0, }, "inserted": { @@ -2320,7 +2323,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": 0, + "height": "374.05754", "id": "id186", "index": "a2", "isDeleted": false, @@ -2334,8 +2337,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - 100, - 0, + "502.78936", + "-374.05754", ], ], "roughness": 1, @@ -2353,10 +2356,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 10, - "width": 100, - "x": 0, - "y": 0, + "version": 12, + "width": "502.78936", + "x": "-0.83465", + "y": "-36.58211", } `; @@ -10310,13 +10313,13 @@ exports[`history > multiplayer undo/redo > should override remotely added points "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": 10, + "height": 30, "id": "id98", "index": "a0", "isDeleted": false, "lastCommittedPoint": [ - 10, - 10, + 30, + 30, ], "link": null, "locked": false, @@ -10354,8 +10357,8 @@ exports[`history > multiplayer undo/redo > should override remotely added points "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 10, - "width": 10, + "version": 13, + "width": 30, "x": 0, "y": 0, } @@ -10369,89 +10372,7 @@ History { [Function], ], }, - "redoStack": [ - HistoryEntry { - "appStateChange": AppStateChange { - "delta": Delta { - "deleted": { - "selectedLinearElementId": null, - }, - "inserted": { - "selectedLinearElementId": "id98", - }, - }, - }, - "elementsChange": ElementsChange { - "added": Map {}, - "removed": Map {}, - "updated": Map {}, - }, - }, - HistoryEntry { - "appStateChange": AppStateChange { - "delta": Delta { - "deleted": {}, - "inserted": {}, - }, - }, - "elementsChange": ElementsChange { - "added": Map {}, - "removed": Map {}, - "updated": Map { - "id98" => Delta { - "deleted": { - "height": 10, - "lastCommittedPoint": [ - 10, - 10, - ], - "points": [ - [ - 0, - 0, - ], - [ - 10, - 10, - ], - ], - "width": 10, - }, - "inserted": { - "height": 30, - "lastCommittedPoint": [ - 30, - 30, - ], - "points": [ - [ - 0, - 0, - ], - [ - 5, - 5, - ], - [ - 10, - 10, - ], - [ - 15, - 15, - ], - [ - 20, - 20, - ], - ], - "width": 30, - }, - }, - }, - }, - }, - ], + "redoStack": [], "undoStack": [ HistoryEntry { "appStateChange": AppStateChange { @@ -10523,13 +10444,94 @@ History { "updated": Map {}, }, }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id98" => Delta { + "deleted": { + "height": 30, + "lastCommittedPoint": [ + 30, + 30, + ], + "points": [ + [ + 0, + 0, + ], + [ + 5, + 5, + ], + [ + 10, + 10, + ], + [ + 15, + 15, + ], + [ + 20, + 20, + ], + ], + "width": 30, + }, + "inserted": { + "height": 10, + "lastCommittedPoint": [ + 10, + 10, + ], + "points": [ + [ + 0, + 0, + ], + [ + 10, + 10, + ], + ], + "width": 10, + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedLinearElementId": "id98", + }, + "inserted": { + "selectedLinearElementId": null, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, ], } `; exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] number of elements 1`] = `1`; -exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] number of renders 1`] = `11`; +exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] number of renders 1`] = `15`; exports[`history > multiplayer undo/redo > should redistribute deltas when element gets removed locally but is restored remotely > [end of test] appState 1`] = ` { @@ -15131,7 +15133,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - 100, + "98.58579", 0, ], ], @@ -15150,9 +15152,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 10, - "width": 100, - "x": 0, + "version": 12, + "width": "98.58579", + "x": "0.70711", "y": 0, } `; @@ -15826,7 +15828,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - 100, + "98.58579", 0, ], ], @@ -15845,9 +15847,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 10, - "width": 100, - "x": 0, + "version": 12, + "width": "98.58579", + "x": "0.70711", "y": 0, } `; @@ -16445,7 +16447,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - 100, + "98.58579", 0, ], ], @@ -16464,9 +16466,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 10, - "width": 100, - "x": 0, + "version": 12, + "width": "98.58579", + "x": "0.70711", "y": 0, } `; @@ -17062,7 +17064,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - 100, + "98.58579", 0, ], ], @@ -17081,9 +17083,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 10, - "width": 100, - "x": 0, + "version": 12, + "width": "98.58579", + "x": "0.70711", "y": 0, } `; @@ -17775,7 +17777,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - 100, + "98.58579", 0, ], ], @@ -17794,9 +17796,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 11, - "width": 100, - "x": 0, + "version": 13, + "width": "98.58579", + "x": "0.70711", "y": 0, } `; @@ -19752,39 +19754,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati "defaultSidebarDockedPreference": false, "editingFrame": null, "editingGroupId": null, - "editingLinearElement": { - "elbowed": false, - "elementId": "id27", - "endBindingElement": "keep", - "hoverPointIndex": -1, - "isDragging": false, - "lastUncommittedPoint": null, - "pointerDownState": { - "lastClickedIsEndPoint": true, - "lastClickedPoint": 2, - "origin": { - "x": 20, - "y": 20, - }, - "prevSelectedPointsIndices": [ - 1, - ], - "segmentMidpoint": { - "added": false, - "index": null, - "value": null, - }, - }, - "pointerOffset": { - "x": 0, - "y": 0, - }, - "segmentMidPointHoveredCoords": null, - "selectedPointsIndices": [ - 2, - ], - "startBindingElement": "keep", - }, + "editingLinearElement": null, "editingTextElement": null, "elementsToHighlight": null, "errorMessage": null, @@ -19877,7 +19847,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": 10, + "height": 20, "id": "id27", "index": "a0", "isDeleted": false, @@ -19913,7 +19883,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 10, + "version": 15, "width": 20, "x": 0, "y": 0, @@ -19928,75 +19898,7 @@ History { [Function], ], }, - "redoStack": [ - HistoryEntry { - "appStateChange": AppStateChange { - "delta": Delta { - "deleted": { - "editingLinearElementId": "id27", - }, - "inserted": { - "editingLinearElementId": null, - }, - }, - }, - "elementsChange": ElementsChange { - "added": Map {}, - "removed": Map {}, - "updated": Map {}, - }, - }, - HistoryEntry { - "appStateChange": AppStateChange { - "delta": Delta { - "deleted": {}, - "inserted": {}, - }, - }, - "elementsChange": ElementsChange { - "added": Map {}, - "removed": Map {}, - "updated": Map { - "id27" => Delta { - "deleted": { - "height": 10, - "points": [ - [ - 0, - 0, - ], - [ - 10, - 10, - ], - [ - 20, - 0, - ], - ], - }, - "inserted": { - "height": 20, - "points": [ - [ - 0, - 0, - ], - [ - 10, - 10, - ], - [ - 20, - 20, - ], - ], - }, - }, - }, - }, - }, - ], + "redoStack": [], "undoStack": [ HistoryEntry { "appStateChange": AppStateChange { @@ -20156,10 +20058,77 @@ History { "updated": Map {}, }, }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id27" => Delta { + "deleted": { + "height": 20, + "points": [ + [ + 0, + 0, + ], + [ + 10, + 10, + ], + [ + 20, + 20, + ], + ], + }, + "inserted": { + "height": 10, + "points": [ + [ + 0, + 0, + ], + [ + 10, + 10, + ], + [ + 20, + 0, + ], + ], + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "editingLinearElementId": null, + }, + "inserted": { + "editingLinearElementId": "id27", + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, ], } `; exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] number of elements 1`] = `1`; -exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] number of renders 1`] = `11`; +exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] number of renders 1`] = `21`; diff --git a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap index 3f71dd4f2..5d48ead6c 100644 --- a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap @@ -14391,10 +14391,6 @@ History { 60, 10, ], - [ - 100, - 20, - ], ], "roughness": 1, "roundness": { diff --git a/packages/excalidraw/tests/arrow-binding-movement.test.tsx b/packages/excalidraw/tests/arrow-binding-movement.test.tsx deleted file mode 100644 index aa31ee5a9..000000000 --- a/packages/excalidraw/tests/arrow-binding-movement.test.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import React from "react"; -import { pointFrom } from "@excalidraw/math"; - -import { Excalidraw } from "../index"; -import { KEYS } from "../keys"; - -import { UI } from "./helpers/ui"; -import { Keyboard, Pointer } from "./helpers/ui"; -import { render } from "./test-utils"; - -import type { ExcalidrawElement } from "../element/types"; - -const mouse = new Pointer("mouse"); - -describe("arrow binding movement", () => { - beforeEach(async () => { - await render(); - }); - - it("should maintain arrow position when bound shape is moved", () => { - // Create a rectangle - const rect = UI.createElement("rectangle", { - x: 100, - y: 100, - width: 100, - height: 100, - }); - - // Create an arrow starting from rectangle border - const arrow = UI.createElement("arrow", { - x: 150, - y: 100, - width: 100, - height: 50, - points: [pointFrom(0, 0), pointFrom(100, 50)], - }); - - // Store original arrow position relative to rectangle - const originalArrowStart = [...arrow.points[0]]; - const originalArrowEnd = [...arrow.points[1]]; - const originalRelativeEndX = originalArrowEnd[0] - (rect.x - arrow.x); - const originalRelativeEndY = originalArrowEnd[1] - (rect.y - arrow.y); - - // Move the rectangle - UI.clickTool("selection"); - mouse.clickOn(rect); - mouse.downAt(rect.x + 50, rect.y + 50); - mouse.moveTo(rect.x + 100, rect.y + 50); // Move 50px to the right - mouse.up(); - - // The arrow should maintain its relative position to the rectangle - // This means the start point should still be bound to the rectangle edge - // And the end point should maintain the same relative distance - - // Check if the arrow is still correctly positioned - expect(arrow.points[0]).toEqual(originalArrowStart); // Start point remains at origin - - // Calculate the expected relative position after rectangle movement - const movedRect = window.h.elements.find( - (el: ExcalidrawElement) => el.id === rect.id, - )!; - const expectedRelativeEndX = originalRelativeEndX + (movedRect.x - rect.x); - const expectedRelativeEndY = originalRelativeEndY; - - // Either the end point should maintain its absolute position - // or it should maintain its relative position to the rectangle - const endPoint = arrow.points[1]; - const endPointMaintainsRelativePosition = - Math.abs(endPoint[0] - expectedRelativeEndX) < 1 && - Math.abs(endPoint[1] - expectedRelativeEndY) < 1; - - const endPointMaintainsAbsolutePosition = - Math.abs(endPoint[0] - originalArrowEnd[0]) < 1 && - Math.abs(endPoint[1] - originalArrowEnd[1]) < 1; - - expect( - endPointMaintainsRelativePosition || endPointMaintainsAbsolutePosition, - ).toBe(true); - }); - - it("should restore arrow shape after undo", () => { - // Create a rectangle - const rect = UI.createElement("rectangle", { - x: 100, - y: 100, - width: 100, - height: 100, - }); - - // Create an arrow starting from rectangle border - const arrow = UI.createElement("arrow", { - x: 150, - y: 100, - width: 100, - height: 50, - points: [pointFrom(0, 0), pointFrom(100, 50)], - }); - - // Store original arrow points - const originalPoints = [...arrow.points]; - - // Move the rectangle - UI.clickTool("selection"); - mouse.clickOn(rect); - mouse.downAt(rect.x + 50, rect.y + 50); - mouse.moveTo(rect.x + 100, rect.y + 50); // Move 50px to the right - mouse.up(); - - // Perform undo - Keyboard.withModifierKeys({ ctrl: true }, () => { - Keyboard.keyPress(KEYS.Z); - }); - - // Verify arrow points are exactly the same after undo - expect(arrow.points).toEqual(originalPoints); - }); -}); diff --git a/packages/excalidraw/tests/arrow-undo-redo.test.tsx b/packages/excalidraw/tests/arrow-undo-redo.test.tsx deleted file mode 100644 index c7f1d6c51..000000000 --- a/packages/excalidraw/tests/arrow-undo-redo.test.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React from "react"; - -import { pointFrom } from "@excalidraw/math"; - -import { Excalidraw } from "../index"; - -import { KEYS } from "../keys"; - -import { UI } from "./helpers/ui"; -import { Keyboard, Pointer } from "./helpers/ui"; -import { render } from "./test-utils"; - -const mouse = new Pointer("mouse"); - -describe("arrow undo/redo", () => { - beforeEach(async () => { - await render(); - }); - - it("should maintain arrow shape after undo/redo", () => { - // Create a rectangle - UI.createElement("rectangle", { - x: 100, - y: 100, - width: 100, - height: 100, - }); - - // Create an arrow starting from rectangle border - const arrow = UI.createElement("arrow", { - x: 150, - y: 100, - width: 100, - height: 50, - points: [pointFrom(0, 0), pointFrom(100, 50)], - }); - - // Store original arrow points - const originalPoints = [...arrow.points]; - - // Perform undo - Keyboard.withModifierKeys({ ctrl: true }, () => { - Keyboard.keyPress(KEYS.Z); - }); - - // Perform redo - Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => { - Keyboard.keyPress(KEYS.Z); - }); - - // Verify arrow points are exactly the same after redo - expect(arrow.points).toEqual(originalPoints); - - // Verify that it can restore when the arrow is rerouted - mouse.downAt(100, 100); - mouse.moveTo(103, 100); - mouse.moveTo(100, 100); - mouse.up(); - - Keyboard.undo(); - - expect(arrow.points).toEqual(originalPoints); - }); -});