Browse Source
* fix: don't mutate the bounded text if not updated when submitted * dont update text for bounded text unless submitted * add specs * use node 16 * fix * Update text when editing and cache prev text * update prev text when props updated * remove only * type properly and remove unnecessary type checks * cache original text and compare with editor value to fix alignement issue after editing and add specs * naming tweak Co-authored-by: dwelle <luzar.david@gmail.com>pull/4569/head
5 changed files with 413 additions and 153 deletions
@ -1,169 +1,413 @@
@@ -1,169 +1,413 @@
|
||||
import ReactDOM from "react-dom"; |
||||
import ExcalidrawApp from "../excalidraw-app"; |
||||
import { render } from "../tests/test-utils"; |
||||
import { Pointer, UI } from "../tests/helpers/ui"; |
||||
import { render, screen } from "../tests/test-utils"; |
||||
import { Keyboard, Pointer, UI } from "../tests/helpers/ui"; |
||||
import { KEYS } from "../keys"; |
||||
|
||||
import { fireEvent } from "../tests/test-utils"; |
||||
import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants"; |
||||
import { ExcalidrawTextElementWithContainer } from "./types"; |
||||
import * as textElementUtils from "./textElement"; |
||||
// Unmount ReactDOM from root
|
||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!); |
||||
|
||||
const tab = " "; |
||||
const mouse = new Pointer("mouse"); |
||||
|
||||
describe("textWysiwyg", () => { |
||||
let textarea: HTMLTextAreaElement; |
||||
beforeEach(async () => { |
||||
await render(<ExcalidrawApp />); |
||||
describe("Test unbounded text", () => { |
||||
let textarea: HTMLTextAreaElement; |
||||
beforeEach(async () => { |
||||
await render(<ExcalidrawApp />); |
||||
|
||||
const element = UI.createElement("text"); |
||||
const element = UI.createElement("text"); |
||||
|
||||
new Pointer("mouse").clickOn(element); |
||||
textarea = document.querySelector( |
||||
".excalidraw-textEditorContainer > textarea", |
||||
)!; |
||||
}); |
||||
mouse.clickOn(element); |
||||
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 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); |
||||
|
||||
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; |
||||
expect(textarea.value).toEqual(`${tab}Line#1\nLine#2`); |
||||
// cursor: " |Line#1\nLine#2"
|
||||
expect(textarea.selectionStart).toEqual(4); |
||||
expect(textarea.selectionEnd).toEqual(4); |
||||
}); |
||||
|
||||
textarea.dispatchEvent(event); |
||||
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; |
||||
|
||||
expect(textarea.value).toEqual(`Line#1\n${tab}Line#2`); |
||||
textarea.dispatchEvent(event); |
||||
|
||||
// cursor: "Line#1\n Lin|e#2"
|
||||
expect(textarea.selectionStart).toEqual(14); |
||||
expect(textarea.selectionEnd).toEqual(14); |
||||
}); |
||||
expect(textarea.value).toEqual(`Line#1\n${tab}Line#2`); |
||||
|
||||
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; |
||||
// cursor: "Line#1\n Lin|e#2"
|
||||
expect(textarea.selectionStart).toEqual(14); |
||||
expect(textarea.selectionEnd).toEqual(14); |
||||
}); |
||||
|
||||
textarea.dispatchEvent(event); |
||||
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; |
||||
|
||||
expect(textarea.value).toEqual(`${tab}Line#1\n${tab}Line#2\nLine#3`); |
||||
textarea.dispatchEvent(event); |
||||
|
||||
// cursor: " Li|ne#1\n Li|ne#2\nLine#3"
|
||||
expect(textarea.selectionStart).toEqual(6); |
||||
expect(textarea.selectionEnd).toEqual(17); |
||||
}); |
||||
expect(textarea.value).toEqual(`${tab}Line#1\n${tab}Line#2\nLine#3`); |
||||
|
||||
it("should remove a tab at the start of the first line", () => { |
||||
const event = new KeyboardEvent("keydown", { |
||||
key: KEYS.TAB, |
||||
shiftKey: true, |
||||
// cursor: " Li|ne#1\n Li|ne#2\nLine#3"
|
||||
expect(textarea.selectionStart).toEqual(6); |
||||
expect(textarea.selectionEnd).toEqual(17); |
||||
}); |
||||
textarea.value = `${tab}Line#1\nLine#2`; |
||||
// cursor: "| Line#1\nLine#2"
|
||||
textarea.selectionStart = 0; |
||||
textarea.selectionEnd = 0; |
||||
|
||||
textarea.dispatchEvent(event); |
||||
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; |
||||
|
||||
expect(textarea.value).toEqual(`Line#1\nLine#2`); |
||||
textarea.dispatchEvent(event); |
||||
|
||||
// cursor: "|Line#1\nLine#2"
|
||||
expect(textarea.selectionStart).toEqual(0); |
||||
expect(textarea.selectionEnd).toEqual(0); |
||||
}); |
||||
expect(textarea.value).toEqual(`Line#1\nLine#2`); |
||||
|
||||
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\nLine#2"
|
||||
expect(textarea.selectionStart).toEqual(0); |
||||
expect(textarea.selectionEnd).toEqual(0); |
||||
}); |
||||
// cursor: "Line#1\n Lin|e#2"
|
||||
textarea.value = `Line#1\n${tab}Line#2`; |
||||
textarea.selectionStart = 15; |
||||
textarea.selectionEnd = 15; |
||||
|
||||
textarea.dispatchEvent(event); |
||||
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; |
||||
|
||||
expect(textarea.value).toEqual(`Line#1\nLine#2`); |
||||
// cursor: "Line#1\nLin|e#2"
|
||||
expect(textarea.selectionStart).toEqual(11); |
||||
expect(textarea.selectionEnd).toEqual(11); |
||||
}); |
||||
textarea.dispatchEvent(event); |
||||
|
||||
it("should remove a tab at the start of the first and second line", () => { |
||||
const event = new KeyboardEvent("keydown", { |
||||
key: KEYS.TAB, |
||||
shiftKey: true, |
||||
expect(textarea.value).toEqual(`Line#1\nLine#2`); |
||||
// cursor: "Line#1\nLin|e#2"
|
||||
expect(textarea.selectionStart).toEqual(11); |
||||
expect(textarea.selectionEnd).toEqual(11); |
||||
}); |
||||
// 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); |
||||
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; |
||||
|
||||
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); |
||||
}); |
||||
textarea.dispatchEvent(event); |
||||
|
||||
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, |
||||
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); |
||||
}); |
||||
// 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 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, |
||||
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`); |
||||
}); |
||||
// 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`); |
||||
}); |
||||
}); |
||||
describe("Test bounded text", () => { |
||||
let rectangle: any; |
||||
const { |
||||
h, |
||||
}: { |
||||
h: { |
||||
elements: any; |
||||
}; |
||||
} = 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 />); |
||||
|
||||
rectangle = UI.createElement("rectangle", { |
||||
x: 10, |
||||
y: 20, |
||||
width: 90, |
||||
height: 75, |
||||
}); |
||||
}); |
||||
|
||||
it("should bind text to container when double clicked on center", 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; |
||||
|
||||
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("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.withModifierKeys({}, () => { |
||||
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("should update font family correctly on undo/redo by selecting bounded text when font family was updated", async () => { |
||||
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].fontFamily).toEqual(FONT_FAMILY.Cascadia); |
||||
|
||||
it("should remove nothing", () => { |
||||
const event = new KeyboardEvent("keydown", { |
||||
key: KEYS.TAB, |
||||
shiftKey: true, |
||||
//undo
|
||||
Keyboard.withModifierKeys({ ctrl: true }, () => { |
||||
Keyboard.keyPress(KEYS.Z); |
||||
}); |
||||
expect(h.elements[1].fontFamily).toEqual(FONT_FAMILY.Virgil); |
||||
|
||||
//redo
|
||||
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => { |
||||
Keyboard.keyPress(KEYS.Z); |
||||
}); |
||||
expect(h.elements[1].fontFamily).toEqual(FONT_FAMILY.Cascadia); |
||||
}); |
||||
// 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 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, |
||||
}; |
||||
}); |
||||
|
||||
Keyboard.withModifierKeys({}, () => { |
||||
Keyboard.keyPress(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.withModifierKeys({}, () => { |
||||
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); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
Loading…
Reference in new issue