12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103 |
- import ReactDOM from "react-dom";
- import ExcalidrawApp from "../excalidraw-app";
- import { GlobalTestState, render, screen } from "../tests/test-utils";
- import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
- import { CODES, KEYS } from "../keys";
- import { fireEvent } from "../tests/test-utils";
- import { queryByText } from "@testing-library/react";
- import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants";
- import {
- ExcalidrawTextElement,
- ExcalidrawTextElementWithContainer,
- FontString,
- } from "./types";
- import * as textElementUtils from "./textElement";
- import { API } from "../tests/helpers/api";
- import { mutateElement } from "./mutateElement";
- import { resize } from "../tests/utils";
- import { getMaxContainerWidth } from "./newElement";
- // Unmount ReactDOM from root
- ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
- const tab = " ";
- const mouse = new Pointer("mouse");
- describe("textWysiwyg", () => {
- describe("start text editing", () => {
- const { h } = window;
- beforeEach(async () => {
- await render(<ExcalidrawApp />);
- h.elements = [];
- });
- it("should prefer editing selected text element (non-bindable container present)", async () => {
- const line = API.createElement({
- type: "line",
- width: 100,
- height: 0,
- points: [
- [0, 0],
- [100, 0],
- ],
- });
- const textSize = 20;
- const text = API.createElement({
- type: "text",
- text: "ola",
- x: line.width / 2 - textSize / 2,
- y: -textSize / 2,
- width: textSize,
- height: textSize,
- });
- h.elements = [text, line];
- API.setSelectedElements([text]);
- Keyboard.keyPress(KEYS.ENTER);
- expect(h.state.editingElement?.id).toBe(text.id);
- expect(
- (h.state.editingElement as ExcalidrawTextElement).containerId,
- ).toBe(null);
- });
- it("should prefer editing selected text element (bindable container present)", async () => {
- const container = API.createElement({
- type: "rectangle",
- width: 100,
- boundElements: [],
- });
- const textSize = 20;
- const boundText = API.createElement({
- type: "text",
- text: "ola",
- x: container.width / 2 - textSize / 2,
- y: container.height / 2 - textSize / 2,
- width: textSize,
- height: textSize,
- containerId: container.id,
- });
- const boundText2 = API.createElement({
- type: "text",
- text: "ola",
- x: container.width / 2 - textSize / 2,
- y: container.height / 2 - textSize / 2,
- width: textSize,
- height: textSize,
- containerId: container.id,
- });
- h.elements = [container, boundText, boundText2];
- mutateElement(container, {
- boundElements: [{ type: "text", id: boundText.id }],
- });
- API.setSelectedElements([boundText2]);
- Keyboard.keyPress(KEYS.ENTER);
- expect(h.state.editingElement?.id).toBe(boundText2.id);
- });
- it("should not create bound text on ENTER if text exists at container center", () => {
- const container = API.createElement({
- type: "rectangle",
- width: 100,
- });
- const textSize = 20;
- const text = API.createElement({
- type: "text",
- text: "ola",
- x: container.width / 2 - textSize / 2,
- y: container.height / 2 - textSize / 2,
- width: textSize,
- height: textSize,
- containerId: container.id,
- });
- mutateElement(container, {
- boundElements: [{ type: "text", id: text.id }],
- });
- h.elements = [container, text];
- API.setSelectedElements([container]);
- Keyboard.keyPress(KEYS.ENTER);
- expect(h.state.editingElement?.id).toBe(text.id);
- });
- it("should edit existing bound text on ENTER even if higher z-index unbound text exists at container center", () => {
- const container = API.createElement({
- type: "rectangle",
- width: 100,
- boundElements: [],
- });
- const textSize = 20;
- const boundText = API.createElement({
- type: "text",
- text: "ola",
- x: container.width / 2 - textSize / 2,
- y: container.height / 2 - textSize / 2,
- width: textSize,
- height: textSize,
- containerId: container.id,
- });
- const boundText2 = API.createElement({
- type: "text",
- text: "ola",
- x: container.width / 2 - textSize / 2,
- y: container.height / 2 - textSize / 2,
- width: textSize,
- height: textSize,
- containerId: container.id,
- });
- h.elements = [container, boundText, boundText2];
- mutateElement(container, {
- boundElements: [{ type: "text", id: boundText.id }],
- });
- API.setSelectedElements([container]);
- Keyboard.keyPress(KEYS.ENTER);
- expect(h.state.editingElement?.id).toBe(boundText.id);
- });
- it("should edit text under cursor when clicked with text tool", () => {
- const text = API.createElement({
- type: "text",
- text: "ola",
- x: 60,
- y: 0,
- width: 100,
- height: 100,
- });
- h.elements = [text];
- UI.clickTool("text");
- mouse.clickAt(text.x + 50, text.y + 50);
- const editor = document.querySelector(
- ".excalidraw-textEditorContainer > textarea",
- ) as HTMLTextAreaElement;
- expect(editor).not.toBe(null);
- expect(h.state.editingElement?.id).toBe(text.id);
- expect(h.elements.length).toBe(1);
- });
- it("should edit text under cursor when double-clicked with selection tool", () => {
- const text = API.createElement({
- type: "text",
- text: "ola",
- x: 60,
- y: 0,
- width: 100,
- height: 100,
- });
- h.elements = [text];
- UI.clickTool("selection");
- mouse.doubleClickAt(text.x + 50, text.y + 50);
- const editor = document.querySelector(
- ".excalidraw-textEditorContainer > textarea",
- ) as HTMLTextAreaElement;
- expect(editor).not.toBe(null);
- expect(h.state.editingElement?.id).toBe(text.id);
- expect(h.elements.length).toBe(1);
- });
- });
- describe("Test container-unbound text", () => {
- const { h } = window;
- let textarea: HTMLTextAreaElement;
- let textElement: ExcalidrawTextElement;
- beforeEach(async () => {
- await render(<ExcalidrawApp />);
- textElement = UI.createElement("text");
- mouse.clickOn(textElement);
- textarea = document.querySelector(
- ".excalidraw-textEditorContainer > textarea",
- )!;
- });
- it("should add a tab at the start of the first line", () => {
- const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
- textarea.value = "Line#1\nLine#2";
- // cursor: "|Line#1\nLine#2"
- textarea.selectionStart = 0;
- textarea.selectionEnd = 0;
- textarea.dispatchEvent(event);
- expect(textarea.value).toEqual(`${tab}Line#1\nLine#2`);
- // cursor: " |Line#1\nLine#2"
- expect(textarea.selectionStart).toEqual(4);
- expect(textarea.selectionEnd).toEqual(4);
- });
- it("should add a tab at the start of the second line", () => {
- const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
- textarea.value = "Line#1\nLine#2";
- // cursor: "Line#1\nLin|e#2"
- textarea.selectionStart = 10;
- textarea.selectionEnd = 10;
- textarea.dispatchEvent(event);
- expect(textarea.value).toEqual(`Line#1\n${tab}Line#2`);
- // cursor: "Line#1\n Lin|e#2"
- expect(textarea.selectionStart).toEqual(14);
- expect(textarea.selectionEnd).toEqual(14);
- });
- it("should add a tab at the start of the first and second line", () => {
- const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
- textarea.value = "Line#1\nLine#2\nLine#3";
- // cursor: "Li|ne#1\nLi|ne#2\nLine#3"
- textarea.selectionStart = 2;
- textarea.selectionEnd = 9;
- textarea.dispatchEvent(event);
- expect(textarea.value).toEqual(`${tab}Line#1\n${tab}Line#2\nLine#3`);
- // cursor: " Li|ne#1\n Li|ne#2\nLine#3"
- expect(textarea.selectionStart).toEqual(6);
- expect(textarea.selectionEnd).toEqual(17);
- });
- it("should remove a tab at the start of the first line", () => {
- const event = new KeyboardEvent("keydown", {
- key: KEYS.TAB,
- shiftKey: true,
- });
- textarea.value = `${tab}Line#1\nLine#2`;
- // cursor: "| Line#1\nLine#2"
- textarea.selectionStart = 0;
- textarea.selectionEnd = 0;
- textarea.dispatchEvent(event);
- expect(textarea.value).toEqual(`Line#1\nLine#2`);
- // cursor: "|Line#1\nLine#2"
- expect(textarea.selectionStart).toEqual(0);
- expect(textarea.selectionEnd).toEqual(0);
- });
- it("should remove a tab at the start of the second line", () => {
- const event = new KeyboardEvent("keydown", {
- key: KEYS.TAB,
- shiftKey: true,
- });
- // cursor: "Line#1\n Lin|e#2"
- textarea.value = `Line#1\n${tab}Line#2`;
- textarea.selectionStart = 15;
- textarea.selectionEnd = 15;
- textarea.dispatchEvent(event);
- expect(textarea.value).toEqual(`Line#1\nLine#2`);
- // cursor: "Line#1\nLin|e#2"
- expect(textarea.selectionStart).toEqual(11);
- expect(textarea.selectionEnd).toEqual(11);
- });
- it("should remove a tab at the start of the first and second line", () => {
- const event = new KeyboardEvent("keydown", {
- key: KEYS.TAB,
- shiftKey: true,
- });
- // cursor: " Li|ne#1\n Li|ne#2\nLine#3"
- textarea.value = `${tab}Line#1\n${tab}Line#2\nLine#3`;
- textarea.selectionStart = 6;
- textarea.selectionEnd = 17;
- textarea.dispatchEvent(event);
- expect(textarea.value).toEqual(`Line#1\nLine#2\nLine#3`);
- // cursor: "Li|ne#1\nLi|ne#2\nLine#3"
- expect(textarea.selectionStart).toEqual(2);
- expect(textarea.selectionEnd).toEqual(9);
- });
- it("should remove a tab at the start of the second line and cursor stay on this line", () => {
- const event = new KeyboardEvent("keydown", {
- key: KEYS.TAB,
- shiftKey: true,
- });
- // cursor: "Line#1\n | Line#2"
- textarea.value = `Line#1\n${tab}Line#2`;
- textarea.selectionStart = 9;
- textarea.selectionEnd = 9;
- textarea.dispatchEvent(event);
- // cursor: "Line#1\n|Line#2"
- expect(textarea.selectionStart).toEqual(7);
- // expect(textarea.selectionEnd).toEqual(7);
- });
- it("should remove partial tabs", () => {
- const event = new KeyboardEvent("keydown", {
- key: KEYS.TAB,
- shiftKey: true,
- });
- // cursor: "Line#1\n Line#|2"
- textarea.value = `Line#1\n Line#2`;
- textarea.selectionStart = 15;
- textarea.selectionEnd = 15;
- textarea.dispatchEvent(event);
- expect(textarea.value).toEqual(`Line#1\nLine#2`);
- });
- it("should remove nothing", () => {
- const event = new KeyboardEvent("keydown", {
- key: KEYS.TAB,
- shiftKey: true,
- });
- // cursor: "Line#1\n Li|ne#2"
- textarea.value = `Line#1\nLine#2`;
- textarea.selectionStart = 9;
- textarea.selectionEnd = 9;
- textarea.dispatchEvent(event);
- expect(textarea.value).toEqual(`Line#1\nLine#2`);
- });
- it("should resize text via shortcuts while in wysiwyg", () => {
- textarea.value = "abc def";
- const origFontSize = textElement.fontSize;
- textarea.dispatchEvent(
- new KeyboardEvent("keydown", {
- key: KEYS.CHEVRON_RIGHT,
- ctrlKey: true,
- shiftKey: true,
- }),
- );
- expect(textElement.fontSize).toBe(origFontSize * 1.1);
- textarea.dispatchEvent(
- new KeyboardEvent("keydown", {
- key: KEYS.CHEVRON_LEFT,
- ctrlKey: true,
- shiftKey: true,
- }),
- );
- expect(textElement.fontSize).toBe(origFontSize);
- });
- it("zooming via keyboard should zoom canvas", () => {
- expect(h.state.zoom.value).toBe(1);
- textarea.dispatchEvent(
- new KeyboardEvent("keydown", {
- code: CODES.MINUS,
- ctrlKey: true,
- }),
- );
- expect(h.state.zoom.value).toBe(0.9);
- textarea.dispatchEvent(
- new KeyboardEvent("keydown", {
- code: CODES.NUM_SUBTRACT,
- ctrlKey: true,
- }),
- );
- expect(h.state.zoom.value).toBe(0.8);
- textarea.dispatchEvent(
- new KeyboardEvent("keydown", {
- code: CODES.NUM_ADD,
- ctrlKey: true,
- }),
- );
- expect(h.state.zoom.value).toBe(0.9);
- textarea.dispatchEvent(
- new KeyboardEvent("keydown", {
- code: CODES.EQUAL,
- ctrlKey: true,
- }),
- );
- expect(h.state.zoom.value).toBe(1);
- });
- it("should paste text correctly", async () => {
- Keyboard.keyPress(KEYS.ENTER);
- await new Promise((r) => setTimeout(r, 0));
- let text = "A quick brown fox jumps over the lazy dog.";
- //@ts-ignore
- textarea.onpaste({
- preventDefault: () => {},
- //@ts-ignore
- clipboardData: {
- getData: () => text,
- },
- });
- await new Promise((cb) => setTimeout(cb, 0));
- textarea.blur();
- expect(textElement.text).toBe(text);
- Keyboard.keyPress(KEYS.ENTER);
- await new Promise((r) => setTimeout(r, 0));
- text = "Hello this text should get merged with the existing one";
- //@ts-ignore
- textarea.onpaste({
- preventDefault: () => {},
- //@ts-ignore
- clipboardData: {
- getData: () => text,
- },
- });
- await new Promise((cb) => setTimeout(cb, 0));
- textarea.blur();
- expect(textElement.text).toMatchInlineSnapshot(
- `"A quick brown fox jumps over the lazy dog.Hello this text should get merged with the existing one"`,
- );
- });
- });
- describe("Test container-bound text", () => {
- let rectangle: any;
- const { h } = window;
- const DUMMY_HEIGHT = 240;
- const DUMMY_WIDTH = 160;
- const APPROX_LINE_HEIGHT = 25;
- const INITIAL_WIDTH = 10;
- beforeAll(() => {
- jest
- .spyOn(textElementUtils, "getApproxLineHeight")
- .mockReturnValue(APPROX_LINE_HEIGHT);
- });
- beforeEach(async () => {
- await render(<ExcalidrawApp />);
- h.elements = [];
- rectangle = UI.createElement("rectangle", {
- x: 10,
- y: 20,
- width: 90,
- height: 75,
- });
- });
- it("should bind text to container when double clicked on center of filled container", async () => {
- expect(h.elements.length).toBe(1);
- expect(h.elements[0].id).toBe(rectangle.id);
- mouse.doubleClickAt(
- rectangle.x + rectangle.width / 2,
- rectangle.y + rectangle.height / 2,
- );
- expect(h.elements.length).toBe(2);
- const text = h.elements[1] as ExcalidrawTextElementWithContainer;
- expect(text.type).toBe("text");
- expect(text.containerId).toBe(rectangle.id);
- mouse.down();
- const editor = document.querySelector(
- ".excalidraw-textEditorContainer > textarea",
- ) as HTMLTextAreaElement;
- fireEvent.change(editor, { target: { value: "Hello World!" } });
- await new Promise((r) => setTimeout(r, 0));
- editor.blur();
- expect(rectangle.boundElements).toStrictEqual([
- { id: text.id, type: "text" },
- ]);
- });
- it("should bind text to container when double clicked on center of transparent container", async () => {
- const rectangle = API.createElement({
- type: "rectangle",
- x: 10,
- y: 20,
- width: 90,
- height: 75,
- backgroundColor: "transparent",
- });
- h.elements = [rectangle];
- mouse.doubleClickAt(
- rectangle.x + rectangle.width / 2,
- rectangle.y + rectangle.height / 2,
- );
- expect(h.elements.length).toBe(2);
- const text = h.elements[1] as ExcalidrawTextElementWithContainer;
- expect(text.type).toBe("text");
- expect(text.containerId).toBe(rectangle.id);
- mouse.down();
- const editor = document.querySelector(
- ".excalidraw-textEditorContainer > textarea",
- ) as HTMLTextAreaElement;
- fireEvent.change(editor, { target: { value: "Hello World!" } });
- await new Promise((r) => setTimeout(r, 0));
- editor.blur();
- expect(rectangle.boundElements).toStrictEqual([
- { id: text.id, type: "text" },
- ]);
- });
- it("should bind text to container when clicked on container and enter pressed", async () => {
- expect(h.elements.length).toBe(1);
- expect(h.elements[0].id).toBe(rectangle.id);
- Keyboard.keyPress(KEYS.ENTER);
- expect(h.elements.length).toBe(2);
- const text = h.elements[1] as ExcalidrawTextElementWithContainer;
- expect(text.type).toBe("text");
- expect(text.containerId).toBe(rectangle.id);
- const editor = document.querySelector(
- ".excalidraw-textEditorContainer > textarea",
- ) as HTMLTextAreaElement;
- await new Promise((r) => setTimeout(r, 0));
- fireEvent.change(editor, { target: { value: "Hello World!" } });
- editor.blur();
- expect(rectangle.boundElements).toStrictEqual([
- { id: text.id, type: "text" },
- ]);
- });
- it("shouldn't bind to non-text-bindable containers", async () => {
- const line = API.createElement({
- type: "line",
- width: 100,
- height: 0,
- points: [
- [0, 0],
- [100, 0],
- ],
- });
- h.elements = [line];
- UI.clickTool("text");
- mouse.clickAt(line.x + line.width / 2, line.y + line.height / 2);
- const editor = document.querySelector(
- ".excalidraw-textEditorContainer > textarea",
- ) as HTMLTextAreaElement;
- fireEvent.change(editor, {
- target: {
- value: "Hello World!",
- },
- });
- fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
- editor.dispatchEvent(new Event("input"));
- expect(line.boundElements).toBe(null);
- expect(h.elements[1].type).toBe("text");
- expect((h.elements[1] as ExcalidrawTextElement).containerId).toBe(null);
- });
- it("shouldn't create text element when pressing 'Enter' key on non text bindable container", async () => {
- h.elements = [];
- const freeDraw = UI.createElement("freedraw", {
- width: 100,
- height: 50,
- });
- API.setSelectedElements([freeDraw]);
- Keyboard.keyPress(KEYS.ENTER);
- expect(h.elements.length).toBe(1);
- });
- it("should'nt bind text to container when not double clicked on center", async () => {
- expect(h.elements.length).toBe(1);
- expect(h.elements[0].id).toBe(rectangle.id);
- // clicking somewhere on top left
- mouse.doubleClickAt(rectangle.x + 20, rectangle.y + 20);
- expect(h.elements.length).toBe(2);
- const text = h.elements[1] as ExcalidrawTextElementWithContainer;
- expect(text.type).toBe("text");
- expect(text.containerId).toBe(null);
- mouse.down();
- const editor = document.querySelector(
- ".excalidraw-textEditorContainer > textarea",
- ) as HTMLTextAreaElement;
- fireEvent.change(editor, { target: { value: "Hello World!" } });
- await new Promise((r) => setTimeout(r, 0));
- editor.blur();
- expect(rectangle.boundElements).toBe(null);
- });
- it("should update font family correctly on undo/redo by selecting bounded text when font family was updated", async () => {
- expect(h.elements.length).toBe(1);
- mouse.doubleClickAt(
- rectangle.x + rectangle.width / 2,
- rectangle.y + rectangle.height / 2,
- );
- mouse.down();
- const text = h.elements[1] as ExcalidrawTextElementWithContainer;
- let editor = document.querySelector(
- ".excalidraw-textEditorContainer > textarea",
- ) as HTMLTextAreaElement;
- await new Promise((r) => setTimeout(r, 0));
- fireEvent.change(editor, { target: { value: "Hello World!" } });
- editor.blur();
- expect(text.fontFamily).toEqual(FONT_FAMILY.Virgil);
- UI.clickTool("text");
- mouse.clickAt(
- rectangle.x + rectangle.width / 2,
- rectangle.y + rectangle.height / 2,
- );
- mouse.down();
- editor = document.querySelector(
- ".excalidraw-textEditorContainer > textarea",
- ) as HTMLTextAreaElement;
- editor.select();
- fireEvent.click(screen.getByTitle(/code/i));
- await new Promise((r) => setTimeout(r, 0));
- editor.blur();
- expect(
- (h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
- ).toEqual(FONT_FAMILY.Cascadia);
- //undo
- Keyboard.withModifierKeys({ ctrl: true }, () => {
- Keyboard.keyPress(KEYS.Z);
- });
- expect(
- (h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
- ).toEqual(FONT_FAMILY.Virgil);
- //redo
- Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
- Keyboard.keyPress(KEYS.Z);
- });
- expect(
- (h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
- ).toEqual(FONT_FAMILY.Cascadia);
- });
- it("should wrap text and vertcially center align once text submitted", async () => {
- jest
- .spyOn(textElementUtils, "measureText")
- .mockImplementation((text, font, maxWidth) => {
- let width = INITIAL_WIDTH;
- let height = APPROX_LINE_HEIGHT;
- let baseline = 10;
- if (!text) {
- return {
- width,
- height,
- baseline,
- };
- }
- baseline = 30;
- width = DUMMY_WIDTH;
- if (text === "Hello \nWorld!") {
- height = APPROX_LINE_HEIGHT * 2;
- }
- if (maxWidth) {
- width = maxWidth;
- // To capture cases where maxWidth passed is initial width
- // due to which the text is not wrapped correctly
- if (maxWidth === INITIAL_WIDTH) {
- height = DUMMY_HEIGHT;
- }
- }
- return {
- width,
- height,
- baseline,
- };
- });
- expect(h.elements.length).toBe(1);
- Keyboard.keyDown(KEYS.ENTER);
- let text = h.elements[1] as ExcalidrawTextElementWithContainer;
- let editor = document.querySelector(
- ".excalidraw-textEditorContainer > textarea",
- ) as HTMLTextAreaElement;
- // mock scroll height
- jest
- .spyOn(editor, "scrollHeight", "get")
- .mockImplementation(() => APPROX_LINE_HEIGHT * 2);
- fireEvent.change(editor, {
- target: {
- value: "Hello World!",
- },
- });
- editor.dispatchEvent(new Event("input"));
- await new Promise((cb) => setTimeout(cb, 0));
- editor.blur();
- text = h.elements[1] as ExcalidrawTextElementWithContainer;
- expect(text.text).toBe("Hello \nWorld!");
- expect(text.originalText).toBe("Hello World!");
- expect(text.y).toBe(
- rectangle.y + rectangle.height / 2 - (APPROX_LINE_HEIGHT * 2) / 2,
- );
- expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING);
- expect(text.height).toBe(APPROX_LINE_HEIGHT * 2);
- expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2);
- // Edit and text by removing second line and it should
- // still vertically align correctly
- mouse.select(rectangle);
- Keyboard.keyPress(KEYS.ENTER);
- editor = document.querySelector(
- ".excalidraw-textEditorContainer > textarea",
- ) as HTMLTextAreaElement;
- fireEvent.change(editor, {
- target: {
- value: "Hello",
- },
- });
- // mock scroll height
- jest
- .spyOn(editor, "scrollHeight", "get")
- .mockImplementation(() => APPROX_LINE_HEIGHT);
- editor.style.height = "25px";
- editor.dispatchEvent(new Event("input"));
- await new Promise((r) => setTimeout(r, 0));
- editor.blur();
- text = h.elements[1] as ExcalidrawTextElementWithContainer;
- expect(text.text).toBe("Hello");
- expect(text.originalText).toBe("Hello");
- expect(text.y).toBe(
- rectangle.y + rectangle.height / 2 - APPROX_LINE_HEIGHT / 2,
- );
- expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING);
- expect(text.height).toBe(APPROX_LINE_HEIGHT);
- expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2);
- });
- it("should unbind bound text when unbind action from context menu is triggered", async () => {
- expect(h.elements.length).toBe(1);
- expect(h.elements[0].id).toBe(rectangle.id);
- Keyboard.keyPress(KEYS.ENTER);
- expect(h.elements.length).toBe(2);
- const text = h.elements[1] as ExcalidrawTextElementWithContainer;
- expect(text.containerId).toBe(rectangle.id);
- const editor = document.querySelector(
- ".excalidraw-textEditorContainer > textarea",
- ) as HTMLTextAreaElement;
- await new Promise((r) => setTimeout(r, 0));
- fireEvent.change(editor, { target: { value: "Hello World!" } });
- editor.blur();
- expect(rectangle.boundElements).toStrictEqual([
- { id: text.id, type: "text" },
- ]);
- mouse.reset();
- UI.clickTool("selection");
- mouse.clickAt(10, 20);
- mouse.down();
- mouse.up();
- fireEvent.contextMenu(GlobalTestState.canvas, {
- button: 2,
- clientX: 20,
- clientY: 30,
- });
- const contextMenu = document.querySelector(".context-menu");
- fireEvent.click(queryByText(contextMenu as HTMLElement, "Unbind text")!);
- expect(h.elements[0].boundElements).toEqual([]);
- expect((h.elements[1] as ExcalidrawTextElement).containerId).toEqual(
- null,
- );
- });
- it("shouldn't bind to container if container has bound text", async () => {
- expect(h.elements.length).toBe(1);
- Keyboard.keyPress(KEYS.ENTER);
- expect(h.elements.length).toBe(2);
- // Bind first text
- const text = h.elements[1] as ExcalidrawTextElementWithContainer;
- expect(text.containerId).toBe(rectangle.id);
- const editor = document.querySelector(
- ".excalidraw-textEditorContainer > textarea",
- ) as HTMLTextAreaElement;
- await new Promise((r) => setTimeout(r, 0));
- fireEvent.change(editor, { target: { value: "Hello World!" } });
- editor.blur();
- expect(rectangle.boundElements).toStrictEqual([
- { id: text.id, type: "text" },
- ]);
- mouse.select(rectangle);
- Keyboard.keyPress(KEYS.ENTER);
- expect(h.elements.length).toBe(2);
- expect(rectangle.boundElements).toStrictEqual([
- { id: h.elements[1].id, type: "text" },
- ]);
- expect(text.containerId).toBe(rectangle.id);
- });
- it("should respect text alignment when resizing", async () => {
- Keyboard.keyPress(KEYS.ENTER);
- let editor = document.querySelector(
- ".excalidraw-textEditorContainer > textarea",
- ) as HTMLTextAreaElement;
- await new Promise((r) => setTimeout(r, 0));
- fireEvent.change(editor, { target: { value: "Hello" } });
- editor.blur();
- // should center align horizontally and vertically by default
- resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
- expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
- Array [
- 109.5,
- 17,
- ]
- `);
- mouse.select(rectangle);
- Keyboard.keyPress(KEYS.ENTER);
- editor = document.querySelector(
- ".excalidraw-textEditorContainer > textarea",
- ) as HTMLTextAreaElement;
- editor.select();
- fireEvent.click(screen.getByTitle("Left"));
- fireEvent.click(screen.getByTitle("Align bottom"));
- await new Promise((r) => setTimeout(r, 0));
- editor.blur();
- // should left align horizontally and bottom vertically after resize
- resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
- expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
- Array [
- 15,
- 90,
- ]
- `);
- mouse.select(rectangle);
- Keyboard.keyPress(KEYS.ENTER);
- editor = document.querySelector(
- ".excalidraw-textEditorContainer > textarea",
- ) as HTMLTextAreaElement;
- editor.select();
- fireEvent.click(screen.getByTitle("Right"));
- fireEvent.click(screen.getByTitle("Align top"));
- await new Promise((r) => setTimeout(r, 0));
- editor.blur();
- // should right align horizontally and top vertically after resize
- resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
- expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
- Array [
- 424,
- -539,
- ]
- `);
- });
- it("should compute the dimensions correctly when text pasted", async () => {
- Keyboard.keyPress(KEYS.ENTER);
- const editor = document.querySelector(
- ".excalidraw-textEditorContainer > textarea",
- ) as HTMLTextAreaElement;
- await new Promise((r) => setTimeout(r, 0));
- const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
- let text =
- "Wikipedia is hosted by the Wikimedia Foundation, a non-profit organization that also hosts a range of other projects.";
- let wrappedText = textElementUtils.wrapText(
- text,
- font,
- getMaxContainerWidth(rectangle),
- );
- jest
- .spyOn(textElementUtils, "measureText")
- .mockImplementation((text, font, maxWidth) => {
- if (text === wrappedText) {
- return { width: rectangle.width, height: 200, baseline: 30 };
- }
- return { width: 0, height: 0, baseline: 0 };
- });
- //@ts-ignore
- editor.onpaste({
- preventDefault: () => {},
- //@ts-ignore
- clipboardData: {
- getData: () => text,
- },
- });
- await new Promise((cb) => setTimeout(cb, 0));
- editor.blur();
- expect(rectangle.width).toBe(100);
- expect(rectangle.height).toBe(210);
- expect((h.elements[1] as ExcalidrawTextElement).text)
- .toMatchInlineSnapshot(`
- "Wikipedi
- a is
- hosted
- by the
- Wikimedi
- a
- Foundati
- on, a
- non-prof
- it
- organiza
- tion
- that
- also
- hosts a
- range of
- other
- projects
- ."
- `);
- expect(
- (h.elements[1] as ExcalidrawTextElement).originalText,
- ).toMatchInlineSnapshot(
- `"Wikipedia is hosted by the Wikimedia Foundation, a non-profit organization that also hosts a range of other projects."`,
- );
- text = "Hello this text should get merged with the existing one";
- wrappedText = textElementUtils.wrapText(
- text,
- font,
- getMaxContainerWidth(rectangle),
- );
- //@ts-ignore
- editor.onpaste({
- preventDefault: () => {},
- //@ts-ignore
- clipboardData: {
- getData: () => text,
- },
- });
- await new Promise((cb) => setTimeout(cb, 0));
- editor.blur();
- expect((h.elements[1] as ExcalidrawTextElement).text)
- .toMatchInlineSnapshot(`
- "Wikipedi
- a is
- hosted
- by the
- Wikimedi
- a
- Foundati
- on, a
- non-prof
- it
- organiza
- tion
- that
- also
- hosts a
- range of
- other
- projects
- .Hello
- this
- text
- should
- get
- merged
- with the
- existing
- one"
- `);
- expect(
- (h.elements[1] as ExcalidrawTextElement).originalText,
- ).toMatchInlineSnapshot(
- `"Wikipedia is hosted by the Wikimedia Foundation, a non-profit organization that also hosts a range of other projects.Hello this text should get merged with the existing one"`,
- );
- });
- it("should always bind to selected container and insert it in correct position", async () => {
- const rectangle2 = UI.createElement("rectangle", {
- x: 5,
- y: 10,
- width: 120,
- height: 100,
- });
- API.setSelectedElements([rectangle]);
- Keyboard.keyPress(KEYS.ENTER);
- expect(h.elements.length).toBe(3);
- expect(h.elements[1].type).toBe("text");
- const text = h.elements[1] as ExcalidrawTextElementWithContainer;
- expect(text.type).toBe("text");
- expect(text.containerId).toBe(rectangle.id);
- mouse.down();
- const editor = document.querySelector(
- ".excalidraw-textEditorContainer > textarea",
- ) as HTMLTextAreaElement;
- fireEvent.change(editor, { target: { value: "Hello World!" } });
- await new Promise((r) => setTimeout(r, 0));
- editor.blur();
- expect(rectangle2.boundElements).toBeNull();
- expect(rectangle.boundElements).toStrictEqual([
- { id: text.id, type: "text" },
- ]);
- });
- });
- });
|