excalidraw/src/tests/linearElementEditor.test.tsx
Barnabás Molnár 6334bd832f
feat: editor redesign 🔥 (#5780)
* Placed eraser into shape switcher (top toolbar).
Redesigned top toolbar.

* Redesigned zoom and undo-redo buttons.

* Started redesigning left toolbar.

* Redesigned help dialog.

* Colour picker now somewhat in line with new design

* [WIP] Changed a bunch of icons.
TODO: organise new icons.

* [WIP] Organised a bunch of icons. Still some to do

* [WIP] Started working on hamburger menu.

* Fixed some bugs with hamburger menu.

* Menu and left toolbar positioning.

* Added some more items to hamburger menu.

* Changed some icons.

* Modal/dialog styling & bunch of fixes.

* Some more dialog improvements & fixes.

* Mobile menu changes.

* Menu can now be closed with outside click.

* Collab avatars and button changes.

* Icon sizing. Left toolbar positioning.

* Implemented welcome screen rendering logic.

* [WIP] Welcome screen content + design.

* Some more welcome screen content and design.

* Merge fixes.

* Tweaked icon set.

* Welcome screen darkmode fix.

* Content updates.

* Various small fixes & adjustments.
Moved language selection into menu.
Fixed some problematic icons.
Slightly moved encryption icon.

* Sidebar header redesign.

* Libraries content rendering logic + some styling.

* Somem more library sidebar styling.

* Publish library dialog styling.

* scroll-back-to-content btn styling

* ColorPicker positioning.

* Library button styling.

* ColorPicker positioning "fix".

* Misc adjustments.

* PenMode button changes.

* Trying to make mobile somewhat usable.

* Added a couple of icons.

* Added some shortcuts.

* Prevent welcome screen flickering.
Fix issue with welcome screen interactivity.
Don't show sidebar button when docked.

* Icon sizing on smaller screens.

* Sidebar styling changes.

* Alignment button... well... alignments.

* Fix inconsistent padding in left toolbar.

* HintViewer changes.

* Hamburger menu changes.

* Move encryption badge back to its original pos.

* Arrowhead changes.
Active state, colours + stronger shadow.

* Added new custom font.

* Fixed bug with library button not rendering.

* Fixed issue with lang selection colours.

* Add tooltips for undo, redo.

* Address some dark mode contrast issues.

* (Re)introduce counter for selectedItems in sidebar

* [WIP] Tweaked bounding box colour & padding.

* Dashed bounding box for remote clients.

* Some more bounding box tweaks.

* Removed docking animation for now...

* Address some RTL issues.

* Welcome screen responsiveness.

* use lighter selection color in dark mode & align naming

* use rounded corners for transform handles

* use lighter gray for welcomeScreen text in dark mode

* disable selection on dialog buttons

* change selection button icon

* fix library item width being flexible

* library: visually align spinner with first section heading

* lint

* fix scrollbar color in dark mode & make thinner

* adapt properties panel max-height

* add shrotcut label to save-to-current-file

* fix unrelated `useOutsideClick` firing for active modal

* add promo color to e+ menu item

* fix type

* lowered button size

* fix transform handles raidus not accounting for zoom

* attempt fix for excal logo on safari

* final fix for excal logo on safari

* fixing fhd resolution button sized

* remove TODO shortcut

* Collab related styling changes.
Expanding avatar list no longer offsets top toolbar.
Added active state & collaborator count badge for collab button.

* Tweaked collab button active colours.

* Added active style to collab btn in hamburger menu

* Remove unnecessary comment.

* Added back promo link for non (signed in) E+ users

* Go to E+ button added for signed in E+ users.

* Close menu & dropdown on modal close.

* tweak icons & fix rendering on smaller sizes [part one]

* align welcomeScreen icons with other UI

* switch icon resize mq to `device-width`

* disable welcomeScreen items `:hover` when selecting on canvas

* change selection box color and style

* reduce selection padding and fix group selection styling

* improve collab cursor styling

- make name borders round
- hide status when "active"
- remove black/gray colors

* add Twitter to hamburger menu

* align collab button

* add shortcut for image export dialog

* revert yarn.lock

* fix more tabler icons

* slightly better-looking penMode button

* change penMode button & tooltip

* revert hamburger menu icon

* align padding on lang picker & canvas bg

* updated robot txt to allow twitter bot and fb bot

* added new OG and tweaked the OG state

* add tooltip to collab button

* align style for scroll-to-content button

* fix pointer-events around toolbar

* fix decor arrow positioning and RTL

* fix welcomeScreen-item active state in dark mode

* change `load` button copy

* prevent shadow anim when opening a docked sidebar

* update E+ links ga params

* show redirect-to-eplus welcomeScreen subheading for signed-in users

* make more generic

* add ga for eplus redirect button

* change copy and icons for hamburger export buttons

* update snaps

* trim the username to account for trailing spaces

* tweaks around decor breakpoints

* fix linear element editor test

* remove .env change

* remove `it.only`

Co-authored-by: dwelle <luzar.david@gmail.com>
Co-authored-by: Maielo <maielo.mv@gmail.com>
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2022-11-01 17:29:58 +01:00

609 lines
18 KiB
TypeScript

import ReactDOM from "react-dom";
import { ExcalidrawLinearElement } from "../element/types";
import ExcalidrawApp from "../excalidraw-app";
import { centerPoint } from "../math";
import { reseed } from "../random";
import * as Renderer from "../renderer/renderScene";
import { Keyboard, Pointer } from "./helpers/ui";
import { screen, render, fireEvent, GlobalTestState } from "./test-utils";
import { API } from "../tests/helpers/api";
import { Point } from "../types";
import { KEYS } from "../keys";
import { LinearElementEditor } from "../element/linearElementEditor";
import { queryByText } from "@testing-library/react";
const renderScene = jest.spyOn(Renderer, "renderScene");
const { h } = window;
describe(" Test Linear Elements", () => {
let container: HTMLElement;
let canvas: HTMLCanvasElement;
beforeEach(async () => {
// Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
localStorage.clear();
renderScene.mockClear();
reseed(7);
const comp = await render(<ExcalidrawApp />);
container = comp.container;
canvas = container.querySelector("canvas")!;
canvas.width = 1000;
canvas.height = 1000;
});
const p1: Point = [20, 20];
const p2: Point = [60, 20];
const midpoint = centerPoint(p1, p2);
const delta = 50;
const mouse = new Pointer("mouse");
const createTwoPointerLinearElement = (
type: ExcalidrawLinearElement["type"],
strokeSharpness: ExcalidrawLinearElement["strokeSharpness"] = "sharp",
roughness: ExcalidrawLinearElement["roughness"] = 0,
) => {
h.elements = [
API.createElement({
x: p1[0],
y: p1[1],
width: p2[0] - p1[0],
height: 0,
type,
roughness,
points: [
[0, 0],
[p2[0] - p1[0], p2[1] - p1[1]],
],
strokeSharpness,
}),
];
mouse.clickAt(p1[0], p1[1]);
};
const createThreePointerLinearElement = (
type: ExcalidrawLinearElement["type"],
strokeSharpness: ExcalidrawLinearElement["strokeSharpness"] = "sharp",
roughness: ExcalidrawLinearElement["roughness"] = 0,
) => {
//dragging line from midpoint
const p3 = [midpoint[0] + delta - p1[0], midpoint[1] + delta - p1[1]];
h.elements = [
API.createElement({
x: p1[0],
y: p1[1],
width: p3[0] - p1[0],
height: 0,
type,
roughness,
points: [
[0, 0],
[p3[0], p3[1]],
[p2[0] - p1[0], p2[1] - p1[1]],
],
strokeSharpness,
}),
];
mouse.clickAt(p1[0], p1[1]);
};
const enterLineEditingMode = (line: ExcalidrawLinearElement) => {
mouse.clickAt(p1[0], p1[1]);
Keyboard.keyPress(KEYS.ENTER);
expect(h.state.editingLinearElement?.elementId).toEqual(line.id);
};
const drag = (startPoint: Point, endPoint: Point) => {
fireEvent.pointerDown(canvas, {
clientX: startPoint[0],
clientY: startPoint[1],
});
fireEvent.pointerMove(canvas, {
clientX: endPoint[0],
clientY: endPoint[1],
});
fireEvent.pointerUp(canvas, {
clientX: endPoint[0],
clientY: endPoint[1],
});
};
const deletePoint = (point: Point) => {
fireEvent.pointerDown(canvas, {
clientX: point[0],
clientY: point[1],
});
fireEvent.pointerUp(canvas, {
clientX: point[0],
clientY: point[1],
});
Keyboard.keyPress(KEYS.DELETE);
};
it("should allow dragging line from midpoint in 2 pointer lines outside editor", async () => {
createTwoPointerLinearElement("line");
const line = h.elements[0] as ExcalidrawLinearElement;
expect(renderScene).toHaveBeenCalledTimes(7);
expect((h.elements[0] as ExcalidrawLinearElement).points.length).toEqual(2);
// drag line from midpoint
drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
expect(renderScene).toHaveBeenCalledTimes(10);
expect(line.points.length).toEqual(3);
expect(line.points).toMatchInlineSnapshot(`
Array [
Array [
0,
0,
],
Array [
70,
50,
],
Array [
40,
0,
],
]
`);
});
it("should allow entering and exiting line editor via context menu", () => {
createTwoPointerLinearElement("line");
fireEvent.contextMenu(GlobalTestState.canvas, {
button: 2,
clientX: midpoint[0],
clientY: midpoint[1],
});
// Enter line editor
let contextMenu = document.querySelector(".context-menu");
fireEvent.contextMenu(GlobalTestState.canvas, {
button: 2,
clientX: midpoint[0],
clientY: midpoint[1],
});
fireEvent.click(queryByText(contextMenu as HTMLElement, "Edit line")!);
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id);
// Exiting line editor
fireEvent.contextMenu(GlobalTestState.canvas, {
button: 2,
clientX: midpoint[0],
clientY: midpoint[1],
});
contextMenu = document.querySelector(".context-menu");
fireEvent.contextMenu(GlobalTestState.canvas, {
button: 2,
clientX: midpoint[0],
clientY: midpoint[1],
});
fireEvent.click(
queryByText(contextMenu as HTMLElement, "Exit line editor")!,
);
expect(h.state.editingLinearElement?.elementId).toBeUndefined();
});
describe("Inside editor", () => {
it("should allow dragging line from midpoint in 2 pointer lines", async () => {
createTwoPointerLinearElement("line");
const line = h.elements[0] as ExcalidrawLinearElement;
enterLineEditingMode(line);
// drag line from midpoint
drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
expect(renderScene).toHaveBeenCalledTimes(14);
expect(line.points.length).toEqual(3);
expect(line.points).toMatchInlineSnapshot(`
Array [
Array [
0,
0,
],
Array [
70,
50,
],
Array [
40,
0,
],
]
`);
});
it("should update the midpoints when element sharpness changed", async () => {
createThreePointerLinearElement("line");
const line = h.elements[0] as ExcalidrawLinearElement;
expect(line.points.length).toEqual(3);
enterLineEditingMode(line);
const midPointsWithSharpEdge = LinearElementEditor.getEditorMidPoints(
line,
h.state,
);
// update sharpness
fireEvent.click(screen.getByTitle("Round"));
expect(renderScene).toHaveBeenCalledTimes(12);
const midPointsWithRoundEdge = LinearElementEditor.getEditorMidPoints(
h.elements[0] as ExcalidrawLinearElement,
h.state,
);
expect(midPointsWithRoundEdge[0]).not.toEqual(midPointsWithSharpEdge[0]);
expect(midPointsWithRoundEdge[1]).not.toEqual(midPointsWithSharpEdge[1]);
expect(midPointsWithRoundEdge).toMatchInlineSnapshot(`
Array [
Array [
55.9697848965255,
47.442326230998205,
],
Array [
76.08587175006699,
43.294165939653226,
],
]
`);
});
it("should update all the midpoints when element position changed", async () => {
createThreePointerLinearElement("line", "round");
const line = h.elements[0] as ExcalidrawLinearElement;
expect(line.points.length).toEqual(3);
enterLineEditingMode(line);
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
expect([line.x, line.y]).toEqual(points[0]);
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
const startPoint = centerPoint(points[0], midPoints[0] as Point);
const deltaX = 50;
const deltaY = 20;
const endPoint: Point = [startPoint[0] + deltaX, startPoint[1] + deltaY];
// Move the element
drag(startPoint, endPoint);
expect(renderScene).toHaveBeenCalledTimes(15);
expect([line.x, line.y]).toEqual([
points[0][0] + deltaX,
points[0][1] + deltaY,
]);
const newMidPoints = LinearElementEditor.getEditorMidPoints(
line,
h.state,
);
expect(midPoints[0]).not.toEqual(newMidPoints[0]);
expect(midPoints[1]).not.toEqual(newMidPoints[1]);
expect(newMidPoints).toMatchInlineSnapshot(`
Array [
Array [
105.96978489652551,
67.4423262309982,
],
Array [
126.08587175006699,
63.294165939653226,
],
]
`);
});
describe("When edges are sharp", () => {
// This is the expected midpoint for line with sharp edge
// hence hardcoding it so if later some bug is introduced
// this will fail and we can fix it
const firstSegmentMidpoint: Point = [55, 45];
const lastSegmentMidpoint: Point = [75, 40];
let line: ExcalidrawLinearElement;
beforeEach(() => {
createThreePointerLinearElement("line");
line = h.elements[0] as ExcalidrawLinearElement;
expect(line.points.length).toEqual(3);
enterLineEditingMode(line);
});
it("should allow dragging lines from midpoints in between segments", async () => {
// drag line via first segment midpoint
drag(firstSegmentMidpoint, [
firstSegmentMidpoint[0] + delta,
firstSegmentMidpoint[1] + delta,
]);
expect(line.points.length).toEqual(4);
// drag line from last segment midpoint
drag(lastSegmentMidpoint, [
lastSegmentMidpoint[0] + delta,
lastSegmentMidpoint[1] + delta,
]);
expect(renderScene).toHaveBeenCalledTimes(19);
expect(line.points.length).toEqual(5);
expect((h.elements[0] as ExcalidrawLinearElement).points)
.toMatchInlineSnapshot(`
Array [
Array [
0,
0,
],
Array [
85,
75,
],
Array [
70,
50,
],
Array [
105,
75,
],
Array [
40,
0,
],
]
`);
});
it("should update only the first segment midpoint when its point is dragged", async () => {
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
const hitCoords: Point = [points[0][0], points[0][1]];
// Drag from first point
drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]);
expect(renderScene).toHaveBeenCalledTimes(15);
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
points[0][0] - delta,
points[0][1] - delta,
]);
const newMidPoints = LinearElementEditor.getEditorMidPoints(
line,
h.state,
);
expect(midPoints[0]).not.toEqual(newMidPoints[0]);
expect(midPoints[1]).toEqual(newMidPoints[1]);
});
it("should hide midpoints in the segment when points moved close", async () => {
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
const hitCoords: Point = [points[0][0], points[0][1]];
// Drag from first point
drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]);
expect(renderScene).toHaveBeenCalledTimes(15);
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
points[0][0] + delta,
points[0][1] + delta,
]);
const newMidPoints = LinearElementEditor.getEditorMidPoints(
line,
h.state,
);
// This midpoint is hidden since the points are too close
expect(newMidPoints[0]).toBeNull();
expect(midPoints[1]).toEqual(newMidPoints[1]);
});
it("should remove the midpoint when one of the points in the segment is deleted", async () => {
const line = h.elements[0] as ExcalidrawLinearElement;
enterLineEditingMode(line);
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
// dragging line from last segment midpoint
drag(lastSegmentMidpoint, [
lastSegmentMidpoint[0] + 50,
lastSegmentMidpoint[1] + 50,
]);
expect(line.points.length).toEqual(4);
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
// delete 3rd point
deletePoint(points[2]);
expect(line.points.length).toEqual(3);
expect(renderScene).toHaveBeenCalledTimes(20);
const newMidPoints = LinearElementEditor.getEditorMidPoints(
line,
h.state,
);
expect(newMidPoints.length).toEqual(2);
expect(midPoints[0]).toEqual(newMidPoints[0]);
expect(midPoints[1]).toEqual(newMidPoints[1]);
});
});
describe("When edges are round", () => {
// This is the expected midpoint for line with round edge
// hence hardcoding it so if later some bug is introduced
// this will fail and we can fix it
const firstSegmentMidpoint: Point = [
55.9697848965255, 47.442326230998205,
];
const lastSegmentMidpoint: Point = [
76.08587175006699, 43.294165939653226,
];
let line: ExcalidrawLinearElement;
beforeEach(() => {
createThreePointerLinearElement("line", "round");
line = h.elements[0] as ExcalidrawLinearElement;
expect(line.points.length).toEqual(3);
enterLineEditingMode(line);
});
it("should allow dragging lines from midpoints in between segments", async () => {
// drag line from first segment midpoint
drag(firstSegmentMidpoint, [
firstSegmentMidpoint[0] + delta,
firstSegmentMidpoint[1] + delta,
]);
expect(line.points.length).toEqual(4);
// drag line from last segment midpoint
drag(lastSegmentMidpoint, [
lastSegmentMidpoint[0] + delta,
lastSegmentMidpoint[1] + delta,
]);
expect(renderScene).toHaveBeenCalledTimes(19);
expect(line.points.length).toEqual(5);
expect((h.elements[0] as ExcalidrawLinearElement).points)
.toMatchInlineSnapshot(`
Array [
Array [
0,
0,
],
Array [
85.96978489652551,
77.4423262309982,
],
Array [
70,
50,
],
Array [
104.58050066266131,
74.24758482724201,
],
Array [
40,
0,
],
]
`);
});
it("should update all the midpoints when its point is dragged", async () => {
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
const hitCoords: Point = [points[0][0], points[0][1]];
// Drag from first point
drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]);
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
points[0][0] - delta,
points[0][1] - delta,
]);
const newMidPoints = LinearElementEditor.getEditorMidPoints(
line,
h.state,
);
expect(midPoints[0]).not.toEqual(newMidPoints[0]);
expect(midPoints[1]).not.toEqual(newMidPoints[1]);
expect(newMidPoints).toMatchInlineSnapshot(`
Array [
Array [
31.884084517616053,
23.13275505472383,
],
Array [
77.74792546875662,
44.57840982272327,
],
]
`);
});
it("should hide midpoints in the segment when points moved close", async () => {
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
const hitCoords: Point = [points[0][0], points[0][1]];
// Drag from first point
drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]);
expect(renderScene).toHaveBeenCalledTimes(15);
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
points[0][0] + delta,
points[0][1] + delta,
]);
const newMidPoints = LinearElementEditor.getEditorMidPoints(
line,
h.state,
);
// This mid point is hidden due to point being too close
expect(newMidPoints[0]).toBeNull();
expect(newMidPoints[1]).not.toEqual(midPoints[1]);
});
it("should update all the midpoints when a point is deleted", async () => {
drag(lastSegmentMidpoint, [
lastSegmentMidpoint[0] + delta,
lastSegmentMidpoint[1] + delta,
]);
expect(line.points.length).toEqual(4);
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
// delete 3rd point
deletePoint(points[2]);
expect(line.points.length).toEqual(3);
const newMidPoints = LinearElementEditor.getEditorMidPoints(
line,
h.state,
);
expect(newMidPoints.length).toEqual(2);
expect(midPoints[0]).not.toEqual(newMidPoints[0]);
expect(midPoints[1]).not.toEqual(newMidPoints[1]);
expect(newMidPoints).toMatchInlineSnapshot(`
Array [
Array [
55.9697848965255,
47.442326230998205,
],
Array [
76.08587175006699,
43.294165939653226,
],
]
`);
});
});
});
});