소스 검색

fix: don't mutate the bounded text if not updated when submitted (#4543)

* 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>
Aakansha Doshi 3 년 전
부모
커밋
50bd5fbae1
5개의 변경된 파일413개의 추가작업 그리고 153개의 파일을 삭제
  1. 1 1
      .github/workflows/test.yml
  2. 5 5
      src/components/App.tsx
  3. 13 5
      src/element/newElement.ts
  4. 368 124
      src/element/textWysiwyg.test.tsx
  5. 26 18
      src/element/textWysiwyg.tsx

+ 1 - 1
.github/workflows/test.yml

@@ -10,7 +10,7 @@ jobs:
       - name: Setup Node.js 14.x
         uses: actions/setup-node@v2
         with:
-          node-version: 14.x
+          node-version: 16.x
       - name: Install and test
         run: |
           yarn --frozen-lockfile

+ 5 - 5
src/components/App.tsx

@@ -1911,8 +1911,8 @@ class App extends React.Component<AppProps, AppState> {
     const updateElement = (
       text: string,
       originalText: string,
-      isDeleted = false,
-      updateDimensions = false,
+      isDeleted: boolean,
+      isSubmit: boolean,
     ) => {
       this.scene.replaceAllElements([
         ...this.scene.getElementsIncludingDeleted().map((_element) => {
@@ -1924,7 +1924,7 @@ class App extends React.Component<AppProps, AppState> {
                 isDeleted,
                 originalText,
               },
-              updateDimensions,
+              isSubmit,
             );
           }
           return _element;
@@ -1950,7 +1950,7 @@ class App extends React.Component<AppProps, AppState> {
         ];
       },
       onChange: withBatchedUpdates((text) => {
-        updateElement(text, text, false, !element.containerId);
+        updateElement(text, text, false, false);
         if (isNonDeletedElement(element)) {
           updateBoundElements(element);
         }
@@ -1996,7 +1996,7 @@ class App extends React.Component<AppProps, AppState> {
 
     // do an initial update to re-initialize element position since we were
     // modifying element's x/y for sake of editor (case: syncing to remote)
-    updateElement(element.text, element.originalText);
+    updateElement(element.text, element.originalText, false, false);
   }
 
   private deselectElements() {

+ 13 - 5
src/element/newElement.ts

@@ -158,7 +158,11 @@ const getAdjustedDimensions = (
   height: number;
   baseline: number;
 } => {
-  const maxWidth = element.containerId ? element.width : null;
+  let maxWidth = null;
+  if (element.containerId) {
+    const container = Scene.getScene(element)!.getElement(element.containerId)!;
+    maxWidth = container.width - BOUND_TEXT_PADDING * 2;
+  }
   const {
     width: nextWidth,
     height: nextHeight,
@@ -246,11 +250,15 @@ export const updateTextElement = (
     originalText,
   }: { text: string; isDeleted?: boolean; originalText: string },
 
-  updateDimensions: boolean,
+  isSubmit: boolean,
 ): ExcalidrawTextElement => {
-  const dimensions = updateDimensions
-    ? getAdjustedDimensions(element, text)
-    : undefined;
+  const boundToContainer = isBoundToContainer(element);
+
+  // Don't update dimensions and text value for bounded text unless submitted
+  const dimensions =
+    boundToContainer && !isSubmit
+      ? undefined
+      : getAdjustedDimensions(element, text);
   return newElementWith(element, {
     text,
     originalText,

+ 368 - 124
src/element/textWysiwyg.test.tsx

@@ -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);
+    });
   });
 });

+ 26 - 18
src/element/textWysiwyg.tsx

@@ -3,6 +3,7 @@ import {
   isWritableElement,
   getFontString,
   getFontFamilyString,
+  isTestEnv,
 } from "../utils";
 import Scene from "../scene/Scene";
 import { isBoundToContainer, isTextElement } from "./typeChecks";
@@ -72,7 +73,7 @@ export const textWysiwyg = ({
     originalText: string;
   }) => void;
   getViewportCoords: (x: number, y: number) => [number, number];
-  element: ExcalidrawElement;
+  element: ExcalidrawTextElement;
   canvas: HTMLCanvasElement | null;
   excalidrawContainer: HTMLDivElement | null;
 }) => {
@@ -93,10 +94,9 @@ export const textWysiwyg = ({
     return false;
   };
   let originalContainerHeight: number;
-  let approxLineHeight = isTextElement(element)
-    ? getApproxLineHeight(getFontString(element))
-    : 0;
+  let approxLineHeight = getApproxLineHeight(getFontString(element));
 
+  const initialText = element.originalText;
   const updateWysiwygStyle = () => {
     const updatedElement = Scene.getScene(element)?.getElement(id);
     if (updatedElement && isTextElement(updatedElement)) {
@@ -123,9 +123,7 @@ export const textWysiwyg = ({
           height = editorHeight;
         }
         if (propertiesUpdated) {
-          approxLineHeight = isTextElement(updatedElement)
-            ? getApproxLineHeight(getFontString(updatedElement))
-            : 0;
+          approxLineHeight = getApproxLineHeight(getFontString(updatedElement));
 
           originalContainerHeight = container.height;
 
@@ -164,7 +162,7 @@ export const textWysiwyg = ({
       }
       const [viewportX, viewportY] = getViewportCoords(coordX, coordY);
       const { textAlign } = updatedElement;
-      editable.value = updatedElement.originalText || updatedElement.text;
+      editable.value = updatedElement.originalText;
       const lines = updatedElement.originalText.split("\n");
       const lineHeight = updatedElement.containerId
         ? approxLineHeight
@@ -217,6 +215,11 @@ export const textWysiwyg = ({
         maxWidth: `${maxWidth}px`,
         maxHeight: `${editorMaxHeight}px`,
       });
+      // For some reason updating font attribute doesn't set font family
+      // hence updating font family explicitly for test environment
+      if (isTestEnv()) {
+        editable.style.fontFamily = getFontFamilyString(updatedElement);
+      }
     }
   };
 
@@ -450,18 +453,23 @@ export const textWysiwyg = ({
           getFontString(updateElement),
           container.width,
         );
-        if (isTextElement(updateElement) && updateElement.containerId) {
+
+        if (updateElement.containerId) {
           const editorHeight = Number(editable.style.height.slice(0, -2));
           if (editable.value) {
-            mutateElement(updateElement, {
-              // vertically center align
-              y: container.y + container.height / 2 - editorHeight / 2,
-              height: editorHeight,
-              width: Number(editable.style.width.slice(0, -2)),
-              // preserve padding
-              x: container.x + BOUND_TEXT_PADDING,
-              angle: container.angle,
-            });
+            // Don't mutate if text is not updated
+            if (initialText !== editable.value) {
+              mutateElement(updateElement, {
+                // vertically center align
+                y: container.y + container.height / 2 - editorHeight / 2,
+                height: editorHeight,
+                width: Number(editable.style.width.slice(0, -2)),
+                // preserve padding
+                x: container.x + BOUND_TEXT_PADDING,
+                angle: container.angle,
+              });
+            }
+
             const boundTextElementId = getBoundTextElementId(container);
             if (!boundTextElementId || boundTextElementId !== element.id) {
               mutateElement(container, {