From db3e5c63efb2efb805bc86deb90e0418955e718b Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Thu, 8 May 2025 12:48:36 +0200 Subject: [PATCH] Fixing tests --- .../__snapshots__/contextmenu.test.tsx.snap | 963 ++++++------------ .../regressionTests.test.tsx.snap | 6 +- .../excalidraw/tests/contextmenu.test.tsx | 16 +- .../excalidraw/tests/regressionTests.test.tsx | 5 +- packages/utils/src/collision.ts | 49 +- 5 files changed, 375 insertions(+), 664 deletions(-) diff --git a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap index 6c258c621..32e5f6d07 100644 --- a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap @@ -1221,7 +1221,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "version": 3, "versionNonce": 1150084233, "width": 20, - "x": -10, + "x": -7, "y": 0, } `; @@ -1274,7 +1274,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "strokeWidth": 2, "type": "rectangle", "width": 20, - "x": -10, + "x": -7, "y": 0, }, "inserted": { @@ -2097,7 +2097,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "version": 3, "versionNonce": 1150084233, "width": 20, - "x": -10, + "x": -7, "y": 0, } `; @@ -2150,7 +2150,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "strokeWidth": 2, "type": "rectangle", "width": 20, - "x": -10, + "x": -7, "y": 0, }, "inserted": { @@ -2307,7 +2307,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "version": 4, "versionNonce": 1014066025, "width": 20, - "x": -10, + "x": -7, "y": 0, } `; @@ -2360,7 +2360,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "strokeWidth": 2, "type": "rectangle", "width": 20, - "x": -10, + "x": -7, "y": 0, }, "inserted": { @@ -2548,7 +2548,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "version": 3, "versionNonce": 1150084233, "width": 20, - "x": -10, + "x": -7, "y": 0, } `; @@ -2582,7 +2582,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "version": 5, "versionNonce": 400692809, "width": 20, - "x": 0, + "x": 3, "y": 10, } `; @@ -2635,7 +2635,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "strokeWidth": 2, "type": "rectangle", "width": 20, - "x": -10, + "x": -7, "y": 0, }, "inserted": { @@ -2689,7 +2689,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "strokeWidth": 2, "type": "rectangle", "width": 20, - "x": 0, + "x": 3, "y": 10, }, "inserted": { @@ -7769,82 +7769,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap "collaborators": Map {}, "contextMenu": { "items": [ - "separator", - { - "icon": , - "keyTest": [Function], - "label": "labels.cut", - "name": "cut", - "perform": [Function], - "trackEvent": { - "category": "element", - }, - }, - { - "icon": , - "keyTest": undefined, - "label": "labels.copy", - "name": "copy", - "perform": [Function], - "trackEvent": { - "category": "element", - }, - }, { "keyTest": undefined, "label": "labels.paste", @@ -7855,78 +7779,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap }, }, "separator", - { - "label": "labels.selectAllElementsInFrame", - "name": "selectAllElementsInFrame", - "perform": [Function], - "predicate": [Function], - "trackEvent": { - "category": "canvas", - }, - }, - { - "label": "labels.removeAllElementsFromFrame", - "name": "removeAllElementsFromFrame", - "perform": [Function], - "predicate": [Function], - "trackEvent": { - "category": "history", - }, - }, - { - "label": "labels.wrapSelectionInFrame", - "name": "wrapSelectionInFrame", - "perform": [Function], - "predicate": [Function], - "trackEvent": { - "category": "element", - }, - }, - "separator", - { - "PanelComponent": [Function], - "icon": , - "keywords": [ - "image", - "crop", - ], - "label": "helpDialog.cropStart", - "name": "cropEditor", - "perform": [Function], - "predicate": [Function], - "trackEvent": { - "category": "menu", - }, - "viewMode": true, - }, - "separator", { "icon": , "keyTest": [Function], - "label": "labels.copyStyles", - "name": "copyStyles", + "label": "labels.selectAll", + "name": "selectAll", "perform": [Function], "trackEvent": { - "category": "element", + "category": "canvas", }, + "viewMode": false, }, { - "icon": , - "keyTest": [Function], - "label": "labels.pasteStyles", - "name": "pasteStyles", - "perform": [Function], - "trackEvent": { - "category": "element", - }, - }, - "separator", - { - "PanelComponent": [Function], - "icon": [Function], - "keyTest": [Function], - "label": "labels.group", - "name": "group", - "perform": [Function], - "predicate": [Function], - "trackEvent": { - "category": "element", - }, - }, - { - "icon": null, - "label": "labels.autoResize", - "name": "autoResize", - "perform": [Function], - "predicate": [Function], - "trackEvent": { - "category": "element", - }, - }, - { - "label": "labels.unbindText", - "name": "unbindText", - "perform": [Function], - "predicate": [Function], - "trackEvent": { - "category": "element", - }, - }, - { - "label": "labels.bindText", - "name": "bindText", - "perform": [Function], - "predicate": [Function], - "trackEvent": { - "category": "element", - }, - }, - { - "label": "labels.createContainerFromText", - "name": "wrapTextInContainer", - "perform": [Function], - "predicate": [Function], - "trackEvent": { - "category": "element", - }, - }, - { - "PanelComponent": [Function], - "icon": [Function], - "keyTest": [Function], - "label": "labels.ungroup", - "name": "ungroup", - "perform": [Function], - "predicate": [Function], - "trackEvent": { - "category": "element", - }, - }, - "separator", - { - "label": "labels.addToLibrary", - "name": "addToLibrary", - "perform": [Function], - "trackEvent": { - "category": "element", - }, - }, - "separator", - { - "PanelComponent": [Function], - "icon": , - "keyPriority": 40, - "keyTest": [Function], - "keywords": [ - "move down", - "zindex", - "layer", - ], - "label": "labels.sendBackward", - "name": "sendBackward", - "perform": [Function], - "trackEvent": { - "category": "element", - }, - }, - { - "PanelComponent": [Function], - "icon": , - "keyPriority": 40, - "keyTest": [Function], - "keywords": [ - "move up", - "zindex", - "layer", - ], - "label": "labels.bringForward", - "name": "bringForward", - "perform": [Function], - "trackEvent": { - "category": "element", - }, - }, - { - "PanelComponent": [Function], - "icon": , - "keyTest": [Function], - "keywords": [ - "move down", - "zindex", - "layer", - ], - "label": "labels.sendToBack", - "name": "sendToBack", - "perform": [Function], - "trackEvent": { - "category": "element", - }, - }, - { - "PanelComponent": [Function], - "icon": , - "keyTest": [Function], - "keywords": [ - "move up", - "zindex", - "layer", - ], - "label": "labels.bringToFront", - "name": "bringToFront", - "perform": [Function], - "trackEvent": { - "category": "element", - }, - }, - "separator", - { - "icon": , - "keyTest": [Function], - "label": "labels.flipHorizontal", - "name": "flipHorizontal", - "perform": [Function], - "trackEvent": { - "category": "element", - }, - }, - { - "icon": , - "keyTest": [Function], - "label": "labels.flipVertical", - "name": "flipVertical", - "perform": [Function], - "trackEvent": { - "category": "element", - }, - }, - "separator", - { - "PanelComponent": [Function], - "category": "Elements", - "keywords": [ - "line", - ], - "label": [Function], - "name": "toggleLinearEditor", - "perform": [Function], - "predicate": [Function], - "trackEvent": { - "category": "element", - }, - }, - "separator", - { - "PanelComponent": [Function], "icon": , - "keyTest": [Function], - "label": [Function], - "name": "hyperlink", + "label": "labels.elementLock.unlockAll", + "name": "unlockAllElements", "perform": [Function], "predicate": [Function], "trackEvent": { - "action": "click", - "category": "hyperlink", + "category": "canvas", }, + "viewMode": false, }, + "separator", { + "checked": [Function], "icon": , - "label": "labels.copyElementLink", - "name": "copyElementLink", - "perform": [Function], - "predicate": [Function], - "trackEvent": { - "category": "element", - }, - }, - "separator", - { - "PanelComponent": [Function], - "icon": , "keyTest": [Function], - "label": "labels.duplicateSelection", - "name": "duplicateSelection", - "perform": [Function], - "trackEvent": { - "category": "element", - }, - }, - { - "icon": [Function], - "keyTest": [Function], - "label": [Function], - "name": "toggleElementLock", + "keywords": [ + "snap", + ], + "label": "labels.toggleGrid", + "name": "gridMode", "perform": [Function], "predicate": [Function], "trackEvent": { - "category": "element", + "category": "canvas", + "predicate": [Function], }, + "viewMode": true, }, - "separator", { - "PanelComponent": [Function], + "checked": [Function], "icon": , "keyTest": [Function], - "label": "labels.delete", - "name": "deleteSelectedElements", + "label": "buttons.objectsSnapMode", + "name": "objectsSnapMode", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "canvas", + "predicate": [Function], + }, + "viewMode": false, + }, + { + "checked": [Function], + "icon": , + "keyTest": [Function], + "label": "buttons.zenMode", + "name": "zenMode", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "canvas", + "predicate": [Function], + }, + "viewMode": true, + }, + { + "checked": [Function], + "icon": , + "keyTest": [Function], + "label": "labels.viewMode", + "name": "viewMode", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "canvas", + "predicate": [Function], + }, + "viewMode": true, + }, + { + "checked": [Function], + "icon": , + "keyTest": [Function], + "keywords": [ + "edit", + "attributes", + "customize", + ], + "label": "stats.fullTitle", + "name": "stats", "perform": [Function], "trackEvent": { - "action": "delete", - "category": "element", + "category": "menu", }, + "viewMode": true, }, ], "left": -17, diff --git a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap index 67d2c5de2..1de6f5db2 100644 --- a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap @@ -2465,7 +2465,7 @@ exports[`regression tests > can drag element that covers another element, while "scrolledOutside": false, "searchMatches": null, "selectedElementIds": { - "id3": true, + "id0": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -2667,7 +2667,7 @@ exports[`regression tests > can drag element that covers another element, while "delta": Delta { "deleted": { "selectedElementIds": { - "id3": true, + "id0": true, }, }, "inserted": { @@ -2681,7 +2681,7 @@ exports[`regression tests > can drag element that covers another element, while "added": {}, "removed": {}, "updated": { - "id3": { + "id0": { "deleted": { "x": 300, "y": 300, diff --git a/packages/excalidraw/tests/contextmenu.test.tsx b/packages/excalidraw/tests/contextmenu.test.tsx index 75de2717f..21f7a6a79 100644 --- a/packages/excalidraw/tests/contextmenu.test.tsx +++ b/packages/excalidraw/tests/contextmenu.test.tsx @@ -304,12 +304,12 @@ describe("contextMenu element", () => { it("selecting 'Copy styles' in context menu copies styles", () => { UI.clickTool("rectangle"); - mouse.down(10, 10); + mouse.down(13, 10); mouse.up(20, 20); fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, - clientX: 3, + clientX: 13, clientY: 3, }); const contextMenu = UI.queryContextMenu(); @@ -389,12 +389,12 @@ describe("contextMenu element", () => { it("selecting 'Delete' in context menu deletes element", () => { UI.clickTool("rectangle"); - mouse.down(10, 10); + mouse.down(13, 10); mouse.up(20, 20); fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, - clientX: 3, + clientX: 13, clientY: 3, }); const contextMenu = UI.queryContextMenu(); @@ -405,12 +405,12 @@ describe("contextMenu element", () => { it("selecting 'Add to library' in context menu adds element to library", async () => { UI.clickTool("rectangle"); - mouse.down(10, 10); + mouse.down(13, 10); mouse.up(20, 20); fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, - clientX: 3, + clientX: 13, clientY: 3, }); const contextMenu = UI.queryContextMenu(); @@ -424,12 +424,12 @@ describe("contextMenu element", () => { it("selecting 'Duplicate' in context menu duplicates element", () => { UI.clickTool("rectangle"); - mouse.down(10, 10); + mouse.down(13, 10); mouse.up(20, 20); fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, - clientX: 3, + clientX: 13, clientY: 3, }); const contextMenu = UI.queryContextMenu(); diff --git a/packages/excalidraw/tests/regressionTests.test.tsx b/packages/excalidraw/tests/regressionTests.test.tsx index 9deefb8c7..f11ea01e9 100644 --- a/packages/excalidraw/tests/regressionTests.test.tsx +++ b/packages/excalidraw/tests/regressionTests.test.tsx @@ -704,7 +704,7 @@ describe("regression tests", () => { // pointer down on rectangle mouse.reset(); - mouse.down(100, 100); + mouse.down(110, 100); // Rectangle is rounded, there is no selection at the corner mouse.up(200, 200); expect(API.getSelectedElement().type).toBe("rectangle"); @@ -989,6 +989,7 @@ describe("regression tests", () => { // select rectangle mouse.reset(); + mouse.moveTo(30, 0); // Rectangle is rounded, there is no selection at the corner mouse.click(); // click on intersection between ellipse and rectangle @@ -1155,6 +1156,7 @@ it( // Select first rectangle while keeping third one selected. // Third rectangle is selected because it was the last element to be created. mouse.reset(); + mouse.moveTo(30, 0); Keyboard.withModifierKeys({ shift: true }, () => { mouse.click(); }); @@ -1176,6 +1178,7 @@ it( // Pointer down o first rectangle that is part of the group mouse.reset(); + mouse.moveTo(30, 0); Keyboard.withModifierKeys({ shift: true }, () => { mouse.down(); }); diff --git a/packages/utils/src/collision.ts b/packages/utils/src/collision.ts index 6f49d4b26..ced2b54a0 100644 --- a/packages/utils/src/collision.ts +++ b/packages/utils/src/collision.ts @@ -6,6 +6,8 @@ import { type GlobalPoint, type LocalPoint, type Polygon, + vectorCross, + vectorFromPoint, } from "@excalidraw/math"; import { intersectElementWithLineSegment } from "@excalidraw/element/collision"; @@ -40,10 +42,19 @@ export const isPointInShape = ( point: GlobalPoint, element: ExcalidrawElement, ) => { - if ( - (isLinearElement(element) || isFreeDrawElement(element)) && - !isPathALoop(element.points) - ) { + if (isLinearElement(element) || isFreeDrawElement(element)) { + if (isPathALoop(element.points)) { + // for a closed path, we need to check if the point is inside the path + const r = isPointInClosedPath( + element.points.map((p) => + pointFrom(element.x + p[0], element.y + p[1]), + ), + point, + ); + //console.log(r); + return r; + } + // There isn't any "inside" for a non-looping path return false; } @@ -56,6 +67,36 @@ export const isPointInShape = ( return intersections.length === 0; }; +/** + * Determine if a closed path contains a point. + * + * Implementation notes: We'll use the fact that the path is a consecutive + * sequence of line segments, these line segments have a winding order and + * the fact that if a point is inside the closed path, the cross product of the + * start point of a line segment to the point p and the end point of the line + * segment will be negative for all segments. + * + * @param points + * @param p + */ +const isPointInClosedPath = ( + points: readonly GlobalPoint[], + p: GlobalPoint, +) => { + const segments = points.slice(1).map((point, i) => { + return lineSegment(points[i], point); + }); + + return segments.every((segment) => { + const c = vectorCross( + vectorFromPoint(segment[0], p), + vectorFromPoint(segment[0], segment[1]), + ); + + return c < 0; + }); +}; + // check if the given element is in the given bounds export const isPointInBounds = ( point: Point,