fix: tests

This commit is contained in:
psmyrdek 2025-03-19 10:23:27 +01:00
parent b7b28d5d54
commit 294f0e93c0
6 changed files with 292 additions and 493 deletions

View file

@ -394,7 +394,7 @@ class Delta<T> {
} }
/** /**
* Encapsulates the modifications captured as `Delta`s. * Encapsulates the modifications captured as `Delta`/s.
*/ */
interface Change<T> { interface Change<T> {
/** /**
@ -1131,9 +1131,7 @@ export class ElementsChange implements Change<SceneElementsMap> {
); );
// Need ordered nextElements to avoid z-index binding issues // Need ordered nextElements to avoid z-index binding issues
ElementsChange.redrawBoundArrows(nextElements, changedElements, { ElementsChange.redrawBoundArrows(nextElements, changedElements);
preservePoints: true,
});
} catch (e) { } catch (e) {
console.error( console.error(
`Couldn't mutate elements after applying elements change`, `Couldn't mutate elements after applying elements change`,
@ -1265,28 +1263,6 @@ export class ElementsChange implements Change<SceneElementsMap> {
delta.deleted.index !== delta.inserted.index; 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); return newElementWith(element, directlyApplicablePartial);
} }
@ -1516,16 +1492,74 @@ export class ElementsChange implements Change<SceneElementsMap> {
private static redrawBoundArrows( private static redrawBoundArrows(
elements: SceneElementsMap, elements: SceneElementsMap,
changed: Map<string, OrderedExcalidrawElement>, changed: Map<string, OrderedExcalidrawElement>,
options?: { preservePoints?: boolean },
) { ) {
// First, collect all arrow elements that need to be updated
const arrowsToUpdate = new Set<string>();
// Check for bindable elements that were changed
for (const element of changed.values()) { for (const element of changed.values()) {
if (!element.isDeleted && isBindableElement(element)) { 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, { updateBoundElements(element, elements, {
changedElements: changed, 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<ExcalidrawLinearElement>;
// 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( private static reorderElements(

View file

@ -739,14 +739,9 @@ export const updateBoundElements = (
simultaneouslyUpdated?: readonly ExcalidrawElement[]; simultaneouslyUpdated?: readonly ExcalidrawElement[];
newSize?: { width: number; height: number }; newSize?: { width: number; height: number };
changedElements?: Map<string, OrderedExcalidrawElement>; changedElements?: Map<string, OrderedExcalidrawElement>;
preservePoints?: boolean;
}, },
) => { ) => {
const { const { newSize, simultaneouslyUpdated } = options ?? {};
newSize,
simultaneouslyUpdated,
preservePoints = false,
} = options ?? {};
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds( const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
simultaneouslyUpdated, simultaneouslyUpdated,
); );
@ -799,20 +794,6 @@ export const updateBoundElements = (
return; 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( const updates = bindableElementsVisitor(
elementsMap, elementsMap,
element, element,

View file

@ -197,7 +197,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 0, "height": "102.35417",
"id": "id172", "id": "id172",
"index": "a2", "index": "a2",
"isDeleted": false, "isDeleted": false,
@ -211,8 +211,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0, 0,
], ],
[ [
100, "101.77517",
0, "102.35417",
], ],
], ],
"roughness": 1, "roughness": 1,
@ -226,9 +226,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 40, "version": 56,
"width": 100, "width": "101.77517",
"x": 0, "x": "0.70711",
"y": 0, "y": 0,
} }
`; `;
@ -297,15 +297,15 @@ History {
"focus": "0.00990", "focus": "0.00990",
"gap": 1, "gap": 1,
}, },
"height": 1, "height": "0.98586",
"points": [ "points": [
[ [
0, 0,
0, 0,
], ],
[ [
100, "98.58579",
-1, "-0.98586",
], ],
], ],
"startBinding": { "startBinding": {
@ -320,15 +320,15 @@ History {
"focus": "-0.02000", "focus": "-0.02000",
"gap": 1, "gap": 1,
}, },
"height": 0, "height": "0.00000",
"points": [ "points": [
[ [
0, 0,
0, 0,
], ],
[ [
100, "98.58579",
0, "0.00000",
], ],
], ],
"startBinding": { "startBinding": {
@ -389,15 +389,15 @@ History {
"focus": 0, "focus": 0,
"gap": 1, "gap": 1,
}, },
"height": 0, "height": "102.35417",
"points": [ "points": [
[ [
0, 0,
0, 0,
], ],
[ [
100, "101.77517",
0, "102.35417",
], ],
], ],
"startBinding": null, "startBinding": null,
@ -409,15 +409,15 @@ History {
"focus": "0.00990", "focus": "0.00990",
"gap": 1, "gap": 1,
}, },
"height": 1, "height": "0.98586",
"points": [ "points": [
[ [
0, 0,
0, 0,
], ],
[ [
100, "98.58579",
-1, "-0.98586",
], ],
], ],
"startBinding": { "startBinding": {
@ -425,7 +425,7 @@ History {
"focus": "0.02970", "focus": "0.02970",
"gap": 1, "gap": 1,
}, },
"y": 1, "y": "0.99364",
}, },
}, },
"id175" => Delta { "id175" => Delta {
@ -686,6 +686,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"selectedElementIds": { "selectedElementIds": {
"id167": true, "id167": true,
}, },
"selectedElementIds": {
"id167": true,
},
"selectedElementsAreBeingDragged": false, "selectedElementsAreBeingDragged": false,
"selectedGroupIds": {}, "selectedGroupIds": {},
"selectionElement": null, "selectionElement": null,
@ -817,7 +820,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 30, "version": 22,
"width": 100, "width": 100,
"x": 150, "x": 150,
"y": 0, "y": 0,
@ -852,7 +855,7 @@ History {
0, 0,
], ],
[ [
100, 0,
0, 0,
], ],
], ],
@ -937,7 +940,7 @@ History {
0, 0,
], ],
[ [
100, 0,
0, 0,
], ],
], ],
@ -1238,7 +1241,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 100, "height": "1.30038",
"id": "id178", "id": "id178",
"index": "Zz", "index": "Zz",
"isDeleted": false, "isDeleted": false,
@ -1252,8 +1255,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0, 0,
], ],
[ [
100, "98.58579",
100, "1.30038",
], ],
], ],
"roughness": 1, "roughness": 1,
@ -1275,9 +1278,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 11, "version": 15,
"width": 100, "width": "98.58579",
"x": 0, "x": "0.70711",
"y": 0, "y": 0,
} }
`; `;
@ -1609,7 +1612,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 100, "height": "1.30038",
"id": "id181", "id": "id181",
"index": "a0", "index": "a0",
"isDeleted": false, "isDeleted": false,
@ -1623,8 +1626,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0, 0,
], ],
[ [
100, "98.58579",
100, "1.30038",
], ],
], ],
"roughness": 1, "roughness": 1,
@ -1646,9 +1649,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 11, "version": 15,
"width": 100, "width": "98.58579",
"x": 0, "x": "0.70711",
"y": 0, "y": 0,
} }
`; `;
@ -1767,7 +1770,7 @@ History {
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 100, "height": "11.27227",
"index": "a0", "index": "a0",
"isDeleted": false, "isDeleted": false,
"lastCommittedPoint": null, "lastCommittedPoint": null,
@ -1780,8 +1783,8 @@ History {
0, 0,
], ],
[ [
100, "98.58579",
100, "11.27227",
], ],
], ],
"roughness": 1, "roughness": 1,
@ -1802,8 +1805,8 @@ History {
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"width": 100, "width": "98.58579",
"x": 0, "x": "0.70711",
"y": 0, "y": 0,
}, },
"inserted": { "inserted": {
@ -2320,7 +2323,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 0, "height": "374.05754",
"id": "id186", "id": "id186",
"index": "a2", "index": "a2",
"isDeleted": false, "isDeleted": false,
@ -2334,8 +2337,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0, 0,
], ],
[ [
100, "502.78936",
0, "-374.05754",
], ],
], ],
"roughness": 1, "roughness": 1,
@ -2353,10 +2356,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 10, "version": 12,
"width": 100, "width": "502.78936",
"x": 0, "x": "-0.83465",
"y": 0, "y": "-36.58211",
} }
`; `;
@ -10310,13 +10313,13 @@ exports[`history > multiplayer undo/redo > should override remotely added points
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 10, "height": 30,
"id": "id98", "id": "id98",
"index": "a0", "index": "a0",
"isDeleted": false, "isDeleted": false,
"lastCommittedPoint": [ "lastCommittedPoint": [
10, 30,
10, 30,
], ],
"link": null, "link": null,
"locked": false, "locked": false,
@ -10354,8 +10357,8 @@ exports[`history > multiplayer undo/redo > should override remotely added points
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 10, "version": 13,
"width": 10, "width": 30,
"x": 0, "x": 0,
"y": 0, "y": 0,
} }
@ -10369,89 +10372,7 @@ History {
[Function], [Function],
], ],
}, },
"redoStack": [ "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,
},
},
},
},
},
],
"undoStack": [ "undoStack": [
HistoryEntry { HistoryEntry {
"appStateChange": AppStateChange { "appStateChange": AppStateChange {
@ -10523,13 +10444,94 @@ History {
"updated": Map {}, "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 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`] = ` 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, 0,
], ],
[ [
100, "98.58579",
0, 0,
], ],
], ],
@ -15150,9 +15152,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 10, "version": 12,
"width": 100, "width": "98.58579",
"x": 0, "x": "0.70711",
"y": 0, "y": 0,
} }
`; `;
@ -15826,7 +15828,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
0, 0,
], ],
[ [
100, "98.58579",
0, 0,
], ],
], ],
@ -15845,9 +15847,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 10, "version": 12,
"width": 100, "width": "98.58579",
"x": 0, "x": "0.70711",
"y": 0, "y": 0,
} }
`; `;
@ -16445,7 +16447,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
0, 0,
], ],
[ [
100, "98.58579",
0, 0,
], ],
], ],
@ -16464,9 +16466,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 10, "version": 12,
"width": 100, "width": "98.58579",
"x": 0, "x": "0.70711",
"y": 0, "y": 0,
} }
`; `;
@ -17062,7 +17064,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
0, 0,
], ],
[ [
100, "98.58579",
0, 0,
], ],
], ],
@ -17081,9 +17083,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 10, "version": 12,
"width": 100, "width": "98.58579",
"x": 0, "x": "0.70711",
"y": 0, "y": 0,
} }
`; `;
@ -17775,7 +17777,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
0, 0,
], ],
[ [
100, "98.58579",
0, 0,
], ],
], ],
@ -17794,9 +17796,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 11, "version": 13,
"width": 100, "width": "98.58579",
"x": 0, "x": "0.70711",
"y": 0, "y": 0,
} }
`; `;
@ -19752,39 +19754,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": { "editingLinearElement": null,
"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",
},
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -19877,7 +19847,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 10, "height": 20,
"id": "id27", "id": "id27",
"index": "a0", "index": "a0",
"isDeleted": false, "isDeleted": false,
@ -19913,7 +19883,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 10, "version": 15,
"width": 20, "width": 20,
"x": 0, "x": 0,
"y": 0, "y": 0,
@ -19928,75 +19898,7 @@ History {
[Function], [Function],
], ],
}, },
"redoStack": [ "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,
],
],
},
},
},
},
},
],
"undoStack": [ "undoStack": [
HistoryEntry { HistoryEntry {
"appStateChange": AppStateChange { "appStateChange": AppStateChange {
@ -20156,10 +20058,77 @@ History {
"updated": Map {}, "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 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`;

View file

@ -14391,10 +14391,6 @@ History {
60, 60,
10, 10,
], ],
[
100,
20,
],
], ],
"roughness": 1, "roughness": 1,
"roundness": { "roundness": {

View file

@ -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(<Excalidraw handleKeyboardGlobally={true} />);
});
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);
});
});

View file

@ -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(<Excalidraw handleKeyboardGlobally={true} />);
});
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);
});
});