excalidraw/src/tests/clipboard.test.tsx
Aakansha Doshi 83383977f5
feat: add line height attribute to text element (#6360)
* feat: add line height attribute to text element

* lint

* update line height when redrawing text bounding box

* fix tests

* retain line height when pasting styles

* fix test

* create a util for calculating ling height using old algo

* update line height when resizing multiple text elements

* make line height backward compatible

* udpate line height for older element when font size updated

* remove logs

* Add specs

* lint

* review fixes

* simplify by changing `lineHeight` from px to unitless

* make param non-optional

* update comment

* fix: jumping text due to font size being calculated incorrectly

* update line height when font family is updated

* lint

* Add spec

* more specs

* rename to getDefaultLineHeight

* fix getting lineHeight for potentially undefined fontFamily

* reduce duplication

* fix fallback

* refactor and comment tweaks

* fix

---------

Co-authored-by: dwelle <luzar.david@gmail.com>
2023-03-22 11:32:38 +05:30

253 lines
7.0 KiB
TypeScript

import ReactDOM from "react-dom";
import { render, waitFor, GlobalTestState } from "./test-utils";
import { Pointer, Keyboard } from "./helpers/ui";
import ExcalidrawApp from "../excalidraw-app";
import { KEYS } from "../keys";
import {
getDefaultLineHeight,
getLineHeightInPx,
} from "../element/textElement";
import { getElementBounds } from "../element";
import { NormalizedZoomValue } from "../types";
const { h } = window;
const mouse = new Pointer("mouse");
jest.mock("../keys.ts", () => {
const actual = jest.requireActual("../keys.ts");
return {
__esmodule: true,
...actual,
isDarwin: false,
KEYS: {
...actual.KEYS,
CTRL_OR_CMD: "ctrlKey",
},
};
});
const setClipboardText = (text: string) => {
Object.assign(navigator, {
clipboard: {
readText: () => text,
},
});
};
const sendPasteEvent = () => {
const clipboardEvent = new Event("paste", {
bubbles: true,
cancelable: true,
composed: true,
});
// set `clipboardData` properties.
// @ts-ignore
clipboardEvent.clipboardData = {
getData: () => window.navigator.clipboard.readText(),
files: [],
};
document.dispatchEvent(clipboardEvent);
};
const pasteWithCtrlCmdShiftV = () => {
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
//triggering keydown with an empty clipboard
Keyboard.keyPress(KEYS.V);
//triggering paste event with faked clipboard
sendPasteEvent();
});
};
const pasteWithCtrlCmdV = () => {
Keyboard.withModifierKeys({ ctrl: true }, () => {
//triggering keydown with an empty clipboard
Keyboard.keyPress(KEYS.V);
//triggering paste event with faked clipboard
sendPasteEvent();
});
};
const sleep = (ms: number) => {
return new Promise((resolve) => setTimeout(() => resolve(null), ms));
};
beforeEach(async () => {
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
localStorage.clear();
mouse.reset();
await render(<ExcalidrawApp />);
h.app.setAppState({ zoom: { value: 1 as NormalizedZoomValue } });
setClipboardText("");
Object.assign(document, {
elementFromPoint: () => GlobalTestState.canvas,
});
});
describe("paste text as single lines", () => {
it("should create an element for each line when copying with Ctrl/Cmd+V", async () => {
const text = "sajgfakfn\naaksfnknas\nakefnkasf";
setClipboardText(text);
pasteWithCtrlCmdV();
await waitFor(() => {
expect(h.elements.length).toEqual(text.split("\n").length);
});
});
it("should ignore empty lines when creating an element for each line", async () => {
const text = "\n\nsajgfakfn\n\n\naaksfnknas\n\nakefnkasf\n\n\n";
setClipboardText(text);
pasteWithCtrlCmdV();
await waitFor(() => {
expect(h.elements.length).toEqual(3);
});
});
it("should not create any element if clipboard has only new lines", async () => {
const text = "\n\n\n\n\n";
setClipboardText(text);
pasteWithCtrlCmdV();
await waitFor(async () => {
await sleep(50); // elements lenght will always be zero if we don't wait, since paste is async
expect(h.elements.length).toEqual(0);
});
});
it("should space items correctly", async () => {
const text = "hkhkjhki\njgkjhffjh\njgkjhffjh";
const lineHeightPx =
getLineHeightInPx(
h.app.state.currentItemFontSize,
getDefaultLineHeight(h.state.currentItemFontFamily),
) +
10 / h.app.state.zoom.value;
mouse.moveTo(100, 100);
setClipboardText(text);
pasteWithCtrlCmdV();
await waitFor(async () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [fx, firstElY] = getElementBounds(h.elements[0]);
for (let i = 1; i < h.elements.length; i++) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [fx, elY] = getElementBounds(h.elements[i]);
expect(elY).toEqual(firstElY + lineHeightPx * i);
}
});
});
it("should leave a space for blank new lines", async () => {
const text = "hkhkjhki\n\njgkjhffjh";
const lineHeightPx =
getLineHeightInPx(
h.app.state.currentItemFontSize,
getDefaultLineHeight(h.state.currentItemFontFamily),
) +
10 / h.app.state.zoom.value;
mouse.moveTo(100, 100);
setClipboardText(text);
pasteWithCtrlCmdV();
await waitFor(async () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [fx, firstElY] = getElementBounds(h.elements[0]);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [lx, lastElY] = getElementBounds(h.elements[1]);
expect(lastElY).toEqual(firstElY + lineHeightPx * 2);
});
});
});
describe("paste text as a single element", () => {
it("should create single text element when copying text with Ctrl/Cmd+Shift+V", async () => {
const text = "sajgfakfn\naaksfnknas\nakefnkasf";
setClipboardText(text);
pasteWithCtrlCmdShiftV();
await waitFor(() => {
expect(h.elements.length).toEqual(1);
});
});
it("should not create any element when only new lines in clipboard", async () => {
const text = "\n\n\n\n";
setClipboardText(text);
pasteWithCtrlCmdShiftV();
await waitFor(async () => {
await sleep(50);
expect(h.elements.length).toEqual(0);
});
});
});
describe("Paste bound text container", () => {
const container = {
type: "ellipse",
id: "container-id",
x: 554.984375,
y: 196.0234375,
width: 166,
height: 187.01953125,
roundness: { type: 2 },
boundElements: [{ type: "text", id: "text-id" }],
};
const textElement = {
type: "text",
id: "text-id",
x: 560.51171875,
y: 202.033203125,
width: 154,
height: 175,
fontSize: 20,
fontFamily: 1,
text: "Excalidraw is a\nvirtual \nopensource \nwhiteboard for \nsketching \nhand-drawn like\ndiagrams",
baseline: 168,
textAlign: "center",
verticalAlign: "middle",
containerId: container.id,
originalText:
"Excalidraw is a virtual opensource whiteboard for sketching hand-drawn like diagrams",
};
it("should fix ellipse bounding box", async () => {
const data = JSON.stringify({
type: "excalidraw/clipboard",
elements: [container, textElement],
});
setClipboardText(data);
pasteWithCtrlCmdShiftV();
await waitFor(async () => {
await sleep(1);
expect(h.elements.length).toEqual(2);
const container = h.elements[0];
expect(container.height).toBe(368);
expect(container.width).toBe(166);
});
});
it("should fix diamond bounding box", async () => {
const data = JSON.stringify({
type: "excalidraw/clipboard",
elements: [
{
...container,
type: "diamond",
},
textElement,
],
});
setClipboardText(data);
pasteWithCtrlCmdShiftV();
await waitFor(async () => {
await sleep(1);
expect(h.elements.length).toEqual(2);
const container = h.elements[0];
expect(container.height).toBe(770);
expect(container.width).toBe(166);
});
});
});