diff options
Diffstat (limited to 'packages/excalidraw/tests')
74 files changed, 70067 insertions, 0 deletions
diff --git a/packages/excalidraw/tests/App.test.tsx b/packages/excalidraw/tests/App.test.tsx new file mode 100644 index 0000000..ad62706 --- /dev/null +++ b/packages/excalidraw/tests/App.test.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import * as StaticScene from "../renderer/staticScene"; +import { reseed } from "../random"; +import { render, queryByTestId, unmountComponent } from "../tests/test-utils"; + +import { Excalidraw } from "../index"; +import { vi } from "vitest"; + +const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene"); + +describe("Test <App/>", () => { + beforeEach(async () => { + unmountComponent(); + localStorage.clear(); + renderStaticScene.mockClear(); + reseed(7); + }); + + it("should show error modal when using brave and measureText API is not working", async () => { + (global.navigator as any).brave = { + isBrave: { + name: "isBrave", + }, + }; + + const originalContext = global.HTMLCanvasElement.prototype.getContext("2d"); + //@ts-ignore + global.HTMLCanvasElement.prototype.getContext = (contextId) => { + return { + ...originalContext, + measureText: () => ({ + width: 0, + }), + }; + }; + + await render(<Excalidraw />); + expect( + queryByTestId( + document.querySelector(".excalidraw-modal-container")!, + "brave-measure-text-error", + ), + ).toMatchSnapshot(); + }); +}); diff --git a/packages/excalidraw/tests/MermaidToExcalidraw.test.tsx b/packages/excalidraw/tests/MermaidToExcalidraw.test.tsx new file mode 100644 index 0000000..05727a0 --- /dev/null +++ b/packages/excalidraw/tests/MermaidToExcalidraw.test.tsx @@ -0,0 +1,117 @@ +import React from "react"; +import { render, waitFor } from "./test-utils"; +import { Excalidraw } from "../index"; +import { expect } from "vitest"; +import { getTextEditor, updateTextEditor } from "./queries/dom"; +import { mockMermaidToExcalidraw } from "./helpers/mocks"; + +mockMermaidToExcalidraw({ + mockRef: true, + parseMermaidToExcalidraw: async (definition) => { + const firstLine = definition.split("\n")[0]; + return new Promise((resolve, reject) => { + if (firstLine === "flowchart TD") { + resolve({ + elements: [ + { + id: "Start", + type: "rectangle", + groupIds: [], + x: 0, + y: 0, + width: 69.703125, + height: 44, + strokeWidth: 2, + label: { + groupIds: [], + text: "Start", + fontSize: 20, + }, + link: null, + }, + { + id: "Stop", + type: "rectangle", + groupIds: [], + x: 2.7109375, + y: 94, + width: 64.28125, + height: 44, + strokeWidth: 2, + label: { + groupIds: [], + text: "Stop", + fontSize: 20, + }, + link: null, + }, + { + id: "Start_Stop", + type: "arrow", + groupIds: [], + x: 34.852, + y: 44, + strokeWidth: 2, + points: [ + [0, 0], + [0, 50], + ], + roundness: { + type: 2, + }, + start: { + id: "Start", + }, + end: { + id: "Stop", + }, + }, + ], + }); + } else { + reject(new Error("ERROR")); + } + }); + }, +}); + +describe("Test <MermaidToExcalidraw/>", () => { + beforeEach(async () => { + await render( + <Excalidraw + initialData={{ + appState: { + openDialog: { name: "ttd", tab: "mermaid" }, + }, + }} + />, + ); + }); + + it("should open mermaid popup when active tool is mermaid", async () => { + const dialog = document.querySelector(".ttd-dialog")!; + await waitFor(() => expect(dialog.querySelector("canvas")).not.toBeNull()); + expect(dialog.outerHTML).toMatchSnapshot(); + }); + + it("should show error in preview when mermaid library throws error", async () => { + const dialog = document.querySelector(".ttd-dialog")!; + + expect(dialog).not.toBeNull(); + + const selector = ".ttd-dialog-input"; + let editor = await getTextEditor(selector, true); + + expect(dialog.querySelector('[data-testid="mermaid-error"]')).toBeNull(); + + expect(editor.textContent).toMatchSnapshot(); + + updateTextEditor(editor, "flowchart TD1"); + editor = await getTextEditor(selector, false); + + expect(editor.textContent).toBe("flowchart TD1"); + expect( + dialog.querySelector('[data-testid="mermaid-error"]'), + ).toMatchInlineSnapshot("null"); + }); +}); diff --git a/packages/excalidraw/tests/__snapshots__/App.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/App.test.tsx.snap new file mode 100644 index 0000000..49c2fd0 --- /dev/null +++ b/packages/excalidraw/tests/__snapshots__/App.test.tsx.snap @@ -0,0 +1,50 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Test <App/> > should show error modal when using brave and measureText API is not working 1`] = ` +<div + data-testid="brave-measure-text-error" +> + <p> + Looks like you are using Brave browser with the + <span + style="font-weight: 600;" + > + Aggressively Block Fingerprinting + </span> + setting enabled. + </p> + <p> + This could result in breaking the + <span + style="font-weight: 600;" + > + Text Elements + </span> + in your drawings. + </p> + <p> + We strongly recommend disabling this setting. You can follow + <a + href="http://docs.excalidraw.com/docs/@excalidraw/excalidraw/faq#turning-off-aggresive-block-fingerprinting-in-brave-browser" + > + these steps + </a> + on how to do so. + </p> + <p> + If disabling this setting doesn't fix the display of text elements, please open an + <a + href="https://github.com/excalidraw/excalidraw/issues/new" + > + issue + </a> + on our GitHub, or write us on + <a + href="https://discord.gg/UexuTaE" + > + Discord + . + </a> + </p> +</div> +`; diff --git a/packages/excalidraw/tests/__snapshots__/MermaidToExcalidraw.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/MermaidToExcalidraw.test.tsx.snap new file mode 100644 index 0000000..2943aee --- /dev/null +++ b/packages/excalidraw/tests/__snapshots__/MermaidToExcalidraw.test.tsx.snap @@ -0,0 +1,19 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Test <MermaidToExcalidraw/> > should open mermaid popup when active tool is mermaid 1`] = ` +"<div class="Modal Dialog ttd-dialog" role="dialog" aria-modal="true" aria-labelledby="dialog-title" data-prevent-outside-click="true"><div class="Modal__background"></div><div class="Modal__content" style="--max-width: 1200px;" tabindex="0"><div class="Island"><div class="Dialog__content"><div dir="ltr" data-orientation="horizontal" class="ttd-dialog-tabs-root"><p class="dialog-mermaid-title">Mermaid to Excalidraw</p><div data-state="active" data-orientation="horizontal" role="tabpanel" aria-labelledby="radix-:r0:-trigger-mermaid" id="radix-:r0:-content-mermaid" tabindex="0" class="ttd-dialog-content" style=""><div class="ttd-dialog-desc">Currently only <a href="https://mermaid.js.org/syntax/flowchart.html">Flowchart</a>,<a href="https://mermaid.js.org/syntax/sequenceDiagram.html"> Sequence, </a> and <a href="https://mermaid.js.org/syntax/classDiagram.html">Class </a>Diagrams are supported. The other types will be rendered as image in Excalidraw.</div><div class="ttd-dialog-panels"><div class="ttd-dialog-panel"><div class="ttd-dialog-panel__header"><label>Mermaid Syntax</label></div><textarea class="ttd-dialog-input" placeholder="Write Mermaid diagram defintion here...">flowchart TD + A[Christmas] -->|Get money| B(Go shopping) + B --> C{Let me think} + C -->|One| D[Laptop] + C -->|Two| E[iPhone] + C -->|Three| F[Car]</textarea><div class="ttd-dialog-panel-button-container invisible" style="display: flex; align-items: center;"><button type="button" class="excalidraw-button ttd-dialog-panel-button"><div class=""></div></button></div></div><div class="ttd-dialog-panel"><div class="ttd-dialog-panel__header"><label>Preview</label></div><div class="ttd-dialog-output-wrapper"><div style="opacity: 1;" class="ttd-dialog-output-canvas-container"><canvas width="89" height="158" dir="ltr"></canvas></div></div><div class="ttd-dialog-panel-button-container" style="display: flex; align-items: center;"><button type="button" class="excalidraw-button ttd-dialog-panel-button"><div class="">Insert<span><svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 20 20" class="" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><g stroke-width="1.25"><path d="M4.16602 10H15.8327"></path><path d="M12.5 13.3333L15.8333 10"></path><path d="M12.5 6.66666L15.8333 9.99999"></path></g></svg></span></div></button><div class="ttd-dialog-submit-shortcut"><div class="ttd-dialog-submit-shortcut__key">Ctrl</div><div class="ttd-dialog-submit-shortcut__key">Enter</div></div></div></div></div></div></div></div></div></div></div>" +`; + +exports[`Test <MermaidToExcalidraw/> > should show error in preview when mermaid library throws error 1`] = ` +"flowchart TD + A[Christmas] -->|Get money| B(Go shopping) + B --> C{Let me think} + C -->|One| D[Laptop] + C -->|Two| E[iPhone] + C -->|Three| F[Car]" +`; diff --git a/packages/excalidraw/tests/__snapshots__/MobileMenu.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/MobileMenu.test.tsx.snap new file mode 100644 index 0000000..ad0c9f0 --- /dev/null +++ b/packages/excalidraw/tests/__snapshots__/MobileMenu.test.tsx.snap @@ -0,0 +1,257 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Test MobileMenu > should initialize with welcome screen and hide once user interacts 1`] = ` +<div + class="welcome-screen-center" +> + <div + class="welcome-screen-center__logo virgil welcome-screen-decor" + > + <div + class="ExcalidrawLogo is-small" + > + <svg + class="ExcalidrawLogo-icon" + fill="none" + viewBox="0 0 40 40" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M39.9 32.889a.326.326 0 0 0-.279-.056c-2.094-3.083-4.774-6-7.343-8.833l-.419-.472a.212.212 0 0 0-.056-.139.586.586 0 0 0-.167-.111l-.084-.083-.056-.056c-.084-.167-.28-.278-.475-.167-.782.39-1.507.973-2.206 1.528-.92.722-1.842 1.445-2.708 2.25a8.405 8.405 0 0 0-.977 1.028c-.14.194-.028.361.14.444-.615.611-1.23 1.223-1.843 1.861a.315.315 0 0 0-.084.223c0 .083.056.166.111.194l1.09.833v.028c1.535 1.528 4.244 3.611 7.12 5.861.418.334.865.667 1.284 1 .195.223.39.473.558.695.084.11.28.139.391.055.056.056.14.111.196.167a.398.398 0 0 0 .167.056.255.255 0 0 0 .224-.111.394.394 0 0 0 .055-.167c.029 0 .028.028.056.028a.318.318 0 0 0 .224-.084l5.082-5.528a.309.309 0 0 0 0-.444Zm-14.63-1.917a.485.485 0 0 0 .111.14c.586.5 1.2 1 1.843 1.555l-2.569-1.945-.251-.166c-.056-.028-.112-.084-.168-.111l-.195-.167.056-.056.055-.055.112-.111c.866-.861 2.346-2.306 3.1-3.028-.81.805-2.43 3.167-2.095 3.944Zm8.767 6.89-2.122-1.612a44.713 44.713 0 0 0-2.625-2.5c1.145.861 2.122 1.611 2.262 1.75 1.117.972 1.06.806 1.815 1.445l.921.666a1.06 1.06 0 0 1-.251.25Zm.558.416-.056-.028c.084-.055.168-.111.252-.194l-.196.222ZM1.089 5.75c.055.361.14.722.195 1.056.335 1.833.67 3.5 1.284 4.75l.252.944c.084.361.223.806.363.917 1.424 1.25 3.602 3.11 5.947 4.889a.295.295 0 0 0 .363 0s0 .027.028.027a.254.254 0 0 0 .196.084.318.318 0 0 0 .223-.084c2.988-3.305 5.221-6.027 6.813-8.305.112-.111.14-.278.14-.417.111-.111.195-.25.307-.333.111-.111.111-.306 0-.39l-.028-.027c0-.055-.028-.139-.084-.167-.698-.666-1.2-1.138-1.731-1.638-.922-.862-1.871-1.75-3.881-3.75l-.028-.028c-.028-.028-.056-.056-.112-.056-.558-.194-1.703-.389-3.127-.639C6.087 2.223 3.21 1.723.614.944c0 0-.168 0-.196.028l-.083.084c-.028.027-.056.055-.224.11h.056-.056c.028.167.028.278.084.473 0 .055.112.5.112.555l.782 3.556Zm15.496 3.278-.335-.334c.084.112.196.195.335.334Zm-3.546 4.666-.056.056c0-.028.028-.056.056-.056Zm-2.038-10c.168.167.866.834 1.033.973-.726-.334-2.54-1.167-3.379-1.445.838.167 1.983.334 2.346.472ZM1.424 2.306c.419.722.754 3.222 1.089 5.666-.196-.778-.335-1.555-.503-2.278-.251-1.277-.503-2.416-.838-3.416.056 0 .14 0 .252.028Zm-.168-.584c-.112 0-.223-.028-.307-.028 0-.027 0-.055-.028-.055.14 0 .223.028.335.083Zm-1.089.222c0-.027 0-.027 0 0ZM39.453 1.333c.028-.11-.558-.61-.363-.639.42-.027.42-.666 0-.666-.558.028-1.144.166-1.675.25-.977.194-1.982.389-2.96.61-2.205.473-4.383.973-6.561 1.557-.67.194-1.424.333-2.066.666-.224.111-.196.333-.084.472-.056.028-.084.028-.14.056-.195.028-.363.056-.558.083-.168.028-.252.167-.224.334 0 .027.028.083.028.11-1.173 1.556-2.485 3.195-3.909 4.945-1.396 1.611-2.876 3.306-4.356 5.056-4.719 5.5-10.052 11.75-15.943 17.25a.268.268 0 0 0 0 .389c.028.027.056.055.084.055-.084.084-.168.14-.252.222-.056.056-.084.111-.084.167a.605.605 0 0 0-.111.139c-.112.111-.112.305.028.389.111.11.307.11.39-.028.029-.028.029-.056.056-.056a.44.44 0 0 1 .615 0c.335.362.67.723.977 1.028l-.698-.583c-.112-.111-.307-.083-.39.028-.113.11-.085.305.027.389l7.427 6.194c.056.056.112.056.196.056s.14-.028.195-.084l.168-.166c.028.027.083.027.111.027.084 0 .14-.027.196-.083 10.052-10.055 18.15-17.639 27.42-24.417.083-.055.111-.166.111-.25.112 0 .196-.083.251-.194 1.704-5.194 2.039-9.806 2.15-12.083v-.028c0-.028.028-.056.028-.083.028-.056.028-.084.028-.084a1.626 1.626 0 0 0-.111-1.028ZM21.472 9.5c.446-.5.893-1.028 1.34-1.5-2.876 3.778-7.65 9.583-14.408 16.5 4.607-5.083 9.242-10.333 13.068-15ZM5.193 35.778h.084-.084Zm3.462 3.194c-.027-.028-.027-.028 0-.028v.028Zm4.16-3.583c.224-.25.448-.472.699-.722 0 0 0 .027.028.027-.252.223-.475.445-.726.695Zm1.146-1.111c.14-.14.279-.334.446-.5l.028-.028c1.648-1.694 3.351-3.389 5.082-5.111l.028-.028c.419-.333.921-.694 1.368-1.028a379.003 379.003 0 0 0-6.952 6.695ZM24.794 6.472c-.921 1.195-1.954 2.778-2.82 4.028-2.736 3.944-11.532 13.583-11.727 13.75a1976.983 1976.983 0 0 1-8.042 7.639l-.167.167c-.14-.167-.14-.417.028-.556C14.49 19.861 22.03 10.167 25.074 5.917c-.084.194-.14.36-.28.555Zm4.83 5.695c-1.116-.64-1.646-1.64-1.34-2.611l.084-.334c.028-.083.084-.194.14-.277.307-.5.754-.917 1.257-1.167.027 0 .055 0 .083-.028-.028-.056-.028-.139-.028-.222.028-.167.14-.278.335-.278.335 0 1.369.306 1.76.639.111.083.223.194.335.305.14.167.363.445.474.667.056.028.112.306.196.445.056.222.111.472.084.694-.028.028 0 .194-.028.194a2.668 2.668 0 0 1-.363 1.028c-.028.028-.028.056-.056.084l-.028.027c-.14.223-.335.417-.53.556-.643.444-1.369.583-2.095.389 0 0-.195-.084-.28-.111Zm8.154-.834a39.098 39.098 0 0 1-.893 3.167c0 .028-.028.083 0 .111-.056 0-.084.028-.14.056-2.206 1.61-4.356 3.305-6.506 5.028 1.843-1.64 3.686-3.306 5.613-4.945.558-.5.949-1.139 1.06-1.861l.28-1.667v-.055c.14-.334.67-.195.586.166Z" + fill="currentColor" + /> + </svg> + <svg + class="ExcalidrawLogo-text" + fill="none" + viewBox="0 0 450 55" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M429.27 96.74c2.47-1.39 4.78-3.02 6.83-4.95 1.43-1.35 2.73-2.86 3.81-4.51-.66.9-1.4 1.77-2.23 2.59-2.91 2.84-5.72 5.09-8.42 6.87h.01ZM343.6 69.36c.33 3.13.58 6.27.79 9.4.09 1.37.18 2.75.25 4.12-.12-4.46-.27-8.93-.5-13.39-.11-2.08-.24-4.16-.4-6.24-.06 1.79-.11 3.85-.13 6.11h-.01ZM378.47 98.34c.01-.37.07-1.13.01-6.51-.11 1.9-.22 3.81-.31 5.71-.07 1.42-.22 2.91-.16 4.35.39.03.78.07 1.17.1-.92-.85-.76-2.01-.72-3.66l.01.01ZM344.09 86.12c-.09-2.41-.22-4.83-.39-7.24v12.21c.15-.05.32-.09.47-.14.05-1.61-.03-3.23-.09-4.83h.01ZM440.69 66.79c-.22-.34-.45-.67-.69-.99-3.71-4.87-9.91-7.14-15.65-8.55-1.05-.26-2.12-.49-3.18-.71 2.29.59 4.48 1.26 6.64 2.02 7.19 2.54 10.57 5.41 12.88 8.23ZM305.09 72.46l1.2 3.6c.84 2.53 1.67 5.06 2.46 7.61.24.78.5 1.57.73 2.36.22-.04.44-.08.67-.12a776.9 776.9 0 0 1-5.01-13.57c-.02.04-.03.09-.05.13v-.01ZM345.49 90.25v.31c1.48-.42 3.05-.83 4.66-1.2-1.56.25-3.12.52-4.66.89ZM371.02 90.22c0-.57-.04-1.14-.11-1.71-.06-.02-.12-.04-.19-.05-.21-.05-.43-.08-.65-.11.42.16.74.88.95 1.87ZM398.93 54.23c-.13 0-.27-.01-.4-.02l.03.4c.11-.15.23-.27.37-.38ZM401.57 62.28v-.15c-1.22-.24-2.86-.61-3.23-1.25-.09-.15-.18-.51-.27-.98-.09.37-.2.73-.33 1.09 1.24.56 2.52.98 3.83 1.29ZM421.73 88.68c-2.97 1.65-6.28 3.12-9.69 3.68v.18c4.72-.14 11.63-3.85 16.33-8.38-2.04 1.75-4.33 3.24-6.63 4.53l-.01-.01ZM411.28 80.92c-.05-1.2-.09-2.4-.15-3.6-.21 5.66-.46 11.38-.47 14.51.24-.02.48-.04.71-.07.15-3.61.05-7.23-.09-10.83v-.01Z" + transform="translate(-144.023 -51.76)" + /> + <path + d="M425.38 67.41c-3.5-1.45-7.19-2.57-14.06-3.62.09 1.97.06 4.88-.03 8.12.03.04.06.09.06.15.19 1.36.28 2.73.37 4.1.25 3.77.39 7.55.41 11.33 0 1.38-.01 2.76-.07 4.13 1.4-.25 2.78-.65 4.12-1.15 4.07-1.5 7.94-3.78 11.28-6.54 2.33-1.92 5.13-4.49 5.88-7.58.63-3.53-2.45-6.68-7.97-8.96l.01.02ZM411.35 92.53v-.06l-.34.03c.11.01.22.03.34.03ZM314.26 64.06c-.23-.59-.47-1.17-.7-1.75.57 1.62 1.11 3.25 1.6 4.9l.15.54 2.35 6.05c.32.82.66 1.64.98 2.46-1.38-4.1-2.83-8.17-4.39-12.2h.01ZM156.82 103.07c-.18.13-.38.23-.58.33 1.32-.03 2.66-.2 3.93-.34.86-.09 1.72-.22 2.58-.33-2.12.1-4.12.17-5.94.34h.01ZM210.14 68.88s.03.04.05.07c.18-.31.39-.64.58-.96-.21.3-.42.6-.64.89h.01ZM201.65 82.8c-.5.77-1.02 1.56-1.49 2.37 1.11-1.55 2.21-3.1 3.2-4.59-.23.23-.49.51-.75.79-.32.47-.65.95-.96 1.43ZM194.03 98.66c-.33-.4-.65-.84-1.05-1.17-.24-.2-.07-.49.17-.56-.23-.26-.42-.5-.63-.75 1.51-2.55 3.93-5.87 6.4-9.28-.17-.08-.29-.28-.2-.49.04-.09.09-.17.13-.26-1.21 1.78-2.42 3.55-3.61 5.33-.87 1.31-1.74 2.64-2.54 4-.29.5-.63 1.04-.87 1.61.81.65 1.63 1.27 2.47 1.88-.09-.11-.18-.21-.27-.32v.01ZM307.79 82.93c-1-3.17-2.05-6.32-3.1-9.48-1.62 4.08-3.69 9.17-6.16 15.19 3.32-1.04 6.77-1.87 10.27-2.5-.32-1.08-.67-2.15-1.01-3.21ZM149.5 80.7c.05-1.71.04-3.43 0-5.14-.1 2.26-.16 4.51-.22 6.77-.02.73-.03 1.46-.04 2.19.14-1.27.2-2.55.24-3.82h.02ZM228.98 98.3c.39 1.25.91 3.03.94 3.91.06-.03.12-.07.17-.1.08-1.29-.55-2.65-1.11-3.81ZM307.72 53.36c.81.5 1.53 1.04 2.07 1.49-.38-.8-.78-1.58-1.21-2.35-.17.03-.34.06-.51.11-.43.12-.86.26-1.29.41.35-.01.53.1.94.34ZM283.69 96.14c3.91-7.25 6.89-13.35 8.88-18.15l1.1-2.66c-1.27 2.64-2.56 5.27-3.83 7.9-1.53 3.15-3.06 6.31-4.58 9.47-.87 1.81-1.76 3.62-2.54 5.47.04.02.07.04.11.07.05.05.1.09.15.14.05-.73.27-1.48.71-2.24ZM289.92 103.23s-.04.01-.05.03c0-.02.04-.03.05-.04.05-.05.11-.1.16-.15l.21-.21c-.55 0-1.5-.27-2.55-.72.4.26.8.51 1.22.74.24.13.48.26.73.37.05.02.1.03.14.05a.27.27 0 0 1 .08-.07h.01ZM269.23 68.49c-.39-.19-.82-.48-1.33-.87-3.06-1.56-6.31-2.78-9.36-2.35-3.5.49-5.7 1.11-7.74 2.44 5.71-2.6 12.82-2.07 18.44.79l-.01-.01ZM177.87 53.69l1.06.03c-.96-.22-2-.25-2.89-.3-4.95-.26-9.99.33-14.86 1.19-2.44.43-4.88.95-7.28 1.59 9.09-1.76 15.69-2.77 23.97-2.51ZM219.85 55.51c-.18.12-.36.27-.56.45-.45.53-.86 1.11-1.26 1.66-1.91 2.61-3.71 5.31-5.57 7.95l-.12.18 8.05-10.11c-.18-.05-.36-.1-.55-.13h.01ZM510.71 54.1c.12-.15.29-.3.53-.45.69-.4 3.72-.63 5.87-.74-.36-.02-.73-.04-1.09-.05-1.84-.03-3.67.09-5.49.35.05.3.12.59.18.88v.01ZM510.76 86.02c1.37-3.07 2.49-6.27 3.57-9.46.55-1.64 1.12-3.3 1.6-4.97-1.59 4.01-3.67 9.14-6.2 15.3.24-.08.5-.14.74-.22.1-.22.19-.44.29-.65ZM566.95 75.76c.11-.02.23.03.31.11-.05-.13-.09-.26-.14-.39-.05.09-.11.18-.17.28ZM511.33 86.41c3.08-.89 6.24-1.62 9.46-2.14-1.51-3.98-2.98-7.96-4.39-11.87-.05.15-.09.31-.14.46-1.02 3.32-2.15 6.61-3.39 9.85-.48 1.25-.98 2.49-1.53 3.7h-.01ZM578.24 74.45c.11-.44.23-.87.35-1.31-.31.7-.64 1.39-.97 2.08.09.21.19.4.28.61.12-.46.23-.92.35-1.38h-.01ZM520.62 53.11c-.09 0-.18-.01-.28-.02.38.34.29 1.08.93 2.53l6.65 17.15c2.2 5.68 4.69 11.36 7.41 16.87l1.06 2.17c-2.95-7.05-5.92-14.08-8.87-21.13-1.58-3.79-3.16-7.59-4.7-11.4-.78-1.92-1.73-3.89-2.25-5.91-.03-.1 0-.19.04-.26h.01ZM578.78 77.87c1.45-5.77 3.07-10.43 3.58-13.36.05-.34.16-.88.31-1.55-.67 1.79-1.37 3.56-2.08 5.33-.12.43-.23.86-.35 1.29-.65 2.43-1.29 4.86-1.9 7.3.14.33.29.65.43 1l.01-.01ZM545.3 94.66c.02-.44.03-.83.05-1.12.02-1.01.05-2.02.11-3.02.03-6.66-.46-14.33-1.46-22.8-.13-.42-.27-1.24-.56-2.89 0-.02 0-.04-.01-.06.62 6.61.95 13.25 1.32 19.87.17 3.08.33 6.16.52 9.23.02.25.03.52.04.78l-.01.01ZM580.77 102.81c.13.2.27.38.37.49.27-.11.53-.22.8-.32-.43.09-.82.05-1.17-.16v-.01ZM530.48 104.07h.33c-.36-.13-.71-.32-1.04-.56.14.24.3.47.45.7.06-.08.14-.13.26-.13v-.01ZM542.63 58.82c.06.23.11.47.15.71.14-.33.36-.62.7-.86-.28.05-.57.11-.85.15ZM583.81 57.87c.15-.7.29-1.41.42-2.11-.14.45-.28.9-.42 1.34-.46 1.44-.89 2.89-1.31 4.34.44-1.19.88-2.37 1.31-3.57ZM523.62 91.48c-4.66 1.17-9.05 2.89-14.02 5.27 4.65-1.84 9.48-3.29 14.28-4.63-.09-.22-.17-.41-.26-.64ZM460.64 78.3c-.04-2.9-.11-5.81-.28-8.71-.1-1.68-.17-3.43-.5-5.09-.07.02-.14.03-.2.05.3 6.54.45 12.17.51 17.12.17-.07.34-.14.51-.2 0-1.06-.01-2.11-.03-3.17h-.01ZM470.63 63.24c-3.38-.26-6.81.32-10.1 1.1.41 2.01.47 4.14.57 6.18.18 3.55.25 7.11.27 10.67 3.31-1.38 6.5-3.12 9.3-5.35 1.96-1.56 3.86-3.41 5.02-5.66.73-1.41 1.19-3.22.26-4.65-1.09-1.7-3.46-2.14-5.32-2.29ZM460.29 63.68c1-.24 2.01-.46 3.04-.65-1.15.16-2.37.38-3.71.69v.13c.07-.02.15-.04.22-.05.11-.13.3-.18.45-.11v-.01ZM457.24 100.96c.43-.03.86-.07 1.29-.11.14-.49.27-.99.38-1.49-.44.7-1 1.23-1.67 1.6ZM482.88 104.98c-.18.23-.36.38-.55.47.14.09.27.19.4.28a70.76 70.76 0 0 0 4.37-4.63c.76-.89 1.52-1.81 2.19-2.77-.3-.27-.61-.53-.92-.79-.07 1.94-4.62 6.32-5.49 7.45v-.01Z" + transform="translate(-144.023 -51.76)" + /> + <path + d="M474.36 63.31c-.4-.16-.84-.27-1.29-.37 1.56.42 3.08 1.22 3.76 2.74.62 1.4.32 2.95-.28 4.32.7-1.22.94-2.34.74-3.47-.24-1.33-1.19-2.54-2.93-3.21v-.01ZM477.34 89.18c-1.2-.81-2.4-1.62-3.6-2.42-.14.1-.26.19-.4.29 1.4.67 2.73 1.39 4 2.13ZM465.88 93.85c.37.25.74.5 1.1.75.46.32.92.65 1.38.97-1.57-1.2-2.01-1.61-2.49-1.72h.01ZM574.92 90.06c-2.28-5.21-4.93-11.13-5.67-12.26-.1-.15-1.57-3.01-1.63-3.08 0 0-.01.02-.02.02.4 1.37 1.09 2.69 1.65 3.99 2.14 4.95 4.36 9.86 6.67 14.73.6 1.26 1.21 2.52 1.83 3.78-.75-2.01-1.64-4.45-2.83-7.18ZM448.73 65.29c.1.2.22.38.34.57.22-.02.43-.06.65-.08v-.08c-.14-.05-.25 0-.99-.41ZM460.16 94.81c-.02.31-.06.59-.1.89-.03 1.71-.33 3.43-.79 5.07.15-.02.3-.03.45-.05.01-.04.02-.08.03-.11.09-.34.15-.69.2-1.03.17-1.07.25-2.16.33-3.24.05-.69.08-1.39.12-2.08-.27.1-.27.26-.24.57v-.02Z" + transform="translate(-144.023 -51.76)" + /> + <path + d="m328.67 98.12-3.22-6.58c-1.29-2.63-2.53-5.29-3.72-7.97-.25-.85-.52-1.69-.79-2.53-.81-2.57-1.67-5.12-2.55-7.67-1.92-5.53-3.9-11.08-6.32-16.41-.72-1.58-1.46-3.44-2.63-4.79-.03-.17-.16-.29-.34-.36a.282.282 0 0 0-.23-.04c-.06-.01-.12 0-.18.01-.74.06-1.5.38-2.19.61-2.22.77-4.4 1.64-6.63 2.38-.03-.08-.06-.16-.09-.25-.15-.42-.82-.24-.67.19.03.09.07.19.1.28l-.18.06c-.36.11-.28.6 0 .68.18 1.18.63 2.36.98 3.49.03.09.06.17.08.26-.08.23-.17.46-.24.64-.37.98-.79 1.94-1.21 2.9-1.27 2.89-2.62 5.75-3.98 8.6-3.18 6.67-6.44 13.31-9.64 19.97-1.08 2.25-2.2 4.5-3.15 6.81-.13.32.24.5.5.37 1.34 1.33 2.84 2.5 4.4 3.57.65.44 1.31.87 2.01 1.24.4.22.86.48 1.33.5.24.01.35-.19.33-.37.11-.1.21-.21.28-.28.41-.41.81-.84 1.2-1.26.85-.92 1.69-1.87 2.5-2.84 6.31-2.34 12.6-4.31 18.71-5.84 2.14 5.3 3.43 8.43 3.97 9.58.55 1.05 1.15 1.88 1.82 2.52 1.32.56 6.96-.03 9.23-1.96.87-1.28 1.19-2.67.93-4.15-.09-.5-.22-.95-.4-1.33l-.01-.03Zm-20.09-45.61c.43.77.83 1.56 1.21 2.35-.54-.45-1.27-.99-2.07-1.49-.42-.24-.6-.35-.94-.34.43-.15.85-.29 1.29-.41.17-.05.34-.08.51-.11Zm-25.86 45.66c.78-1.85 1.67-3.66 2.54-5.47 1.51-3.16 3.05-6.31 4.58-9.47 1.28-2.63 2.56-5.26 3.83-7.9l-1.1 2.66c-1.99 4.79-4.97 10.9-8.88 18.15-.43.76-.66 1.51-.71 2.24-.05-.05-.1-.09-.15-.14a.259.259 0 0 0-.11-.07Zm6.24 4.71c-.42-.23-.82-.48-1.22-.74 1.05.45 2 .72 2.55.72l-.21.21c-.05.05-.11.1-.16.15-.01.01-.04.03-.05.04 0-.02.03-.02.05-.03a.27.27 0 0 0-.08.07c-.05-.02-.1-.03-.14-.05-.25-.1-.49-.24-.73-.37h-.01Zm15.73-29.43c1.05 3.15 2.1 6.31 3.1 9.48.34 1.06.69 2.13 1.01 3.21-3.5.63-6.95 1.46-10.27 2.5 2.48-6.03 4.54-11.11 6.16-15.19Zm4.79 12.57c-.23-.79-.49-1.58-.73-2.36-.79-2.54-1.63-5.08-2.46-7.61l-1.2-3.6c.02-.04.04-.09.05-.13 1.6 4.45 3.28 9 5.01 13.57l-.67.12v.01Zm5.83-18.27-.15-.54c-.49-1.64-1.03-3.28-1.6-4.9.23.58.47 1.17.7 1.75 1.56 4.03 3.01 8.1 4.39 12.2-.33-.82-.67-1.64-.98-2.46l-2.35-6.05h-.01ZM390.43 79.37c-.13-10.43-.22-17.5-.24-19.97-.24-1.6.21-2.88-.65-3.65-.14-.13-.32-.23-.52-.32h.03c.45 0 .45-.69 0-.7-1.75-.03-3.5-.04-5.25-.14-1.38-.08-2.76-.21-4.15-.31-.07 0-.12.01-.17.04-.21-.07-.47.03-.45.31l.03.45c-.11.14-.19.3-.22.5-.21 1.26-.32 13.67-.36 23.59-.32 5.79-.67 11.57-.97 17.36-.09 1.73-.29 3.54-.21 5.3-.39.02-.38.64.04.69v.12c.05.44.74.45.7 0v-.06c1.1.09 2.2.21 3.3.3 1.14.19 2.44.2 3.29.17 1.73-.05 2.92-.05 3.8-.37.45-.05.9-.11 1.35-.17.44-.06.25-.73-.19-.67h-.01c.24-.32.45-.72.62-1.25.66-1.84.41-6.36.34-11.33l-.13-9.9.02.01Zm-12.26 18.17c.09-1.91.2-3.81.31-5.71.06 5.38 0 6.14-.01 6.51-.05 1.65-.21 2.81.72 3.66-.39-.04-.78-.07-1.17-.1-.06-1.44.09-2.93.16-4.35l-.01-.01ZM588.97 53.85c-2.06-.25-3.17-.51-3.76-.6a.3.3 0 0 1 .04-.08c.22-.39-.39-.75-.6-.35-.56 1.02-.9 2.19-1.26 3.29-.61 1.88-1.17 3.78-1.72 5.68-.63 2.19-1.24 4.39-1.83 6.59-.81 2.03-1.67 4.05-2.61 6.03-1.7-3.64-3.11-6.04-4.03-7.57-2.26-3.74-2.85-5.48-3.57-6.08l.31-.09c.43-.12.25-.8-.19-.67-1.06.3-2.12.6-3.17.95-.93.32-1.85.69-2.76 1.07-.13.05-.19.16-.22.27-.04.02-.08.05-.11.07-.04-.06-.07-.12-.11-.18a.354.354 0 0 0-.48-.12c-.16.09-.22.32-.13.48l.33.54c0 .09.02.18.06.28.51 1.16.78 1.38.72 1.47-2.42 3.44-5.41 7.86-6.2 9.1-1.27 1.97-2.01 3.14-2.45 3.84l-.91-6.56-.43-4.1c-.19-1.85-.37-3.23-.53-4.13-.19-1.1-.3-2.15-.45-3.16-.2-1.36-.29-2.06-.47-2.42h.04c.45.02.45-.68 0-.7-3.43-.16-6.81.94-10.17 1.48-.24-.22-.73-.04-.58.32.24.59.33 1.25.43 1.87.17 1.06.29 2.13.4 3.2.32 3.09.53 6.2.74 9.3.44 6.75.77 13.51 1.17 20.26.11 1.95.13 3.96.46 5.89.05.3.37.31.55.14.74 1.71 2.87 1.27 6.13 1.27 1.34 0 2.39.04 2.99-.11.02.32.48.53.63.18 3.61-8.26 7.41-16.46 12.05-24.2.03-.05.04-.1.05-.15.3.73.64 1.45.94 2.16.97 2.26 1.97 4.52 2.98 6.76 2.26 5.03 4.54 10.07 7.09 14.96.47.9.94 1.79 1.47 2.65.2.32.4.67.66.96-.18.25 0 .68.34.54.91-.38 1.82-.75 2.76-1.07 1.04-.35 2.11-.65 3.17-.95.39-.11.28-.66-.07-.68.62-.4.95-.96.87-1.91-.3-3.34.72-7.47.86-8.52l2.14-11.43c1.75-10.74 3.13-17.51 3.23-20.86.02-.49.08-2.84.13-3.24.17-1.25.48-1-4.96-1.65l.03-.02Zm-46.19 5.67c-.04-.24-.09-.48-.15-.71l.85-.15c-.34.24-.56.53-.7.86Zm1.95 25.12c-.36-6.63-.7-13.26-1.32-19.87 0 .02 0 .04.01.06.29 1.65.44 2.47.56 2.89 1 8.46 1.5 16.14 1.46 22.8-.06.99-.1 2-.11 3.02-.01.29-.03.68-.05 1.12-.01-.26-.03-.53-.04-.78-.19-3.08-.35-6.16-.52-9.23l.01-.01Zm36.4 18.66c-.11-.11-.24-.29-.37-.49.35.21.74.26 1.17.16-.27.11-.53.22-.8.32v.01Zm-.89-33.72c.12-.43.23-.86.35-1.29.71-1.77 1.41-3.55 2.08-5.33-.15.68-.26 1.22-.31 1.55-.5 2.94-2.13 7.59-3.58 13.36-.15-.35-.29-.66-.43-1 .61-2.44 1.25-4.87 1.9-7.3l-.01.01Zm3.56-12.48c.14-.44.28-.89.42-1.34-.13.7-.27 1.41-.42 2.11-.43 1.19-.86 2.38-1.31 3.57.42-1.45.85-2.9 1.31-4.34Zm-5.22 16.05c-.11.44-.23.87-.35 1.31-.12.46-.23.92-.35 1.38-.1-.22-.19-.4-.28-.61.34-.69.66-1.38.97-2.08h.01Zm-11.64 2.62c.06-.1.12-.19.17-.28.05.13.09.26.14.39a.398.398 0 0 0-.31-.11Zm2.3 2.98c-.56-1.3-1.25-2.63-1.65-3.99 0 0 .01-.02.02-.02.06.08 1.52 2.93 1.63 3.08.73 1.13 3.38 7.04 5.67 12.26 1.2 2.73 2.08 5.17 2.83 7.18-.62-1.25-1.23-2.51-1.83-3.78-2.31-4.87-4.53-9.78-6.67-14.73ZM275.92 87.03c-1.06-2.18-1.13-3.45-2.44-2.93-1.52.57-2.94 1.3-4.5 2.1-1.4.72-2.68 1.44-3.92 2.12.01-.25-.24-.5-.51-.34-4.8 2.93-12.41 4.7-17.28 1.31-1.98-1.77-3.32-4.15-3.97-5.78-.29-.95-.49-1.94-.63-2.93-.14-3.34 1.58-6.53 3.9-9.12.8-.79 1.68-1.51 2.66-2.12 3.7-2.3 8.22-3.07 12.51-2.51 2.71.35 5.32 1.24 7.71 2.55.39.22.75-.39.35-.6-.18-.1-.37-.18-.55-.27.56.27 1.03.33 1.51.19l-.48.39c-.15.11-.23.3-.13.48.09.15.33.24.48.13 1.3-.97 2.46-2.09 3.45-3.37.37-.29.64-.6.65-.97v-.02c.08-.33-.03-.7-.21-1.08-.31-.87-.98-2.01-2.19-3.26-2.43-2.52-3.79-3.45-5.68-4.26-1.14-.49-3.12-1.06-4.42-1.23-3.28-.42-10.64-1.21-18.18 4.11-7.74 5.46-11.94 12.3-12.23 20.61-.08 2.06.04 3.98.34 5.71.74 4.18 2.57 8 5.44 11.34 4.26 4.99 9.76 7.52 16.34 7.52 4.85 0 9.69-1.77 14.89-4.62.23-.12.45-.23.68-.35 2.19-1.1 4.37-2.23 6.46-3.5.49-.3 1.03-.61 1.5-.98 1.47-.87 1.11-1.12.49-2.95-.39-1.14-.76-2.7-2.06-5.36l.02-.01Zm-17.38-21.76c3.05-.42 6.31.79 9.36 2.35.51.39.94.68 1.33.87-5.61-2.86-12.72-3.39-18.44-.79 2.05-1.33 4.24-1.95 7.74-2.44l.01.01ZM443.67 72.67c-.4-2.2-1.15-4.33-2.37-6.22-1.49-2.32-3.58-4.19-5.91-5.64-6.17-3.81-13.75-5.11-20.83-6.01-3.23-.41-6.47-.69-9.72-.92l-1.39-.12c-.85-.07-1.52-.1-2.05-.1-1.08-.06-2.17-.12-3.25-.17-.08 0-.14.02-.19.05-.1.05-.18.14-.16.3.27 2.55-.01 5.12-.92 7.52-.15.38.4.56.62.28 1.32.59 2.68 1.05 4.08 1.37 0 2.78-.14 7.58-.33 12.91 0 0 0 .02-.01.03-.61 3.66-.79 7.42-1 11.12-.23 4.01-.43 8.03-.44 12.05 0 .64 0 1.28.03 1.93.02.31 0 .68.15.96.06.11.14.16.24.17-.2.17-.21.54.11.59 3.83.67 7.78.71 11.68.25 2.3-.19 4.87-.65 7.65-1.56 1.85-.54 3.67-1.18 5.43-1.91 7.2-3.02 14.31-8.07 17.35-15.53.76-1.86 1.17-3.8 1.31-5.75.3-1.93.28-3.82-.09-5.58l.01-.02Zm-19.32-15.42c5.74 1.41 11.94 3.68 15.65 8.55.25.32.47.65.69.99-2.3-2.82-5.68-5.69-12.88-8.23-2.16-.76-4.35-1.43-6.64-2.02 1.06.21 2.13.45 3.18.71Zm-25.82-3.04c.13 0 .27.01.4.02-.14.1-.26.23-.37.38 0-.13-.02-.26-.03-.4Zm34.82 22.17c-.75 3.09-3.55 5.66-5.88 7.58-3.35 2.76-7.21 5.03-11.28 6.54-1.33.49-2.71.9-4.12 1.15.06-1.38.08-2.76.07-4.13-.02-3.78-.16-7.56-.41-11.33-.09-1.37-.18-2.74-.37-4.1 0-.06-.03-.11-.06-.15.09-3.25.12-6.16.03-8.12 6.86 1.05 10.56 2.17 14.06 3.62 5.52 2.28 8.59 5.44 7.97 8.96l-.01-.02Zm-22 16.15c-.12 0-.23-.02-.34-.03l.34-.03v.06Zm-.69-.7c0-3.13.26-8.84.47-14.51.06 1.2.11 2.41.15 3.6.15 3.6.25 7.23.09 10.83-.24.03-.48.05-.71.07v.01Zm-12.33-30.94c.37.63 2.01 1.01 3.23 1.25v.15c-1.31-.31-2.59-.73-3.83-1.29.12-.36.23-.72.33-1.09.08.48.18.84.27.98Zm13.7 31.65v-.18c3.41-.56 6.71-2.02 9.69-3.68 2.31-1.28 4.59-2.78 6.63-4.53-4.69 4.53-11.61 8.24-16.33 8.38l.01.01Zm24.07-.75c-2.05 1.93-4.37 3.56-6.83 4.95 2.7-1.78 5.52-4.03 8.42-6.87.82-.82 1.56-1.69 2.23-2.59-1.08 1.65-2.38 3.16-3.81 4.51h-.01ZM187.16 92.14c-.79-2.47-2.1-7.12-3.1-6.87-.19-.01-2.09.77-4.08 1.54-3.06 1.18-5.91 2.13-10.09 2.82-2.74.42-5.87 1.01-10.61 1.06.04-3.34.05-6.01.05-7.99 7.97-.65 12.33-2.11 16.37-3.55 1.11-.39 2.69-1.01 2.63-1.8-.08-.35-.55-1.39-1.17-2.61-.47-1.16-.98-2.31-1.61-3.38-.42-.71-1.04-1.69-1.86-2.06-.11-.08-.22-.13-.29-.12-.02 0-.04 0-.07.01-.19-.04-.39-.05-.6-.01-.17.03-.24.15-.25.28-.04.02-.09.04-.14.05-4.33 1.48-8.85 2.33-13.24 3.61a499.1 499.1 0 0 0-.31-8.19c4.51-.99 8.88-1.38 13.11-1.82 3.68-.38 6.28.12 7.47.34.59.11.9.16 1.16.18h.1c-.1.37.44.66.62.28.02-.04.03-.08.05-.13.15.2.53.22.62-.1.17-.58.19-1.21.21-1.81v-.36c.03-.15.05-.3.07-.45.52-2.47.33-5.09-.64-7.44-.11-.27-.44-.28-.6-.14-.08-.21-.15-.42-.24-.62-.19-.41-.79-.05-.6.35.03.07.05.15.09.22-.98-.42-2.15-.54-3.17-.63-2.17-.19-4.37-.14-6.54 0-5.7.35-11.4 1.3-16.91 2.79-2.08.56-4.13 1.22-6.14 2-4.54 1.05-3.79 1.51-2.17 6.07.18.51.46 1.68.54 1.94.82 2.47 1.08 2.13 3.1 2.13s0 .05 0 .08h.52c-.48 2.66-.51 5.45-.62 8.13-.15 3.48-.22 6.96-.28 10.45 0 .41-.01.82-.02 1.23-.16.29-.33.57-.51.85-.05.38-.09.77-.14 1.18-.42 3.52-.59 6.48-.52 8.8v.34c.02.47.05.76.06.87.16 1.57-.26 3.47 1.35 3.79 1.61.32 3.5.55 4.85.55.11 0 .22-.02.33-.02 1.79.24 3.67.05 5.45-.12 2.85-.28 5.69-.7 8.51-1.19 3.03-.53 6.05-1.14 9.04-1.86 2.4-.58 4.82-1.19 7.13-2.06.51-.19 1.73-.57 2.46-1.14 1.81-.68 2.18-1 1.57-2.67-.23-.62-.48-1.49-.91-2.78l-.03-.02Zm-11.12-38.71c.89.05 1.93.08 2.89.3-.33 0-.68-.02-1.06-.03-8.28-.26-14.88.75-23.97 2.51 2.41-.64 4.85-1.16 7.28-1.59 4.87-.86 9.91-1.45 14.86-1.19Zm-26.53 22.13c.03 1.71.04 3.43 0 5.14-.04 1.27-.11 2.55-.24 3.82 0-.73.02-1.46.04-2.19.05-2.26.12-4.51.22-6.77h-.02Zm6.73 27.85c.2-.1.4-.21.58-.33 1.82-.17 3.82-.24 5.94-.34-.86.11-1.72.24-2.58.33-1.27.14-2.61.31-3.93.34h-.01ZM534.48 85.44c-3.52-8.38-7.07-16.75-10.5-25.17-.63-1.54-1.25-3.09-1.86-4.65-.31-.8-.65-1.6-.87-2.43-.04-.17-.17-.24-.31-.25.1-.2 0-.51-.29-.53-1.59-.08-3.18-.22-4.78-.25-1.96-.03-3.91.13-5.84.42-.31.05-.31.38-.13.56-.03.06-.05.14-.04.22.23 1.54.63 3.06 1.16 4.53.13.35.27.7.41 1.06l-2.68 6.18c-.11.03-.2.09-.25.22-.67 1.9-1.52 3.73-2.34 5.56a536.85 536.85 0 0 1-3.9 8.45c-2.64 5.64-5.34 11.25-7.91 16.93-.44.97-.88 1.94-1.29 2.93-.2.48-.47 1-.55 1.52v.05c-.02.12.02.26.16.34 1.19.73 2.41 1.41 3.66 2.05 1.2.62 2.45 1.25 3.76 1.61.43.12.62-.55.19-.67-1.13-.31-2.2-.83-3.24-1.36 1.09.36 2.1.69 2.75.93 2.82 1.01 2.38 1.1 4.3-3.75 2.1-1.09 4.34-1.96 6.53-2.79 4.35-1.64 8.8-3.03 13.27-4.29.82 2.01 1.77 3.97 2.72 5.92.35.83.62 1.45.79 1.82.22.42.45.8.69 1.15.17.33.33.67.5 1 .42.8.84 1.63 1.4 2.35.23.29.6 0 .55-.31 1.53-.02 3.06-.07 4.58-.27.92-.12 1.82-.32 2.71-.54 1.39-.27 3.85-1.11 3.74-1.42-.67-1.96-1.55-3.87-2.34-5.78-1.57-3.78-3.16-7.56-4.75-11.33v-.01Zm-11.65-26.16c1.54 3.81 3.12 7.6 4.7 11.4 2.94 7.05 5.91 14.09 8.87 21.13l-1.06-2.17c-2.71-5.51-5.2-11.19-7.41-16.87l-6.65-17.15c-.65-1.45-.55-2.19-.93-2.53.09 0 .18.01.28.02a.29.29 0 0 0-.04.26c.52 2.02 1.47 3.98 2.25 5.91h-.01Zm-6.58 13.58c.05-.15.09-.31.14-.46 1.41 3.92 2.88 7.9 4.39 11.87-3.22.52-6.38 1.25-9.46 2.14.55-1.22 1.05-2.46 1.53-3.7 1.24-3.24 2.37-6.53 3.39-9.85h.01Zm-.23-20c.36 0 .73.03 1.09.05-2.15.1-5.18.33-5.87.74-.24.15-.41.3-.53.45-.06-.29-.13-.58-.18-.88 1.82-.26 3.65-.39 5.49-.35v-.01Zm-.09 18.72c-.49 1.67-1.05 3.33-1.6 4.97-1.07 3.19-2.19 6.38-3.57 9.46-.09.21-.19.43-.29.65-.25.07-.5.14-.74.22 2.53-6.16 4.61-11.29 6.2-15.3Zm-6.34 25.16c4.97-2.38 9.37-4.1 14.02-5.27l.26.64c-4.8 1.35-9.63 2.8-14.28 4.63Zm20.17 6.76c.33.23.68.42 1.04.56h-.33c-.12 0-.21.06-.26.13-.15-.23-.31-.45-.45-.7v.01ZM226.57 91.75c-3.55-4.74-6.68-9.11-9.31-12.99 9.2-15.25 10.05-17.81 10.35-18.38.17-.34 1.09-2.27.64-2.53-1.13-.65-1.03-.65-2.97-1.71-1.19-.65-3.04-1.61-4.53-2.12-1.71-.59-1.24-.36-3 2.77-.06.1-.11.2-.17.3-.75 1.02-1.48 2.05-2.2 3.09-1.88 2.71-3.73 5.45-5.69 8.1-3.68-4.91-6.88-8.76-9.51-11.43-.15-.15-.3-.29-.46-.42-1.27-1.28-7.24 3.53-7.93 5.58-.09.09-.19.16-.28.25-.27.26.03.64.33.58.19.65.5 1.29.94 1.91 3.85 5.06 7.19 9.76 9.94 14-1.23 2.61-3.06 5-4.67 7.38l-2.28 3.33c-.5.66-.93 1.23-1.29 1.69-.67.93-2.09 2.61-2.3 3.87-.51.85-1.16 1.84-1.29 2.83-.06.44.61.63.67.19.01-.08.04-.15.06-.22 1.36 1.08 2.76 2.11 4.19 3.11 1.3.91 2.62 1.85 4.04 2.56.21.1.4 0 .48-.17.24.07.48.14.72.2.44.1.62-.57.19-.67-2.02-.48-3.77-1.57-5.23-3.02-.47-.46-.9-.96-1.32-1.46 1.74 1.35 4.2 2.89 5.89 4.14 1.39 1.03 2.85-2.27 4.22-4.2 1.86-2.64 3.96-5.86 5.52-8.29l10.39 14.51c.67.81 1.14 1.21 1.57 1.36-.05.24.12.51.41.4 1.53-.58 3.05-1.19 4.54-1.87 1.52-.69 3.06-1.45 4.36-2.5a.28.28 0 0 0 .12-.23c1.66-1.1.81-1.74-1.41-4.91-1.13-1.58-1.71-2.36-3.7-5.01l-.03-.02Zm2.41 6.54c.56 1.15 1.19 2.52 1.11 3.81-.06.04-.12.07-.17.1-.03-.88-.55-2.66-.94-3.91Zm-16.51-32.73c1.86-2.65 3.65-5.35 5.57-7.95.4-.55.81-1.13 1.26-1.66.19-.18.38-.33.56-.45.18.03.36.08.55.13l-8.05 10.11.12-.18h-.01ZM192.7 95.48c.79-1.37 1.66-2.69 2.54-4 1.19-1.79 2.4-3.56 3.61-5.33-.04.09-.09.17-.13.26-.1.22.03.41.2.49-2.47 3.42-4.89 6.73-6.4 9.28.21.24.4.48.63.75-.24.07-.4.36-.17.56.4.33.72.77 1.05 1.17.09.11.18.21.27.32-.84-.61-1.66-1.24-2.47-1.88.24-.57.58-1.11.87-1.61v-.01Zm7.46-10.32c.47-.81.98-1.59 1.49-2.37.31-.48.64-.95.96-1.43.26-.29.52-.56.75-.79-.99 1.48-2.09 3.03-3.2 4.59Zm10.03-16.22s-.03-.05-.05-.07c.22-.29.43-.59.64-.89-.2.32-.4.65-.58.96h-.01ZM371.54 87.96c-.01-.08-.01-.16-.03-.23-.06-.38-.58-.29-.66.03-.3-.05-.6-.08-.81-.11-1.14-.15-2.29-.19-3.44-.2 1.04-.09 2.09-.18 3.14-.23.45-.02.45-.72 0-.7-6.57.35-13.14 1.23-19.65 2.11-1.53.21-3.05.42-4.57.68-.01 0-.02.01-.04.01-.04-3.33-.13-6.66-.24-9.99-.19-5.7-.4-11.41-.88-17.1-.13-1.51-.23-3.07-.49-4.58 0-.25 0-.48-.02-.68-.06-1.19-.04-2.61-.68-2.78-.16-.07-.72-.16-1.5-.24.22-.17.16-.62-.2-.63-1.19-.04-2.39.09-3.57.23-1.2.14-2.41.32-3.59.6-.16-.1-.41-.06-.5.12-.06.02-.13.03-.19.05-.35.1-.29.55-.03.66-.26.6-.19 2.27-.21 3-.02.66-.66 33.73-.9 40.3-.03.65.06 1.12.04 1.45-.16 3.05.87 4.96 6.34 3.93 1.09-.08 2.75-.77 5.36-1.43 4.13-1.04 5.78-1.52 6.2-1.65 6.43-1.69 6.78-1.97 11.72-2.43.55-.05 4.8-.38 6.03-.3.64.04 1.19.07 1.65.1.09 0 .16-.03.24-.05.1.27.56.33.66-.02.39-1.32.61-2.71.78-4.08.2-1.61.29-3.24.15-4.86.24.03.52-.23.38-.53-.09-.2-.27-.33-.49-.43v-.02Zm-.63.56c.07.57.11 1.14.11 1.71-.21-.99-.53-1.71-.95-1.87.22.03.44.06.65.11.06.01.12.04.19.05Zm-25.41 1.73c1.54-.36 3.1-.64 4.66-.89-1.61.37-3.18.77-4.66 1.2v-.31Zm-.86-7.37c-.07-1.37-.16-2.75-.25-4.12-.21-3.13-.45-6.27-.79-9.4.02-2.25.08-4.31.13-6.11.16 2.08.29 4.16.4 6.24.23 4.46.38 8.93.5 13.39h.01Zm-.94-4c.16 2.41.29 4.83.39 7.24.06 1.6.14 3.22.09 4.83-.15.05-.32.09-.47.14V78.88h-.01ZM483.72 92.83c-3.05-2.28-6.22-4.4-9.38-6.51 8.86-6.49 13.49-12.95 13.73-19.23.04-.76 0-1.5-.13-2.2-.67-3.82-3.5-6.68-8.39-8.48.13.04.27.08.4.13 3.92 1.39 7.74 4.23 8.5 8.56.34 1.95-.05 3.96-.98 5.69-.21.4.39.75.6.35 1.86-3.46 1.46-7.55-.97-10.63-3.53-4.47-9.76-5.88-15.16-6.16-2.32-.12-4.64-.04-6.95.19-6 .32-12.71 1.68-17.63 3.21-.37.11-.67.23-.92.35-.2-.17-.62.02-.57.37v.03c-.64.68-.18 1.64.48 3.21.38.91.67 1.89 1.15 2.58.32.76.68 1.51 1.13 2.19.14.21.38.19.53.07.19-.02.38-.05.57-.08v1.57c-.06.06-.1.13-.11.23-.27 4.18-.34 8.38-.48 12.57l-.3 9.03c-.24 3.91-.44 6.77-.46 7.26-.05.88-.11 1.95.07 2.81-.01.22-.02.43-.04.65 0 .11-.02.23-.03.35 0 .05-.03.27-.01.16-.05.4.5.59.64.28.05.04.12.08.2.08 1.75.13 3.5.28 5.25.3 1.69.02 3.38-.12 5.06-.32.08.23.36.39.55.15.06-.08.11-.17.16-.26.18-.09.24-.32.18-.48.05-.2.1-.4.13-.6.16-.86.25-1.74.33-2.62.11-1.17.17-2.34.23-3.51.15-.01.32-.03.52-.04.36-.03 1.73-.15 2.06-.15.39 0 .7-.02.95-.04 1.76 1.11 3.45 2.35 5.14 3.55 2.83 2.01 5.64 4.04 8.47 6.04 1.42 1 2.85 2 4.29 2.97.1.06.19.07.27.04.08 0 .17-.02.25-.1 1.61-1.56 3.15-3.18 4.6-4.88.75-.88 1.49-1.78 2.15-2.73.01.01.03.02.04.03.34.3.83-.2.49-.49-2.16-1.9-4.34-3.76-6.64-5.48l.03-.01Zm-6.38-3.65a55.72 55.72 0 0 0-4-2.13c.14-.1.26-.19.4-.29 1.2.81 2.4 1.61 3.6 2.42Zm-20.1 11.78c.67-.37 1.23-.91 1.67-1.6-.11.5-.24 1-.38 1.49-.43.04-.86.08-1.29.11Zm2.38-37.24c1.34-.31 2.56-.52 3.71-.69-1.03.19-2.04.41-3.04.65-.14-.07-.34-.02-.45.11-.07.02-.15.04-.22.05v-.13.01Zm.04.84c.07-.02.14-.03.2-.05.34 1.66.41 3.41.5 5.09.17 2.9.24 5.81.28 8.71l.03 3.17c-.17.07-.34.14-.51.2-.06-4.96-.21-10.58-.51-17.12h.01Zm16.04 5.62c-1.16 2.25-3.06 4.1-5.02 5.66-2.8 2.23-5.99 3.97-9.3 5.35-.01-3.56-.09-7.12-.27-10.67-.1-2.04-.16-4.16-.57-6.18 3.3-.78 6.72-1.36 10.1-1.1 1.85.14 4.23.59 5.32 2.29.92 1.43.46 3.24-.26 4.65Zm.85-.18c.6-1.37.9-2.92.28-4.32-.67-1.52-2.2-2.32-3.76-2.74.46.1.89.21 1.29.37 1.74.67 2.69 1.88 2.93 3.21.2 1.13-.05 2.25-.74 3.47V70Zm-27.47-4.14c-.12-.19-.23-.38-.34-.57.74.42.85.36.99.41v.08c-.22.03-.43.06-.65.08Zm11.21 30.46c-.08 1.08-.16 2.17-.33 3.24-.05.35-.11.69-.2 1.03 0 .04-.02.07-.03.11-.15.02-.3.04-.45.05.45-1.64.76-3.36.79-5.07.03-.29.08-.57.1-.89-.03-.31-.03-.47.24-.57-.04.69-.07 1.39-.12 2.08v.02Zm5.6-2.47c.48.11.92.52 2.49 1.72-.46-.32-.92-.65-1.38-.97-.37-.25-.73-.5-1.1-.75h-.01Zm21.23 7.24a70.76 70.76 0 0 1-4.37 4.63c-.14-.09-.27-.19-.4-.28.19-.09.37-.24.55-.47.87-1.14 5.43-5.51 5.49-7.45.31.26.62.53.92.79-.67.97-1.42 1.88-2.19 2.77v.01Z" + fill="currentColor" + transform="translate(-144.023 -51.76)" + /> + </svg> + </div> + </div> + <div + class="welcome-screen-center__heading welcome-screen-decor virgil" + > + All your data is saved locally in your browser. + </div> + <div + class="welcome-screen-menu" + > + <button + class="welcome-screen-menu-item " + type="button" + > + <div + class="welcome-screen-menu-item__icon" + > + <svg + aria-hidden="true" + class="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + viewBox="0 0 20 20" + > + <path + d="m9.257 6.351.183.183H15.819c.34 0 .727.182 1.051.506.323.323.505.708.505 1.05v5.819c0 .316-.183.7-.52 1.035-.337.338-.723.522-1.037.522H4.182c-.352 0-.74-.181-1.058-.5-.318-.318-.499-.705-.499-1.057V5.182c0-.351.181-.736.5-1.054.32-.321.71-.503 1.057-.503H6.53l2.726 2.726Z" + stroke-width="1.25" + /> + </svg> + </div> + <div + class="welcome-screen-menu-item__text" + > + Open + </div> + <div + class="welcome-screen-menu-item__shortcut" + > + Ctrl+O + </div> + </button> + <button + class="welcome-screen-menu-item " + type="button" + > + <div + class="welcome-screen-menu-item__icon" + > + <svg + aria-hidden="true" + class="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + viewBox="0 0 24 24" + > + <g + stroke-width="1.5" + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <circle + cx="12" + cy="12" + r="9" + /> + <line + x1="12" + x2="12" + y1="17" + y2="17.01" + /> + <path + d="M12 13.5a1.5 1.5 0 0 1 1 -1.5a2.6 2.6 0 1 0 -3 -4" + /> + </g> + </svg> + </div> + <div + class="welcome-screen-menu-item__text" + > + Help + </div> + <div + class="welcome-screen-menu-item__shortcut" + > + ? + </div> + </button> + <button + class="welcome-screen-menu-item " + type="button" + > + <div + class="welcome-screen-menu-item__icon" + > + <svg + aria-hidden="true" + class="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + viewBox="0 0 24 24" + > + <g + stroke-width="1.5" + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <circle + cx="9" + cy="7" + r="4" + /> + <path + d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" + /> + <path + d="M16 3.13a4 4 0 0 1 0 7.75" + /> + <path + d="M21 21v-2a4 4 0 0 0 -3 -3.85" + /> + </g> + </svg> + </div> + <div + class="welcome-screen-menu-item__text" + > + Live collaboration... + </div> + </button> + <a + class="welcome-screen-menu-item " + href="undefined/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest" + rel="noreferrer" + target="_blank" + > + <div + class="welcome-screen-menu-item__icon" + > + <svg + aria-hidden="true" + class="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + viewBox="0 0 24 24" + > + <g + stroke-width="1.5" + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <rect + height="4" + rx="1" + width="18" + x="3" + y="8" + /> + <line + x1="12" + x2="12" + y1="8" + y2="21" + /> + <path + d="M19 12v7a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-7" + /> + <path + d="M7.5 8a2.5 2.5 0 0 1 0 -5a4.8 8 0 0 1 4.5 5a4.8 8 0 0 1 4.5 -5a2.5 2.5 0 0 1 0 5" + /> + </g> + </svg> + </div> + <div + class="welcome-screen-menu-item__text" + > + Try Excalidraw Plus! + </div> + </a> + </div> +</div> +`; diff --git a/packages/excalidraw/tests/__snapshots__/charts.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/charts.test.tsx.snap new file mode 100644 index 0000000..868e27e --- /dev/null +++ b/packages/excalidraw/tests/__snapshots__/charts.test.tsx.snap @@ -0,0 +1,20 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`tryParseSpreadsheet > works for numbers with comma in them 1`] = ` +{ + "spreadsheet": { + "labels": [ + "Week 1", + "Week 2", + "Week 3", + ], + "title": "Users", + "values": [ + 814, + 10301, + 4264, + ], + }, + "type": "VALID_SPREADSHEET", +} +`; diff --git a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap new file mode 100644 index 0000000..f77eb8d --- /dev/null +++ b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap @@ -0,0 +1,9977 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`contextMenu element > right-clicking on a group should select whole group > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": { + "items": [ + "separator", + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M7 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" + /> + <path + d="M17 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" + /> + <path + d="M9.15 14.85l8.85 -10.85" + /> + <path + d="M6 4l8.85 10.85" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.cut", + "name": "cut", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + viewBox="0 0 20 20" + > + <g + strokeWidth="1.25" + > + <path + d="M14.375 6.458H8.958a2.5 2.5 0 0 0-2.5 2.5v5.417a2.5 2.5 0 0 0 2.5 2.5h5.417a2.5 2.5 0 0 0 2.5-2.5V8.958a2.5 2.5 0 0 0-2.5-2.5Z" + /> + <path + clipRule="evenodd" + d="M11.667 3.125c.517 0 .986.21 1.325.55.34.338.55.807.55 1.325v1.458H8.333c-.485 0-.927.185-1.26.487-.343.312-.57.75-.609 1.24l-.005 5.357H5a1.87 1.87 0 0 1-1.326-.55 1.87 1.87 0 0 1-.549-1.325V5c0-.518.21-.987.55-1.326.338-.34.807-.549 1.325-.549h6.667Z" + /> + </g> + </svg>, + "keyTest": undefined, + "label": "labels.copy", + "name": "copy", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "keyTest": undefined, + "label": "labels.paste", + "name": "paste", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "label": "labels.selectAllElementsInFrame", + "name": "selectAllElementsInFrame", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "canvas", + }, + }, + { + "label": "labels.removeAllElementsFromFrame", + "name": "removeAllElementsFromFrame", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "history", + }, + }, + { + "label": "labels.wrapSelectionInFrame", + "name": "wrapSelectionInFrame", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth="1.25" + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M8 5v10a1 1 0 0 0 1 1h10" + /> + <path + d="M5 8h10a1 1 0 0 1 1 1v10" + /> + </g> + </svg>, + "keywords": [ + "image", + "crop", + ], + "label": "helpDialog.cropStart", + "name": "cropEditor", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "menu", + }, + "viewMode": true, + }, + "separator", + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M14 3v4a1 1 0 0 0 1 1h4" + /> + <path + d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4" + /> + <path + d="M20 15h-1a2 2 0 0 0 -2 2v2a2 2 0 0 0 2 2h1v-3" + /> + <path + d="M5 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6" + /> + <path + d="M11 21v-6l3 6v-6" + /> + </g> + </svg>, + "keyTest": [Function], + "keywords": [ + "png", + "clipboard", + "copy", + ], + "label": "labels.copyAsPng", + "name": "copyAsPng", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M14 3v4a1 1 0 0 0 1 1h4" + /> + <path + d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4" + /> + <path + d="M4 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75" + /> + <path + d="M10 15l2 6l2 -6" + /> + <path + d="M20 15h-1a2 2 0 0 0 -2 2v2a2 2 0 0 0 2 2h1v-3" + /> + </g> + </svg>, + "keywords": [ + "svg", + "clipboard", + "copy", + ], + "label": "labels.copyAsSvg", + "name": "copyAsSvg", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "keywords": [ + "text", + "clipboard", + "copy", + ], + "label": "labels.copyText", + "name": "copyText", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M5 3m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z" + /> + <path + d="M19 6h1a2 2 0 0 1 2 2a5 5 0 0 1 -5 5l-5 0v2" + /> + <path + d="M10 15m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.copyStyles", + "name": "copyStyles", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M5 3m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z" + /> + <path + d="M19 6h1a2 2 0 0 1 2 2a5 5 0 0 1 -5 5l-5 0v2" + /> + <path + d="M10 15m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.pasteStyles", + "name": "pasteStyles", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": [Function], + "keyTest": [Function], + "label": "labels.group", + "name": "group", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": null, + "label": "labels.autoResize", + "name": "autoResize", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "label": "labels.unbindText", + "name": "unbindText", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "label": "labels.bindText", + "name": "bindText", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "label": "labels.createContainerFromText", + "name": "wrapTextInContainer", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "PanelComponent": [Function], + "icon": [Function], + "keyTest": [Function], + "label": "labels.ungroup", + "name": "ungroup", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "label": "labels.addToLibrary", + "name": "addToLibrary", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + style={ + { + "transform": "rotate(180deg)", + } + } + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.50000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M12 5l0 14" + /> + <path + d="M16 9l-4 -4" + /> + <path + d="M8 9l4 -4" + /> + </g> + </svg>, + "keyPriority": 40, + "keyTest": [Function], + "keywords": [ + "move down", + "zindex", + "layer", + ], + "label": "labels.sendBackward", + "name": "sendBackward", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.50000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M12 5l0 14" + /> + <path + d="M16 9l-4 -4" + /> + <path + d="M8 9l4 -4" + /> + </g> + </svg>, + "keyPriority": 40, + "keyTest": [Function], + "keywords": [ + "move up", + "zindex", + "layer", + ], + "label": "labels.bringForward", + "name": "bringForward", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + style={ + { + "transform": "rotate(180deg)", + } + } + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.50000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M12 10l0 10" + /> + <path + d="M12 10l4 4" + /> + <path + d="M12 10l-4 4" + /> + <path + d="M4 4l16 0" + /> + </g> + </svg>, + "keyTest": [Function], + "keywords": [ + "move down", + "zindex", + "layer", + ], + "label": "labels.sendToBack", + "name": "sendToBack", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.50000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M12 10l0 10" + /> + <path + d="M12 10l4 4" + /> + <path + d="M12 10l-4 4" + /> + <path + d="M4 4l16 0" + /> + </g> + </svg>, + "keyTest": [Function], + "keywords": [ + "move up", + "zindex", + "layer", + ], + "label": "labels.bringToFront", + "name": "bringToFront", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M12 3l0 18" + /> + <path + d="M16 7l0 10l5 0l-5 -10" + /> + <path + d="M8 7l0 10l-5 0l5 -10" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.flipHorizontal", + "name": "flipHorizontal", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M3 12l18 0" + /> + <path + d="M7 16l10 0l-10 5l0 -5" + /> + <path + d="M7 8l10 0l-10 -5l0 5" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.flipVertical", + "name": "flipVertical", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "category": "Elements", + "keywords": [ + "line", + ], + "label": [Function], + "name": "toggleLinearEditor", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + viewBox="0 0 20 20" + > + <g + strokeWidth="1.25" + > + <path + d="M8.333 11.667a2.917 2.917 0 0 0 4.167 0l3.333-3.334a2.946 2.946 0 1 0-4.166-4.166l-.417.416" + /> + <path + d="M11.667 8.333a2.917 2.917 0 0 0-4.167 0l-3.333 3.334a2.946 2.946 0 0 0 4.166 4.166l.417-.416" + /> + </g> + </svg>, + "keyTest": [Function], + "label": [Function], + "name": "hyperlink", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "action": "click", + "category": "hyperlink", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <React.Fragment> + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z" + /> + <path + d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2" + /> + </React.Fragment> + </svg>, + "label": "labels.copyElementLink", + "name": "copyElementLink", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + viewBox="0 0 20 20" + > + <g + strokeWidth="1.25" + > + <path + d="M14.375 6.458H8.958a2.5 2.5 0 0 0-2.5 2.5v5.417a2.5 2.5 0 0 0 2.5 2.5h5.417a2.5 2.5 0 0 0 2.5-2.5V8.958a2.5 2.5 0 0 0-2.5-2.5Z" + /> + <path + clipRule="evenodd" + d="M11.667 3.125c.517 0 .986.21 1.325.55.34.338.55.807.55 1.325v1.458H8.333c-.485 0-.927.185-1.26.487-.343.312-.57.75-.609 1.24l-.005 5.357H5a1.87 1.87 0 0 1-1.326-.55 1.87 1.87 0 0 1-.549-1.325V5c0-.518.21-.987.55-1.326.338-.34.807-.549 1.325-.549h6.667Z" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.duplicateSelection", + "name": "duplicateSelection", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": [Function], + "keyTest": [Function], + "label": [Function], + "name": "toggleElementLock", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + viewBox="0 0 20 20" + > + <path + d="M3.333 5.833h13.334M8.333 9.167v5M11.667 9.167v5M4.167 5.833l.833 10c0 .92.746 1.667 1.667 1.667h6.666c.92 0 1.667-.746 1.667-1.667l.833-10M7.5 5.833v-2.5c0-.46.373-.833.833-.833h3.334c.46 0 .833.373.833.833v2.5" + strokeWidth="1.25" + /> + </svg>, + "keyTest": [Function], + "label": "labels.delete", + "name": "deleteSelectedElements", + "perform": [Function], + "trackEvent": { + "action": "delete", + "category": "element", + }, + }, + ], + "left": 30, + "top": 40, + }, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 100, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 20, + "offsetTop": 10, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + "id1": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": { + "g1": true, + }, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 200, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`contextMenu element > right-clicking on a group should select whole group > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "red", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "g1", + ], + "height": 100, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "versionNonce": 1278240551, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`contextMenu element > right-clicking on a group should select whole group > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "red", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "g1", + ], + "height": 100, + "id": "id1", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "versionNonce": 449462985, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`contextMenu element > right-clicking on a group should select whole group > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [], +} +`; + +exports[`contextMenu element > right-clicking on a group should select whole group > [end of test] number of elements 1`] = `2`; + +exports[`contextMenu element > right-clicking on a group should select whole group > [end of test] number of renders 1`] = `5`; + +exports[`contextMenu element > selecting 'Add to library' in context menu adds element to library > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 100, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 20, + "offsetTop": 10, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": { + "message": "Added to library", + }, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 200, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`contextMenu element > selecting 'Add to library' in context menu adds element to library > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 449462985, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 3, + "versionNonce": 1150084233, + "width": 20, + "x": -10, + "y": 0, +} +`; + +exports[`contextMenu element > selecting 'Add to library' in context menu adds element to library > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 20, + "x": -10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`contextMenu element > selecting 'Add to library' in context menu adds element to library > [end of test] number of elements 1`] = `1`; + +exports[`contextMenu element > selecting 'Add to library' in context menu adds element to library > [end of test] number of renders 1`] = `5`; + +exports[`contextMenu element > selecting 'Bring forward' in context menu brings element forward > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 100, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 20, + "offsetTop": 10, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 200, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`contextMenu element > selecting 'Bring forward' in context menu brings element forward > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id1", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1014066025, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 3, + "versionNonce": 1604849351, + "width": 20, + "x": 20, + "y": 30, +} +`; + +exports[`contextMenu element > selecting 'Bring forward' in context menu brings element forward > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id0", + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 449462985, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "versionNonce": 23633383, + "width": 20, + "x": -10, + "y": 0, +} +`; + +exports[`contextMenu element > selecting 'Bring forward' in context menu brings element forward > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 20, + "x": -10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 20, + "x": 20, + "y": 30, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "index": "a2", + }, + "inserted": { + "index": "a0", + }, + }, + }, + }, + }, + ], +} +`; + +exports[`contextMenu element > selecting 'Bring forward' in context menu brings element forward > [end of test] number of elements 1`] = `2`; + +exports[`contextMenu element > selecting 'Bring forward' in context menu brings element forward > [end of test] number of renders 1`] = `10`; + +exports[`contextMenu element > selecting 'Bring to front' in context menu brings element to front > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 100, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 20, + "offsetTop": 10, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 200, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`contextMenu element > selecting 'Bring to front' in context menu brings element to front > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id1", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1014066025, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 3, + "versionNonce": 1604849351, + "width": 20, + "x": 20, + "y": 30, +} +`; + +exports[`contextMenu element > selecting 'Bring to front' in context menu brings element to front > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id0", + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 449462985, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "versionNonce": 23633383, + "width": 20, + "x": -10, + "y": 0, +} +`; + +exports[`contextMenu element > selecting 'Bring to front' in context menu brings element to front > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 20, + "x": -10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 20, + "x": 20, + "y": 30, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "index": "a2", + }, + "inserted": { + "index": "a0", + }, + }, + }, + }, + }, + ], +} +`; + +exports[`contextMenu element > selecting 'Bring to front' in context menu brings element to front > [end of test] number of elements 1`] = `2`; + +exports[`contextMenu element > selecting 'Bring to front' in context menu brings element to front > [end of test] number of renders 1`] = `10`; + +exports[`contextMenu element > selecting 'Copy styles' in context menu copies styles > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 100, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 20, + "offsetTop": 10, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": { + "message": "Copied styles.", + }, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 200, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`contextMenu element > selecting 'Copy styles' in context menu copies styles > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 449462985, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 3, + "versionNonce": 1150084233, + "width": 20, + "x": -10, + "y": 0, +} +`; + +exports[`contextMenu element > selecting 'Copy styles' in context menu copies styles > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 20, + "x": -10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`contextMenu element > selecting 'Copy styles' in context menu copies styles > [end of test] number of elements 1`] = `1`; + +exports[`contextMenu element > selecting 'Copy styles' in context menu copies styles > [end of test] number of renders 1`] = `5`; + +exports[`contextMenu element > selecting 'Delete' in context menu deletes element > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 100, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 20, + "offsetTop": 10, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 200, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`contextMenu element > selecting 'Delete' in context menu deletes element > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id0", + "index": "a0", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 449462985, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "versionNonce": 1014066025, + "width": 20, + "x": -10, + "y": 0, +} +`; + +exports[`contextMenu element > selecting 'Delete' in context menu deletes element > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 20, + "x": -10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": {}, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map { + "id0" => Delta { + "deleted": { + "isDeleted": true, + }, + "inserted": { + "isDeleted": false, + }, + }, + }, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`contextMenu element > selecting 'Delete' in context menu deletes element > [end of test] number of elements 1`] = `1`; + +exports[`contextMenu element > selecting 'Delete' in context menu deletes element > [end of test] number of renders 1`] = `6`; + +exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 100, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 20, + "offsetTop": 10, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id1": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 200, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 449462985, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 3, + "versionNonce": 1150084233, + "width": 20, + "x": -10, + "y": 0, +} +`; + +exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id1", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1014066025, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "versionNonce": 238820263, + "width": 20, + "x": 0, + "y": 10, +} +`; + +exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 20, + "x": -10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 20, + "x": 0, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] number of elements 1`] = `2`; + +exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] number of renders 1`] = `6`; + +exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 100, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 20, + "offsetTop": 10, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id1": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + "id1": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": { + "id3": true, + }, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 200, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "id3", + ], + "height": 20, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 449462985, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "versionNonce": 493213705, + "width": 20, + "x": -10, + "y": 0, +} +`; + +exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "id3", + ], + "height": 20, + "id": "id1", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1014066025, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "versionNonce": 915032327, + "width": 20, + "x": 20, + "y": 30, +} +`; + +exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 20, + "x": -10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 20, + "x": 20, + "y": 30, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedGroupIds": { + "id3": true, + }, + }, + "inserted": { + "selectedGroupIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "groupIds": [ + "id3", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + "id1" => Delta { + "deleted": { + "groupIds": [ + "id3", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + }, + }, + }, + ], +} +`; + +exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] number of elements 1`] = `2`; + +exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] number of renders 1`] = `10`; + +exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "#a5d8ff", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "cross-hatch", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 60, + "currentItemRoughness": 2, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#e03131", + "currentItemStrokeStyle": "dotted", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 100, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 20, + "offsetTop": 10, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": { + "message": "Copied styles.", + }, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 200, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "#a5d8ff", + "boundElements": null, + "customData": undefined, + "fillStyle": "cross-hatch", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 60, + "roughness": 2, + "roundness": { + "type": 3, + }, + "seed": 449462985, + "strokeColor": "#e03131", + "strokeStyle": "dotted", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "versionNonce": 941653321, + "width": 20, + "x": -10, + "y": 0, +} +`; + +exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "#a5d8ff", + "boundElements": null, + "customData": undefined, + "fillStyle": "cross-hatch", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id1", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 60, + "roughness": 2, + "roundness": { + "type": 3, + }, + "seed": 289600103, + "strokeColor": "#e03131", + "strokeStyle": "dotted", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 9, + "versionNonce": 640725609, + "width": 20, + "x": 20, + "y": 30, +} +`; + +exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 20, + "x": -10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 20, + "x": 20, + "y": 30, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id1" => Delta { + "deleted": { + "strokeColor": "#e03131", + }, + "inserted": { + "strokeColor": "#1e1e1e", + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id1" => Delta { + "deleted": { + "backgroundColor": "#a5d8ff", + }, + "inserted": { + "backgroundColor": "transparent", + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id1" => Delta { + "deleted": { + "fillStyle": "cross-hatch", + }, + "inserted": { + "fillStyle": "solid", + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id1" => Delta { + "deleted": { + "strokeStyle": "dotted", + }, + "inserted": { + "strokeStyle": "solid", + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id1" => Delta { + "deleted": { + "roughness": 2, + }, + "inserted": { + "roughness": 1, + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id1" => Delta { + "deleted": { + "opacity": 60, + }, + "inserted": { + "opacity": 100, + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "backgroundColor": "#a5d8ff", + "fillStyle": "cross-hatch", + "opacity": 60, + "roughness": 2, + "strokeColor": "#e03131", + "strokeStyle": "dotted", + }, + "inserted": { + "backgroundColor": "transparent", + "fillStyle": "solid", + "opacity": 100, + "roughness": 1, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + }, + }, + }, + }, + }, + ], +} +`; + +exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of elements 1`] = `2`; + +exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of renders 1`] = `16`; + +exports[`contextMenu element > selecting 'Send backward' in context menu sends element backward > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 100, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 20, + "offsetTop": 10, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id1": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 200, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`contextMenu element > selecting 'Send backward' in context menu sends element backward > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id1", + "index": "Zz", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1014066025, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "versionNonce": 23633383, + "width": 20, + "x": 20, + "y": 30, +} +`; + +exports[`contextMenu element > selecting 'Send backward' in context menu sends element backward > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 449462985, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 3, + "versionNonce": 1150084233, + "width": 20, + "x": -10, + "y": 0, +} +`; + +exports[`contextMenu element > selecting 'Send backward' in context menu sends element backward > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 20, + "x": -10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 20, + "x": 20, + "y": 30, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id1" => Delta { + "deleted": { + "index": "Zz", + }, + "inserted": { + "index": "a1", + }, + }, + }, + }, + }, + ], +} +`; + +exports[`contextMenu element > selecting 'Send backward' in context menu sends element backward > [end of test] number of elements 1`] = `2`; + +exports[`contextMenu element > selecting 'Send backward' in context menu sends element backward > [end of test] number of renders 1`] = `9`; + +exports[`contextMenu element > selecting 'Send to back' in context menu sends element to back > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 100, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 20, + "offsetTop": 10, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id1": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 200, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`contextMenu element > selecting 'Send to back' in context menu sends element to back > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id1", + "index": "Zz", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1014066025, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "versionNonce": 23633383, + "width": 20, + "x": 20, + "y": 30, +} +`; + +exports[`contextMenu element > selecting 'Send to back' in context menu sends element to back > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 449462985, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 3, + "versionNonce": 1150084233, + "width": 20, + "x": -10, + "y": 0, +} +`; + +exports[`contextMenu element > selecting 'Send to back' in context menu sends element to back > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 20, + "x": -10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 20, + "x": 20, + "y": 30, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id1" => Delta { + "deleted": { + "index": "Zz", + }, + "inserted": { + "index": "a1", + }, + }, + }, + }, + }, + ], +} +`; + +exports[`contextMenu element > selecting 'Send to back' in context menu sends element to back > [end of test] number of elements 1`] = `2`; + +exports[`contextMenu element > selecting 'Send to back' in context menu sends element to back > [end of test] number of renders 1`] = `9`; + +exports[`contextMenu element > selecting 'Ungroup selection' in context menu ungroups selected group > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 100, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 20, + "offsetTop": 10, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id1": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + "id1": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 200, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`contextMenu element > selecting 'Ungroup selection' in context menu ungroups selected group > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 449462985, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 5, + "versionNonce": 1723083209, + "width": 20, + "x": -10, + "y": 0, +} +`; + +exports[`contextMenu element > selecting 'Ungroup selection' in context menu ungroups selected group > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id1", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 238820263, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 5, + "versionNonce": 760410951, + "width": 20, + "x": 20, + "y": 30, +} +`; + +exports[`contextMenu element > selecting 'Ungroup selection' in context menu ungroups selected group > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 20, + "x": -10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 20, + "x": 20, + "y": 30, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedGroupIds": { + "id3": true, + }, + }, + "inserted": { + "selectedGroupIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "groupIds": [ + "id3", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + "id1" => Delta { + "deleted": { + "groupIds": [ + "id3", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedGroupIds": {}, + }, + "inserted": { + "selectedGroupIds": { + "id3": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "groupIds": [], + }, + "inserted": { + "groupIds": [ + "id3", + ], + }, + }, + "id1" => Delta { + "deleted": { + "groupIds": [], + }, + "inserted": { + "groupIds": [ + "id3", + ], + }, + }, + }, + }, + }, + ], +} +`; + +exports[`contextMenu element > selecting 'Ungroup selection' in context menu ungroups selected group > [end of test] number of elements 1`] = `2`; + +exports[`contextMenu element > selecting 'Ungroup selection' in context menu ungroups selected group > [end of test] number of renders 1`] = `11`; + +exports[`contextMenu element > shows 'Group selection' in context menu for multiple selected elements > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": { + "items": [ + "separator", + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M7 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" + /> + <path + d="M17 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" + /> + <path + d="M9.15 14.85l8.85 -10.85" + /> + <path + d="M6 4l8.85 10.85" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.cut", + "name": "cut", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + viewBox="0 0 20 20" + > + <g + strokeWidth="1.25" + > + <path + d="M14.375 6.458H8.958a2.5 2.5 0 0 0-2.5 2.5v5.417a2.5 2.5 0 0 0 2.5 2.5h5.417a2.5 2.5 0 0 0 2.5-2.5V8.958a2.5 2.5 0 0 0-2.5-2.5Z" + /> + <path + clipRule="evenodd" + d="M11.667 3.125c.517 0 .986.21 1.325.55.34.338.55.807.55 1.325v1.458H8.333c-.485 0-.927.185-1.26.487-.343.312-.57.75-.609 1.24l-.005 5.357H5a1.87 1.87 0 0 1-1.326-.55 1.87 1.87 0 0 1-.549-1.325V5c0-.518.21-.987.55-1.326.338-.34.807-.549 1.325-.549h6.667Z" + /> + </g> + </svg>, + "keyTest": undefined, + "label": "labels.copy", + "name": "copy", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "keyTest": undefined, + "label": "labels.paste", + "name": "paste", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "label": "labels.selectAllElementsInFrame", + "name": "selectAllElementsInFrame", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "canvas", + }, + }, + { + "label": "labels.removeAllElementsFromFrame", + "name": "removeAllElementsFromFrame", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "history", + }, + }, + { + "label": "labels.wrapSelectionInFrame", + "name": "wrapSelectionInFrame", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth="1.25" + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M8 5v10a1 1 0 0 0 1 1h10" + /> + <path + d="M5 8h10a1 1 0 0 1 1 1v10" + /> + </g> + </svg>, + "keywords": [ + "image", + "crop", + ], + "label": "helpDialog.cropStart", + "name": "cropEditor", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "menu", + }, + "viewMode": true, + }, + "separator", + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M14 3v4a1 1 0 0 0 1 1h4" + /> + <path + d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4" + /> + <path + d="M20 15h-1a2 2 0 0 0 -2 2v2a2 2 0 0 0 2 2h1v-3" + /> + <path + d="M5 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6" + /> + <path + d="M11 21v-6l3 6v-6" + /> + </g> + </svg>, + "keyTest": [Function], + "keywords": [ + "png", + "clipboard", + "copy", + ], + "label": "labels.copyAsPng", + "name": "copyAsPng", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M14 3v4a1 1 0 0 0 1 1h4" + /> + <path + d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4" + /> + <path + d="M4 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75" + /> + <path + d="M10 15l2 6l2 -6" + /> + <path + d="M20 15h-1a2 2 0 0 0 -2 2v2a2 2 0 0 0 2 2h1v-3" + /> + </g> + </svg>, + "keywords": [ + "svg", + "clipboard", + "copy", + ], + "label": "labels.copyAsSvg", + "name": "copyAsSvg", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "keywords": [ + "text", + "clipboard", + "copy", + ], + "label": "labels.copyText", + "name": "copyText", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M5 3m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z" + /> + <path + d="M19 6h1a2 2 0 0 1 2 2a5 5 0 0 1 -5 5l-5 0v2" + /> + <path + d="M10 15m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.copyStyles", + "name": "copyStyles", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M5 3m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z" + /> + <path + d="M19 6h1a2 2 0 0 1 2 2a5 5 0 0 1 -5 5l-5 0v2" + /> + <path + d="M10 15m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.pasteStyles", + "name": "pasteStyles", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": [Function], + "keyTest": [Function], + "label": "labels.group", + "name": "group", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": null, + "label": "labels.autoResize", + "name": "autoResize", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "label": "labels.unbindText", + "name": "unbindText", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "label": "labels.bindText", + "name": "bindText", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "label": "labels.createContainerFromText", + "name": "wrapTextInContainer", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "PanelComponent": [Function], + "icon": [Function], + "keyTest": [Function], + "label": "labels.ungroup", + "name": "ungroup", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "label": "labels.addToLibrary", + "name": "addToLibrary", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + style={ + { + "transform": "rotate(180deg)", + } + } + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.50000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M12 5l0 14" + /> + <path + d="M16 9l-4 -4" + /> + <path + d="M8 9l4 -4" + /> + </g> + </svg>, + "keyPriority": 40, + "keyTest": [Function], + "keywords": [ + "move down", + "zindex", + "layer", + ], + "label": "labels.sendBackward", + "name": "sendBackward", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.50000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M12 5l0 14" + /> + <path + d="M16 9l-4 -4" + /> + <path + d="M8 9l4 -4" + /> + </g> + </svg>, + "keyPriority": 40, + "keyTest": [Function], + "keywords": [ + "move up", + "zindex", + "layer", + ], + "label": "labels.bringForward", + "name": "bringForward", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + style={ + { + "transform": "rotate(180deg)", + } + } + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.50000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M12 10l0 10" + /> + <path + d="M12 10l4 4" + /> + <path + d="M12 10l-4 4" + /> + <path + d="M4 4l16 0" + /> + </g> + </svg>, + "keyTest": [Function], + "keywords": [ + "move down", + "zindex", + "layer", + ], + "label": "labels.sendToBack", + "name": "sendToBack", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.50000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M12 10l0 10" + /> + <path + d="M12 10l4 4" + /> + <path + d="M12 10l-4 4" + /> + <path + d="M4 4l16 0" + /> + </g> + </svg>, + "keyTest": [Function], + "keywords": [ + "move up", + "zindex", + "layer", + ], + "label": "labels.bringToFront", + "name": "bringToFront", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M12 3l0 18" + /> + <path + d="M16 7l0 10l5 0l-5 -10" + /> + <path + d="M8 7l0 10l-5 0l5 -10" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.flipHorizontal", + "name": "flipHorizontal", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M3 12l18 0" + /> + <path + d="M7 16l10 0l-10 5l0 -5" + /> + <path + d="M7 8l10 0l-10 -5l0 5" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.flipVertical", + "name": "flipVertical", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "category": "Elements", + "keywords": [ + "line", + ], + "label": [Function], + "name": "toggleLinearEditor", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + viewBox="0 0 20 20" + > + <g + strokeWidth="1.25" + > + <path + d="M8.333 11.667a2.917 2.917 0 0 0 4.167 0l3.333-3.334a2.946 2.946 0 1 0-4.166-4.166l-.417.416" + /> + <path + d="M11.667 8.333a2.917 2.917 0 0 0-4.167 0l-3.333 3.334a2.946 2.946 0 0 0 4.166 4.166l.417-.416" + /> + </g> + </svg>, + "keyTest": [Function], + "label": [Function], + "name": "hyperlink", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "action": "click", + "category": "hyperlink", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <React.Fragment> + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z" + /> + <path + d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2" + /> + </React.Fragment> + </svg>, + "label": "labels.copyElementLink", + "name": "copyElementLink", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + viewBox="0 0 20 20" + > + <g + strokeWidth="1.25" + > + <path + d="M14.375 6.458H8.958a2.5 2.5 0 0 0-2.5 2.5v5.417a2.5 2.5 0 0 0 2.5 2.5h5.417a2.5 2.5 0 0 0 2.5-2.5V8.958a2.5 2.5 0 0 0-2.5-2.5Z" + /> + <path + clipRule="evenodd" + d="M11.667 3.125c.517 0 .986.21 1.325.55.34.338.55.807.55 1.325v1.458H8.333c-.485 0-.927.185-1.26.487-.343.312-.57.75-.609 1.24l-.005 5.357H5a1.87 1.87 0 0 1-1.326-.55 1.87 1.87 0 0 1-.549-1.325V5c0-.518.21-.987.55-1.326.338-.34.807-.549 1.325-.549h6.667Z" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.duplicateSelection", + "name": "duplicateSelection", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": [Function], + "keyTest": [Function], + "label": [Function], + "name": "toggleElementLock", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + viewBox="0 0 20 20" + > + <path + d="M3.333 5.833h13.334M8.333 9.167v5M11.667 9.167v5M4.167 5.833l.833 10c0 .92.746 1.667 1.667 1.667h6.666c.92 0 1.667-.746 1.667-1.667l.833-10M7.5 5.833v-2.5c0-.46.373-.833.833-.833h3.334c.46 0 .833.373.833.833v2.5" + strokeWidth="1.25" + /> + </svg>, + "keyTest": [Function], + "label": "labels.delete", + "name": "deleteSelectedElements", + "perform": [Function], + "trackEvent": { + "action": "delete", + "category": "element", + }, + }, + ], + "left": -17, + "top": -7, + }, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 100, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 20, + "offsetTop": 10, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id0": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + "id1": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 200, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`contextMenu element > shows 'Group selection' in context menu for multiple selected elements > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 453191, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 3, + "versionNonce": 1014066025, + "width": 10, + "x": -10, + "y": 0, +} +`; + +exports[`contextMenu element > shows 'Group selection' in context menu for multiple selected elements > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id1", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 400692809, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 3, + "versionNonce": 23633383, + "width": 10, + "x": 12, + "y": 0, +} +`; + +exports[`contextMenu element > shows 'Group selection' in context menu for multiple selected elements > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": -10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 12, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`contextMenu element > shows 'Group selection' in context menu for multiple selected elements > [end of test] number of elements 1`] = `2`; + +exports[`contextMenu element > shows 'Group selection' in context menu for multiple selected elements > [end of test] number of renders 1`] = `10`; + +exports[`contextMenu element > shows 'Ungroup selection' in context menu for group inside selected elements > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": { + "items": [ + "separator", + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M7 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" + /> + <path + d="M17 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" + /> + <path + d="M9.15 14.85l8.85 -10.85" + /> + <path + d="M6 4l8.85 10.85" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.cut", + "name": "cut", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + viewBox="0 0 20 20" + > + <g + strokeWidth="1.25" + > + <path + d="M14.375 6.458H8.958a2.5 2.5 0 0 0-2.5 2.5v5.417a2.5 2.5 0 0 0 2.5 2.5h5.417a2.5 2.5 0 0 0 2.5-2.5V8.958a2.5 2.5 0 0 0-2.5-2.5Z" + /> + <path + clipRule="evenodd" + d="M11.667 3.125c.517 0 .986.21 1.325.55.34.338.55.807.55 1.325v1.458H8.333c-.485 0-.927.185-1.26.487-.343.312-.57.75-.609 1.24l-.005 5.357H5a1.87 1.87 0 0 1-1.326-.55 1.87 1.87 0 0 1-.549-1.325V5c0-.518.21-.987.55-1.326.338-.34.807-.549 1.325-.549h6.667Z" + /> + </g> + </svg>, + "keyTest": undefined, + "label": "labels.copy", + "name": "copy", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "keyTest": undefined, + "label": "labels.paste", + "name": "paste", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "label": "labels.selectAllElementsInFrame", + "name": "selectAllElementsInFrame", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "canvas", + }, + }, + { + "label": "labels.removeAllElementsFromFrame", + "name": "removeAllElementsFromFrame", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "history", + }, + }, + { + "label": "labels.wrapSelectionInFrame", + "name": "wrapSelectionInFrame", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth="1.25" + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M8 5v10a1 1 0 0 0 1 1h10" + /> + <path + d="M5 8h10a1 1 0 0 1 1 1v10" + /> + </g> + </svg>, + "keywords": [ + "image", + "crop", + ], + "label": "helpDialog.cropStart", + "name": "cropEditor", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "menu", + }, + "viewMode": true, + }, + "separator", + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M14 3v4a1 1 0 0 0 1 1h4" + /> + <path + d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4" + /> + <path + d="M20 15h-1a2 2 0 0 0 -2 2v2a2 2 0 0 0 2 2h1v-3" + /> + <path + d="M5 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6" + /> + <path + d="M11 21v-6l3 6v-6" + /> + </g> + </svg>, + "keyTest": [Function], + "keywords": [ + "png", + "clipboard", + "copy", + ], + "label": "labels.copyAsPng", + "name": "copyAsPng", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M14 3v4a1 1 0 0 0 1 1h4" + /> + <path + d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4" + /> + <path + d="M4 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75" + /> + <path + d="M10 15l2 6l2 -6" + /> + <path + d="M20 15h-1a2 2 0 0 0 -2 2v2a2 2 0 0 0 2 2h1v-3" + /> + </g> + </svg>, + "keywords": [ + "svg", + "clipboard", + "copy", + ], + "label": "labels.copyAsSvg", + "name": "copyAsSvg", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "keywords": [ + "text", + "clipboard", + "copy", + ], + "label": "labels.copyText", + "name": "copyText", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M5 3m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z" + /> + <path + d="M19 6h1a2 2 0 0 1 2 2a5 5 0 0 1 -5 5l-5 0v2" + /> + <path + d="M10 15m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.copyStyles", + "name": "copyStyles", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M5 3m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z" + /> + <path + d="M19 6h1a2 2 0 0 1 2 2a5 5 0 0 1 -5 5l-5 0v2" + /> + <path + d="M10 15m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.pasteStyles", + "name": "pasteStyles", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": [Function], + "keyTest": [Function], + "label": "labels.group", + "name": "group", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": null, + "label": "labels.autoResize", + "name": "autoResize", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "label": "labels.unbindText", + "name": "unbindText", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "label": "labels.bindText", + "name": "bindText", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "label": "labels.createContainerFromText", + "name": "wrapTextInContainer", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "PanelComponent": [Function], + "icon": [Function], + "keyTest": [Function], + "label": "labels.ungroup", + "name": "ungroup", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "label": "labels.addToLibrary", + "name": "addToLibrary", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + style={ + { + "transform": "rotate(180deg)", + } + } + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.50000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M12 5l0 14" + /> + <path + d="M16 9l-4 -4" + /> + <path + d="M8 9l4 -4" + /> + </g> + </svg>, + "keyPriority": 40, + "keyTest": [Function], + "keywords": [ + "move down", + "zindex", + "layer", + ], + "label": "labels.sendBackward", + "name": "sendBackward", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.50000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M12 5l0 14" + /> + <path + d="M16 9l-4 -4" + /> + <path + d="M8 9l4 -4" + /> + </g> + </svg>, + "keyPriority": 40, + "keyTest": [Function], + "keywords": [ + "move up", + "zindex", + "layer", + ], + "label": "labels.bringForward", + "name": "bringForward", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + style={ + { + "transform": "rotate(180deg)", + } + } + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.50000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M12 10l0 10" + /> + <path + d="M12 10l4 4" + /> + <path + d="M12 10l-4 4" + /> + <path + d="M4 4l16 0" + /> + </g> + </svg>, + "keyTest": [Function], + "keywords": [ + "move down", + "zindex", + "layer", + ], + "label": "labels.sendToBack", + "name": "sendToBack", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.50000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M12 10l0 10" + /> + <path + d="M12 10l4 4" + /> + <path + d="M12 10l-4 4" + /> + <path + d="M4 4l16 0" + /> + </g> + </svg>, + "keyTest": [Function], + "keywords": [ + "move up", + "zindex", + "layer", + ], + "label": "labels.bringToFront", + "name": "bringToFront", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M12 3l0 18" + /> + <path + d="M16 7l0 10l5 0l-5 -10" + /> + <path + d="M8 7l0 10l-5 0l5 -10" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.flipHorizontal", + "name": "flipHorizontal", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M3 12l18 0" + /> + <path + d="M7 16l10 0l-10 5l0 -5" + /> + <path + d="M7 8l10 0l-10 -5l0 5" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.flipVertical", + "name": "flipVertical", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "category": "Elements", + "keywords": [ + "line", + ], + "label": [Function], + "name": "toggleLinearEditor", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + viewBox="0 0 20 20" + > + <g + strokeWidth="1.25" + > + <path + d="M8.333 11.667a2.917 2.917 0 0 0 4.167 0l3.333-3.334a2.946 2.946 0 1 0-4.166-4.166l-.417.416" + /> + <path + d="M11.667 8.333a2.917 2.917 0 0 0-4.167 0l-3.333 3.334a2.946 2.946 0 0 0 4.166 4.166l.417-.416" + /> + </g> + </svg>, + "keyTest": [Function], + "label": [Function], + "name": "hyperlink", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "action": "click", + "category": "hyperlink", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <React.Fragment> + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z" + /> + <path + d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2" + /> + </React.Fragment> + </svg>, + "label": "labels.copyElementLink", + "name": "copyElementLink", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + viewBox="0 0 20 20" + > + <g + strokeWidth="1.25" + > + <path + d="M14.375 6.458H8.958a2.5 2.5 0 0 0-2.5 2.5v5.417a2.5 2.5 0 0 0 2.5 2.5h5.417a2.5 2.5 0 0 0 2.5-2.5V8.958a2.5 2.5 0 0 0-2.5-2.5Z" + /> + <path + clipRule="evenodd" + d="M11.667 3.125c.517 0 .986.21 1.325.55.34.338.55.807.55 1.325v1.458H8.333c-.485 0-.927.185-1.26.487-.343.312-.57.75-.609 1.24l-.005 5.357H5a1.87 1.87 0 0 1-1.326-.55 1.87 1.87 0 0 1-.549-1.325V5c0-.518.21-.987.55-1.326.338-.34.807-.549 1.325-.549h6.667Z" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.duplicateSelection", + "name": "duplicateSelection", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": [Function], + "keyTest": [Function], + "label": [Function], + "name": "toggleElementLock", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + viewBox="0 0 20 20" + > + <path + d="M3.333 5.833h13.334M8.333 9.167v5M11.667 9.167v5M4.167 5.833l.833 10c0 .92.746 1.667 1.667 1.667h6.666c.92 0 1.667-.746 1.667-1.667l.833-10M7.5 5.833v-2.5c0-.46.373-.833.833-.833h3.334c.46 0 .833.373.833.833v2.5" + strokeWidth="1.25" + /> + </svg>, + "keyTest": [Function], + "label": "labels.delete", + "name": "deleteSelectedElements", + "perform": [Function], + "trackEvent": { + "action": "delete", + "category": "element", + }, + }, + ], + "left": -17, + "top": -7, + }, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 100, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 20, + "offsetTop": 10, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id0": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + "id1": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": { + "id4": true, + }, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 200, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`contextMenu element > shows 'Ungroup selection' in context menu for group inside selected elements > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "id4", + ], + "height": 10, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 449462985, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "versionNonce": 81784553, + "width": 10, + "x": -10, + "y": 0, +} +`; + +exports[`contextMenu element > shows 'Ungroup selection' in context menu for group inside selected elements > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "id4", + ], + "height": 10, + "id": "id1", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 238820263, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "versionNonce": 747212839, + "width": 10, + "x": 12, + "y": 0, +} +`; + +exports[`contextMenu element > shows 'Ungroup selection' in context menu for group inside selected elements > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": -10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 12, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedGroupIds": { + "id4": true, + }, + }, + "inserted": { + "selectedGroupIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "groupIds": [ + "id4", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + "id1" => Delta { + "deleted": { + "groupIds": [ + "id4", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + }, + }, + }, + ], +} +`; + +exports[`contextMenu element > shows 'Ungroup selection' in context menu for group inside selected elements > [end of test] number of elements 1`] = `2`; + +exports[`contextMenu element > shows 'Ungroup selection' in context menu for group inside selected elements > [end of test] number of renders 1`] = `11`; + +exports[`contextMenu element > shows context menu for canvas > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": { + "items": [ + { + "keyTest": undefined, + "label": "labels.paste", + "name": "paste", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M14 3v4a1 1 0 0 0 1 1h4" + /> + <path + d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4" + /> + <path + d="M20 15h-1a2 2 0 0 0 -2 2v2a2 2 0 0 0 2 2h1v-3" + /> + <path + d="M5 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6" + /> + <path + d="M11 21v-6l3 6v-6" + /> + </g> + </svg>, + "keyTest": [Function], + "keywords": [ + "png", + "clipboard", + "copy", + ], + "label": "labels.copyAsPng", + "name": "copyAsPng", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M14 3v4a1 1 0 0 0 1 1h4" + /> + <path + d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4" + /> + <path + d="M4 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75" + /> + <path + d="M10 15l2 6l2 -6" + /> + <path + d="M20 15h-1a2 2 0 0 0 -2 2v2a2 2 0 0 0 2 2h1v-3" + /> + </g> + </svg>, + "keywords": [ + "svg", + "clipboard", + "copy", + ], + "label": "labels.copyAsSvg", + "name": "copyAsSvg", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "keywords": [ + "text", + "clipboard", + "copy", + ], + "label": "labels.copyText", + "name": "copyText", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g> + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M8 8m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v6a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z" + /> + <path + d="M12 20v.01" + /> + <path + d="M16 20v.01" + /> + <path + d="M8 20v.01" + /> + <path + d="M4 20v.01" + /> + <path + d="M4 16v.01" + /> + <path + d="M4 12v.01" + /> + <path + d="M4 8v.01" + /> + <path + d="M4 4v.01" + /> + <path + d="M8 4v.01" + /> + <path + d="M12 4v.01" + /> + <path + d="M16 4v.01" + /> + <path + d="M20 4v.01" + /> + <path + d="M20 8v.01" + /> + <path + d="M20 12v.01" + /> + <path + d="M20 16v.01" + /> + <path + d="M20 20v.01" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.selectAll", + "name": "selectAll", + "perform": [Function], + "trackEvent": { + "category": "canvas", + }, + "viewMode": false, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + viewBox="0 0 20 20" + > + <g> + <path + d="M13.542 8.542H6.458a2.5 2.5 0 0 0-2.5 2.5v3.75a2.5 2.5 0 0 0 2.5 2.5h7.084a2.5 2.5 0 0 0 2.5-2.5v-3.75a2.5 2.5 0 0 0-2.5-2.5Z" + stroke="currentColor" + strokeWidth="1.25" + /> + <path + d="M10 13.958a1.042 1.042 0 1 0 0-2.083 1.042 1.042 0 0 0 0 2.083Z" + stroke="currentColor" + strokeWidth="1.25" + /> + <mask + height={9} + id="UnlockedIcon" + maskUnits="userSpaceOnUse" + style={ + { + "maskType": "alpha", + } + } + width={9} + x={6} + y={1} + > + <path + d="M6.399 9.561V5.175c0-.93.401-1.823 1.116-2.48a3.981 3.981 0 0 1 2.693-1.028c1.01 0 1.98.37 2.694 1.027.715.658 1.116 1.55 1.116 2.481" + fill="#fff" + stroke="none" + /> + </mask> + <g + mask="url(#UnlockedIcon)" + > + <path + d="M5.149 9.561v1.25h2.5v-1.25h-2.5Zm5.06-7.894V.417v1.25Zm2.559 3.508v1.25h2.5v-1.25h-2.5ZM7.648 8.51V5.175h-2.5V8.51h2.5Zm0-3.334c0-.564.243-1.128.713-1.561L6.668 1.775c-.959.883-1.52 2.104-1.52 3.4h2.5Zm.713-1.561a2.732 2.732 0 0 1 1.847-.697v-2.5c-1.31 0-2.585.478-3.54 1.358L8.36 3.614Zm1.847-.697c.71 0 1.374.26 1.847.697l1.694-1.839a5.231 5.231 0 0 0-3.54-1.358v2.5Zm1.847.697c.47.433.713.997.713 1.561h2.5c0-1.296-.56-2.517-1.52-3.4l-1.693 1.839Z" + fill="currentColor" + stroke="none" + /> + </g> + </g> + </svg>, + "label": "labels.elementLock.unlockAll", + "name": "unlockAllElements", + "paletteName": "Unlock all elements", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "canvas", + }, + "viewMode": false, + }, + "separator", + { + "checked": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.50000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M3 6h18" + /> + <path + d="M3 12h18" + /> + <path + d="M3 18h18" + /> + <path + d="M6 3v18" + /> + <path + d="M12 3v18" + /> + <path + d="M18 3v18" + /> + </g> + </svg>, + "keyTest": [Function], + "keywords": [ + "snap", + ], + "label": "labels.toggleGrid", + "name": "gridMode", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "canvas", + "predicate": [Function], + }, + "viewMode": true, + }, + { + "checked": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M4 13v-8a2 2 0 0 1 2 -2h1a2 2 0 0 1 2 2v8a2 2 0 0 0 6 0v-8a2 2 0 0 1 2 -2h1a2 2 0 0 1 2 2v8a8 8 0 0 1 -16 0" + /> + <path + d="M4 8l5 0" + /> + <path + d="M15 8l4 0" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "buttons.objectsSnapMode", + "name": "objectsSnapMode", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "canvas", + "predicate": [Function], + }, + "viewMode": false, + }, + { + "checked": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M3 14c.83 .642 2.077 1.017 3.5 1c1.423 .017 2.67 -.358 3.5 -1c.83 -.642 2.077 -1.017 3.5 -1c1.423 -.017 2.67 .358 3.5 1" + /> + <path + d="M8 3a2.4 2.4 0 0 0 -1 2a2.4 2.4 0 0 0 1 2" + /> + <path + d="M12 3a2.4 2.4 0 0 0 -1 2a2.4 2.4 0 0 0 1 2" + /> + <path + d="M3 10h14v5a6 6 0 0 1 -6 6h-2a6 6 0 0 1 -6 -6v-5z" + /> + <path + d="M16.746 16.726a3 3 0 1 0 .252 -5.555" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "buttons.zenMode", + "name": "zenMode", + "paletteName": "Toggle zen mode", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "canvas", + "predicate": [Function], + }, + "viewMode": true, + }, + { + "checked": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + fill="none" + stroke="currentColor" + strokeWidth={"1.50000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" + /> + <path + d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.viewMode", + "name": "viewMode", + "paletteName": "Toggle view mode", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "canvas", + "predicate": [Function], + }, + "viewMode": true, + }, + { + "checked": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M5 3v18" + /> + <path + d="M19 21v-18" + /> + <path + d="M5 7h14" + /> + <path + d="M5 15h14" + /> + <path + d="M8 13v4" + /> + <path + d="M11 13v4" + /> + <path + d="M16 13v4" + /> + <path + d="M14 5v4" + /> + <path + d="M11 5v4" + /> + <path + d="M8 5v4" + /> + <path + d="M3 21h18" + /> + </g> + </svg>, + "keyTest": [Function], + "keywords": [ + "edit", + "attributes", + "customize", + ], + "label": "stats.fullTitle", + "name": "stats", + "paletteName": "Toggle stats", + "perform": [Function], + "trackEvent": { + "category": "menu", + }, + "viewMode": true, + }, + ], + "left": -19, + "top": -9, + }, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 100, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 20, + "offsetTop": 10, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 200, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`contextMenu element > shows context menu for canvas > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [], +} +`; + +exports[`contextMenu element > shows context menu for canvas > [end of test] number of elements 1`] = `0`; + +exports[`contextMenu element > shows context menu for canvas > [end of test] number of renders 1`] = `3`; + +exports[`contextMenu element > shows context menu for element > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": { + "items": [ + "separator", + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M7 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" + /> + <path + d="M17 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" + /> + <path + d="M9.15 14.85l8.85 -10.85" + /> + <path + d="M6 4l8.85 10.85" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.cut", + "name": "cut", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + viewBox="0 0 20 20" + > + <g + strokeWidth="1.25" + > + <path + d="M14.375 6.458H8.958a2.5 2.5 0 0 0-2.5 2.5v5.417a2.5 2.5 0 0 0 2.5 2.5h5.417a2.5 2.5 0 0 0 2.5-2.5V8.958a2.5 2.5 0 0 0-2.5-2.5Z" + /> + <path + clipRule="evenodd" + d="M11.667 3.125c.517 0 .986.21 1.325.55.34.338.55.807.55 1.325v1.458H8.333c-.485 0-.927.185-1.26.487-.343.312-.57.75-.609 1.24l-.005 5.357H5a1.87 1.87 0 0 1-1.326-.55 1.87 1.87 0 0 1-.549-1.325V5c0-.518.21-.987.55-1.326.338-.34.807-.549 1.325-.549h6.667Z" + /> + </g> + </svg>, + "keyTest": undefined, + "label": "labels.copy", + "name": "copy", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "keyTest": undefined, + "label": "labels.paste", + "name": "paste", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "label": "labels.selectAllElementsInFrame", + "name": "selectAllElementsInFrame", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "canvas", + }, + }, + { + "label": "labels.removeAllElementsFromFrame", + "name": "removeAllElementsFromFrame", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "history", + }, + }, + { + "label": "labels.wrapSelectionInFrame", + "name": "wrapSelectionInFrame", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth="1.25" + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M8 5v10a1 1 0 0 0 1 1h10" + /> + <path + d="M5 8h10a1 1 0 0 1 1 1v10" + /> + </g> + </svg>, + "keywords": [ + "image", + "crop", + ], + "label": "helpDialog.cropStart", + "name": "cropEditor", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "menu", + }, + "viewMode": true, + }, + "separator", + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M14 3v4a1 1 0 0 0 1 1h4" + /> + <path + d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4" + /> + <path + d="M20 15h-1a2 2 0 0 0 -2 2v2a2 2 0 0 0 2 2h1v-3" + /> + <path + d="M5 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6" + /> + <path + d="M11 21v-6l3 6v-6" + /> + </g> + </svg>, + "keyTest": [Function], + "keywords": [ + "png", + "clipboard", + "copy", + ], + "label": "labels.copyAsPng", + "name": "copyAsPng", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M14 3v4a1 1 0 0 0 1 1h4" + /> + <path + d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4" + /> + <path + d="M4 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75" + /> + <path + d="M10 15l2 6l2 -6" + /> + <path + d="M20 15h-1a2 2 0 0 0 -2 2v2a2 2 0 0 0 2 2h1v-3" + /> + </g> + </svg>, + "keywords": [ + "svg", + "clipboard", + "copy", + ], + "label": "labels.copyAsSvg", + "name": "copyAsSvg", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "keywords": [ + "text", + "clipboard", + "copy", + ], + "label": "labels.copyText", + "name": "copyText", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M5 3m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z" + /> + <path + d="M19 6h1a2 2 0 0 1 2 2a5 5 0 0 1 -5 5l-5 0v2" + /> + <path + d="M10 15m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.copyStyles", + "name": "copyStyles", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M5 3m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z" + /> + <path + d="M19 6h1a2 2 0 0 1 2 2a5 5 0 0 1 -5 5l-5 0v2" + /> + <path + d="M10 15m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.pasteStyles", + "name": "pasteStyles", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": [Function], + "keyTest": [Function], + "label": "labels.group", + "name": "group", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": null, + "label": "labels.autoResize", + "name": "autoResize", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "label": "labels.unbindText", + "name": "unbindText", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "label": "labels.bindText", + "name": "bindText", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "label": "labels.createContainerFromText", + "name": "wrapTextInContainer", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "PanelComponent": [Function], + "icon": [Function], + "keyTest": [Function], + "label": "labels.ungroup", + "name": "ungroup", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "label": "labels.addToLibrary", + "name": "addToLibrary", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + style={ + { + "transform": "rotate(180deg)", + } + } + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.50000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M12 5l0 14" + /> + <path + d="M16 9l-4 -4" + /> + <path + d="M8 9l4 -4" + /> + </g> + </svg>, + "keyPriority": 40, + "keyTest": [Function], + "keywords": [ + "move down", + "zindex", + "layer", + ], + "label": "labels.sendBackward", + "name": "sendBackward", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.50000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M12 5l0 14" + /> + <path + d="M16 9l-4 -4" + /> + <path + d="M8 9l4 -4" + /> + </g> + </svg>, + "keyPriority": 40, + "keyTest": [Function], + "keywords": [ + "move up", + "zindex", + "layer", + ], + "label": "labels.bringForward", + "name": "bringForward", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + style={ + { + "transform": "rotate(180deg)", + } + } + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.50000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M12 10l0 10" + /> + <path + d="M12 10l4 4" + /> + <path + d="M12 10l-4 4" + /> + <path + d="M4 4l16 0" + /> + </g> + </svg>, + "keyTest": [Function], + "keywords": [ + "move down", + "zindex", + "layer", + ], + "label": "labels.sendToBack", + "name": "sendToBack", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.50000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M12 10l0 10" + /> + <path + d="M12 10l4 4" + /> + <path + d="M12 10l-4 4" + /> + <path + d="M4 4l16 0" + /> + </g> + </svg>, + "keyTest": [Function], + "keywords": [ + "move up", + "zindex", + "layer", + ], + "label": "labels.bringToFront", + "name": "bringToFront", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M12 3l0 18" + /> + <path + d="M16 7l0 10l5 0l-5 -10" + /> + <path + d="M8 7l0 10l-5 0l5 -10" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.flipHorizontal", + "name": "flipHorizontal", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M3 12l18 0" + /> + <path + d="M7 16l10 0l-10 5l0 -5" + /> + <path + d="M7 8l10 0l-10 -5l0 5" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.flipVertical", + "name": "flipVertical", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "category": "Elements", + "keywords": [ + "line", + ], + "label": [Function], + "name": "toggleLinearEditor", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + viewBox="0 0 20 20" + > + <g + strokeWidth="1.25" + > + <path + d="M8.333 11.667a2.917 2.917 0 0 0 4.167 0l3.333-3.334a2.946 2.946 0 1 0-4.166-4.166l-.417.416" + /> + <path + d="M11.667 8.333a2.917 2.917 0 0 0-4.167 0l-3.333 3.334a2.946 2.946 0 0 0 4.166 4.166l.417-.416" + /> + </g> + </svg>, + "keyTest": [Function], + "label": [Function], + "name": "hyperlink", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "action": "click", + "category": "hyperlink", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <React.Fragment> + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z" + /> + <path + d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2" + /> + </React.Fragment> + </svg>, + "label": "labels.copyElementLink", + "name": "copyElementLink", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + viewBox="0 0 20 20" + > + <g + strokeWidth="1.25" + > + <path + d="M14.375 6.458H8.958a2.5 2.5 0 0 0-2.5 2.5v5.417a2.5 2.5 0 0 0 2.5 2.5h5.417a2.5 2.5 0 0 0 2.5-2.5V8.958a2.5 2.5 0 0 0-2.5-2.5Z" + /> + <path + clipRule="evenodd" + d="M11.667 3.125c.517 0 .986.21 1.325.55.34.338.55.807.55 1.325v1.458H8.333c-.485 0-.927.185-1.26.487-.343.312-.57.75-.609 1.24l-.005 5.357H5a1.87 1.87 0 0 1-1.326-.55 1.87 1.87 0 0 1-.549-1.325V5c0-.518.21-.987.55-1.326.338-.34.807-.549 1.325-.549h6.667Z" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.duplicateSelection", + "name": "duplicateSelection", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": [Function], + "keyTest": [Function], + "label": [Function], + "name": "toggleElementLock", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + viewBox="0 0 20 20" + > + <path + d="M3.333 5.833h13.334M8.333 9.167v5M11.667 9.167v5M4.167 5.833l.833 10c0 .92.746 1.667 1.667 1.667h6.666c.92 0 1.667-.746 1.667-1.667l.833-10M7.5 5.833v-2.5c0-.46.373-.833.833-.833h3.334c.46 0 .833.373.833.833v2.5" + strokeWidth="1.25" + /> + </svg>, + "keyTest": [Function], + "label": "labels.delete", + "name": "deleteSelectedElements", + "perform": [Function], + "trackEvent": { + "action": "delete", + "category": "element", + }, + }, + ], + "left": -17, + "top": -7, + }, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 100, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 20, + "offsetTop": 10, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 200, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`contextMenu element > shows context menu for element > [end of test] appState 2`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": { + "items": [ + "separator", + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M7 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" + /> + <path + d="M17 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" + /> + <path + d="M9.15 14.85l8.85 -10.85" + /> + <path + d="M6 4l8.85 10.85" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.cut", + "name": "cut", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + viewBox="0 0 20 20" + > + <g + strokeWidth="1.25" + > + <path + d="M14.375 6.458H8.958a2.5 2.5 0 0 0-2.5 2.5v5.417a2.5 2.5 0 0 0 2.5 2.5h5.417a2.5 2.5 0 0 0 2.5-2.5V8.958a2.5 2.5 0 0 0-2.5-2.5Z" + /> + <path + clipRule="evenodd" + d="M11.667 3.125c.517 0 .986.21 1.325.55.34.338.55.807.55 1.325v1.458H8.333c-.485 0-.927.185-1.26.487-.343.312-.57.75-.609 1.24l-.005 5.357H5a1.87 1.87 0 0 1-1.326-.55 1.87 1.87 0 0 1-.549-1.325V5c0-.518.21-.987.55-1.326.338-.34.807-.549 1.325-.549h6.667Z" + /> + </g> + </svg>, + "keyTest": undefined, + "label": "labels.copy", + "name": "copy", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "keyTest": undefined, + "label": "labels.paste", + "name": "paste", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "label": "labels.selectAllElementsInFrame", + "name": "selectAllElementsInFrame", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "canvas", + }, + }, + { + "label": "labels.removeAllElementsFromFrame", + "name": "removeAllElementsFromFrame", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "history", + }, + }, + { + "label": "labels.wrapSelectionInFrame", + "name": "wrapSelectionInFrame", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth="1.25" + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M8 5v10a1 1 0 0 0 1 1h10" + /> + <path + d="M5 8h10a1 1 0 0 1 1 1v10" + /> + </g> + </svg>, + "keywords": [ + "image", + "crop", + ], + "label": "helpDialog.cropStart", + "name": "cropEditor", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "menu", + }, + "viewMode": true, + }, + "separator", + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M14 3v4a1 1 0 0 0 1 1h4" + /> + <path + d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4" + /> + <path + d="M20 15h-1a2 2 0 0 0 -2 2v2a2 2 0 0 0 2 2h1v-3" + /> + <path + d="M5 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6" + /> + <path + d="M11 21v-6l3 6v-6" + /> + </g> + </svg>, + "keyTest": [Function], + "keywords": [ + "png", + "clipboard", + "copy", + ], + "label": "labels.copyAsPng", + "name": "copyAsPng", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M14 3v4a1 1 0 0 0 1 1h4" + /> + <path + d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4" + /> + <path + d="M4 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75" + /> + <path + d="M10 15l2 6l2 -6" + /> + <path + d="M20 15h-1a2 2 0 0 0 -2 2v2a2 2 0 0 0 2 2h1v-3" + /> + </g> + </svg>, + "keywords": [ + "svg", + "clipboard", + "copy", + ], + "label": "labels.copyAsSvg", + "name": "copyAsSvg", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "keywords": [ + "text", + "clipboard", + "copy", + ], + "label": "labels.copyText", + "name": "copyText", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M5 3m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z" + /> + <path + d="M19 6h1a2 2 0 0 1 2 2a5 5 0 0 1 -5 5l-5 0v2" + /> + <path + d="M10 15m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.copyStyles", + "name": "copyStyles", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M5 3m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z" + /> + <path + d="M19 6h1a2 2 0 0 1 2 2a5 5 0 0 1 -5 5l-5 0v2" + /> + <path + d="M10 15m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.pasteStyles", + "name": "pasteStyles", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": [Function], + "keyTest": [Function], + "label": "labels.group", + "name": "group", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": null, + "label": "labels.autoResize", + "name": "autoResize", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "label": "labels.unbindText", + "name": "unbindText", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "label": "labels.bindText", + "name": "bindText", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "label": "labels.createContainerFromText", + "name": "wrapTextInContainer", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "PanelComponent": [Function], + "icon": [Function], + "keyTest": [Function], + "label": "labels.ungroup", + "name": "ungroup", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "label": "labels.addToLibrary", + "name": "addToLibrary", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + style={ + { + "transform": "rotate(180deg)", + } + } + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.50000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M12 5l0 14" + /> + <path + d="M16 9l-4 -4" + /> + <path + d="M8 9l4 -4" + /> + </g> + </svg>, + "keyPriority": 40, + "keyTest": [Function], + "keywords": [ + "move down", + "zindex", + "layer", + ], + "label": "labels.sendBackward", + "name": "sendBackward", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.50000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M12 5l0 14" + /> + <path + d="M16 9l-4 -4" + /> + <path + d="M8 9l4 -4" + /> + </g> + </svg>, + "keyPriority": 40, + "keyTest": [Function], + "keywords": [ + "move up", + "zindex", + "layer", + ], + "label": "labels.bringForward", + "name": "bringForward", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + style={ + { + "transform": "rotate(180deg)", + } + } + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.50000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M12 10l0 10" + /> + <path + d="M12 10l4 4" + /> + <path + d="M12 10l-4 4" + /> + <path + d="M4 4l16 0" + /> + </g> + </svg>, + "keyTest": [Function], + "keywords": [ + "move down", + "zindex", + "layer", + ], + "label": "labels.sendToBack", + "name": "sendToBack", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.50000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M12 10l0 10" + /> + <path + d="M12 10l4 4" + /> + <path + d="M12 10l-4 4" + /> + <path + d="M4 4l16 0" + /> + </g> + </svg>, + "keyTest": [Function], + "keywords": [ + "move up", + "zindex", + "layer", + ], + "label": "labels.bringToFront", + "name": "bringToFront", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M12 3l0 18" + /> + <path + d="M16 7l0 10l5 0l-5 -10" + /> + <path + d="M8 7l0 10l-5 0l5 -10" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.flipHorizontal", + "name": "flipHorizontal", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <g + strokeWidth={"1.25000"} + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M3 12l18 0" + /> + <path + d="M7 16l10 0l-10 5l0 -5" + /> + <path + d="M7 8l10 0l-10 -5l0 5" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.flipVertical", + "name": "flipVertical", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "category": "Elements", + "keywords": [ + "line", + ], + "label": [Function], + "name": "toggleLinearEditor", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + viewBox="0 0 20 20" + > + <g + strokeWidth="1.25" + > + <path + d="M8.333 11.667a2.917 2.917 0 0 0 4.167 0l3.333-3.334a2.946 2.946 0 1 0-4.166-4.166l-.417.416" + /> + <path + d="M11.667 8.333a2.917 2.917 0 0 0-4.167 0l-3.333 3.334a2.946 2.946 0 0 0 4.166 4.166l.417-.416" + /> + </g> + </svg>, + "keyTest": [Function], + "label": [Function], + "name": "hyperlink", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "action": "click", + "category": "hyperlink", + }, + }, + { + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + viewBox="0 0 24 24" + > + <React.Fragment> + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z" + /> + <path + d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2" + /> + </React.Fragment> + </svg>, + "label": "labels.copyElementLink", + "name": "copyElementLink", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + viewBox="0 0 20 20" + > + <g + strokeWidth="1.25" + > + <path + d="M14.375 6.458H8.958a2.5 2.5 0 0 0-2.5 2.5v5.417a2.5 2.5 0 0 0 2.5 2.5h5.417a2.5 2.5 0 0 0 2.5-2.5V8.958a2.5 2.5 0 0 0-2.5-2.5Z" + /> + <path + clipRule="evenodd" + d="M11.667 3.125c.517 0 .986.21 1.325.55.34.338.55.807.55 1.325v1.458H8.333c-.485 0-.927.185-1.26.487-.343.312-.57.75-.609 1.24l-.005 5.357H5a1.87 1.87 0 0 1-1.326-.55 1.87 1.87 0 0 1-.549-1.325V5c0-.518.21-.987.55-1.326.338-.34.807-.549 1.325-.549h6.667Z" + /> + </g> + </svg>, + "keyTest": [Function], + "label": "labels.duplicateSelection", + "name": "duplicateSelection", + "perform": [Function], + "trackEvent": { + "category": "element", + }, + }, + { + "icon": [Function], + "keyTest": [Function], + "label": [Function], + "name": "toggleElementLock", + "perform": [Function], + "predicate": [Function], + "trackEvent": { + "category": "element", + }, + }, + "separator", + { + "PanelComponent": [Function], + "icon": <svg + aria-hidden="true" + className="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + viewBox="0 0 20 20" + > + <path + d="M3.333 5.833h13.334M8.333 9.167v5M11.667 9.167v5M4.167 5.833l.833 10c0 .92.746 1.667 1.667 1.667h6.666c.92 0 1.667-.746 1.667-1.667l.833-10M7.5 5.833v-2.5c0-.46.373-.833.833-.833h3.334c.46 0 .833.373.833.833v2.5" + strokeWidth="1.25" + /> + </svg>, + "keyTest": [Function], + "label": "labels.delete", + "name": "deleteSelectedElements", + "perform": [Function], + "trackEvent": { + "action": "delete", + "category": "element", + }, + }, + ], + "left": 80, + "top": 90, + }, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 100, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 20, + "offsetTop": 10, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id1": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 200, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`contextMenu element > shows context menu for element > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 449462985, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 3, + "versionNonce": 1150084233, + "width": 20, + "x": -10, + "y": 0, +} +`; + +exports[`contextMenu element > shows context menu for element > [end of test] element 0 2`] = ` +{ + "angle": 0, + "backgroundColor": "red", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 200, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "versionNonce": 1278240551, + "width": 200, + "x": 0, + "y": 0, +} +`; + +exports[`contextMenu element > shows context menu for element > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "red", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 200, + "id": "id1", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "versionNonce": 449462985, + "width": 200, + "x": 0, + "y": 0, +} +`; + +exports[`contextMenu element > shows context menu for element > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 20, + "x": -10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`contextMenu element > shows context menu for element > [end of test] history 2`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [], +} +`; + +exports[`contextMenu element > shows context menu for element > [end of test] number of elements 1`] = `1`; + +exports[`contextMenu element > shows context menu for element > [end of test] number of elements 2`] = `2`; + +exports[`contextMenu element > shows context menu for element > [end of test] number of renders 1`] = `5`; + +exports[`contextMenu element > shows context menu for element > [end of test] number of renders 2`] = `6`; diff --git a/packages/excalidraw/tests/__snapshots__/dragCreate.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/dragCreate.test.tsx.snap new file mode 100644 index 0000000..acc9b79 --- /dev/null +++ b/packages/excalidraw/tests/__snapshots__/dragCreate.test.tsx.snap @@ -0,0 +1,210 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Test dragCreate > add element to the scene when pointer dragging long enough > arrow 3`] = `1`; + +exports[`Test dragCreate > add element to the scene when pointer dragging long enough > arrow 4`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 50, + "id": "id0", + "index": "a0", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 30, + 50, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "seed": 1278240551, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 4, + "versionNonce": 2019559783, + "width": 30, + "x": 30, + "y": 20, +} +`; + +exports[`Test dragCreate > add element to the scene when pointer dragging long enough > diamond 3`] = `1`; + +exports[`Test dragCreate > add element to the scene when pointer dragging long enough > diamond 4`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 50, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "seed": 1278240551, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "diamond", + "updated": 1, + "version": 3, + "versionNonce": 401146281, + "width": 30, + "x": 30, + "y": 20, +} +`; + +exports[`Test dragCreate > add element to the scene when pointer dragging long enough > ellipse 3`] = `1`; + +exports[`Test dragCreate > add element to the scene when pointer dragging long enough > ellipse 4`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 50, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "seed": 1278240551, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "ellipse", + "updated": 1, + "version": 3, + "versionNonce": 401146281, + "width": 30, + "x": 30, + "y": 20, +} +`; + +exports[`Test dragCreate > add element to the scene when pointer dragging long enough > line 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "endArrowhead": null, + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 50, + "id": "id0", + "index": "a0", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 30, + 50, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "seed": 1278240551, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "line", + "updated": 1, + "version": 4, + "versionNonce": 2019559783, + "width": 30, + "x": 30, + "y": 20, +} +`; + +exports[`Test dragCreate > add element to the scene when pointer dragging long enough > rectangle 3`] = `1`; + +exports[`Test dragCreate > add element to the scene when pointer dragging long enough > rectangle 4`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 50, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1278240551, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 3, + "versionNonce": 401146281, + "width": 30, + "x": 30, + "y": 20, +} +`; diff --git a/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap new file mode 100644 index 0000000..e5e431d --- /dev/null +++ b/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap @@ -0,0 +1,654 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu items if passed from host 1`] = ` +<div + class="dropdown-menu" + data-testid="dropdown-menu" +> + <div + class="Island dropdown-menu-container" + style="--padding: 2; z-index: 2;" + > + <button + class="dropdown-menu-item dropdown-menu-item-base" + > + <div + class="dropdown-menu-item__text" + > + Click me + </div> + </button> + <a + class="dropdown-menu-item dropdown-menu-item-base" + href="blog.excalidaw.com" + rel="noreferrer" + target="_blank" + > + <div + class="dropdown-menu-item__text" + > + Excalidraw blog + </div> + </a> + <div + class="dropdown-menu-item-base dropdown-menu-item-custom" + > + <button + style="height: 2rem;" + > + custom menu item + </button> + </div> + <button + aria-label="Help" + class="dropdown-menu-item dropdown-menu-item-base" + data-testid="help-menu-item" + title="Help" + > + <div + class="dropdown-menu-item__icon" + > + <svg + aria-hidden="true" + class="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + viewBox="0 0 24 24" + > + <g + stroke-width="1.5" + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <circle + cx="12" + cy="12" + r="9" + /> + <line + x1="12" + x2="12" + y1="17" + y2="17.01" + /> + <path + d="M12 13.5a1.5 1.5 0 0 1 1 -1.5a2.6 2.6 0 1 0 -3 -4" + /> + </g> + </svg> + </div> + <div + class="dropdown-menu-item__text" + > + Help + </div> + <div + class="dropdown-menu-item__shortcut" + > + ? + </div> + </button> + </div> +</div> +`; + +exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should render menu with default items when "UIOPtions" is "undefined" 1`] = ` +<div + class="dropdown-menu" + data-testid="dropdown-menu" +> + <div + class="Island dropdown-menu-container" + style="--padding: 2; z-index: 2;" + > + <button + aria-label="Open" + class="dropdown-menu-item dropdown-menu-item-base" + data-testid="load-button" + title="Open" + > + <div + class="dropdown-menu-item__icon" + > + <svg + aria-hidden="true" + class="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + viewBox="0 0 20 20" + > + <path + d="m9.257 6.351.183.183H15.819c.34 0 .727.182 1.051.506.323.323.505.708.505 1.05v5.819c0 .316-.183.7-.52 1.035-.337.338-.723.522-1.037.522H4.182c-.352 0-.74-.181-1.058-.5-.318-.318-.499-.705-.499-1.057V5.182c0-.351.181-.736.5-1.054.32-.321.71-.503 1.057-.503H6.53l2.726 2.726Z" + stroke-width="1.25" + /> + </svg> + </div> + <div + class="dropdown-menu-item__text" + > + Open + </div> + <div + class="dropdown-menu-item__shortcut" + > + Ctrl+O + </div> + </button> + <button + aria-label="Save to..." + class="dropdown-menu-item dropdown-menu-item-base" + data-testid="json-export-button" + title="Save to..." + > + <div + class="dropdown-menu-item__icon" + > + <svg + aria-hidden="true" + class="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + viewBox="0 0 20 20" + > + <path + d="M3.333 14.167v1.666c0 .92.747 1.667 1.667 1.667h10c.92 0 1.667-.746 1.667-1.667v-1.666M5.833 9.167 10 13.333l4.167-4.166M10 3.333v10" + stroke-width="1.25" + /> + </svg> + </div> + <div + class="dropdown-menu-item__text" + > + Save to... + </div> + </button> + <button + aria-label="Export image..." + class="dropdown-menu-item dropdown-menu-item-base" + data-testid="image-export-button" + title="Export image..." + > + <div + class="dropdown-menu-item__icon" + > + <svg + aria-hidden="true" + class="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + viewBox="0 0 24 24" + > + <g + stroke-width="1.25" + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M15 8h.01" + /> + <path + d="M12 20h-5a3 3 0 0 1 -3 -3v-10a3 3 0 0 1 3 -3h10a3 3 0 0 1 3 3v5" + /> + <path + d="M4 15l4 -4c.928 -.893 2.072 -.893 3 0l4 4" + /> + <path + d="M14 14l1 -1c.617 -.593 1.328 -.793 2.009 -.598" + /> + <path + d="M19 16v6" + /> + <path + d="M22 19l-3 3l-3 -3" + /> + </g> + </svg> + </div> + <div + class="dropdown-menu-item__text" + > + Export image... + </div> + <div + class="dropdown-menu-item__shortcut" + > + Ctrl+Shift+E + </div> + </button> + <button + aria-label="Find on canvas" + class="dropdown-menu-item dropdown-menu-item-base" + data-testid="search-menu-button" + title="Find on canvas" + > + <div + class="dropdown-menu-item__icon" + > + <svg + aria-hidden="true" + class="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + viewBox="0 0 24 24" + > + <g + stroke-width="1.5" + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" + /> + <path + d="M21 21l-6 -6" + /> + </g> + </svg> + </div> + <div + class="dropdown-menu-item__text" + > + Find on canvas + </div> + <div + class="dropdown-menu-item__shortcut" + > + Ctrl+F + </div> + </button> + <button + aria-label="Help" + class="dropdown-menu-item dropdown-menu-item-base" + data-testid="help-menu-item" + title="Help" + > + <div + class="dropdown-menu-item__icon" + > + <svg + aria-hidden="true" + class="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + viewBox="0 0 24 24" + > + <g + stroke-width="1.5" + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <circle + cx="12" + cy="12" + r="9" + /> + <line + x1="12" + x2="12" + y1="17" + y2="17.01" + /> + <path + d="M12 13.5a1.5 1.5 0 0 1 1 -1.5a2.6 2.6 0 1 0 -3 -4" + /> + </g> + </svg> + </div> + <div + class="dropdown-menu-item__text" + > + Help + </div> + <div + class="dropdown-menu-item__shortcut" + > + ? + </div> + </button> + <button + aria-label="Reset the canvas" + class="dropdown-menu-item dropdown-menu-item-base" + data-testid="clear-canvas-button" + title="Reset the canvas" + > + <div + class="dropdown-menu-item__icon" + > + <svg + aria-hidden="true" + class="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + viewBox="0 0 20 20" + > + <path + d="M3.333 5.833h13.334M8.333 9.167v5M11.667 9.167v5M4.167 5.833l.833 10c0 .92.746 1.667 1.667 1.667h6.666c.92 0 1.667-.746 1.667-1.667l.833-10M7.5 5.833v-2.5c0-.46.373-.833.833-.833h3.334c.46 0 .833.373.833.833v2.5" + stroke-width="1.25" + /> + </svg> + </div> + <div + class="dropdown-menu-item__text" + > + Reset the canvas + </div> + </button> + <div + style="height: 1px; margin: .5rem 0px;" + /> + <div + class="dropdown-menu-group " + > + <p + class="dropdown-menu-group-title" + > + Excalidraw links + </p> + <a + aria-label="GitHub" + class="dropdown-menu-item dropdown-menu-item-base" + href="https://github.com/excalidraw/excalidraw" + rel="noreferrer" + target="_blank" + title="GitHub" + > + <div + class="dropdown-menu-item__icon" + > + <svg + aria-hidden="true" + class="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + viewBox="0 0 20 20" + > + <path + d="M7.5 15.833c-3.583 1.167-3.583-2.083-5-2.5m10 4.167v-2.917c0-.833.083-1.166-.417-1.666 2.334-.25 4.584-1.167 4.584-5a3.833 3.833 0 0 0-1.084-2.667 3.5 3.5 0 0 0-.083-2.667s-.917-.25-2.917 1.084a10.25 10.25 0 0 0-5.166 0C5.417 2.333 4.5 2.583 4.5 2.583a3.5 3.5 0 0 0-.083 2.667 3.833 3.833 0 0 0-1.084 2.667c0 3.833 2.25 4.75 4.584 5-.5.5-.5 1-.417 1.666V17.5" + stroke-width="1.25" + /> + </svg> + </div> + <div + class="dropdown-menu-item__text" + > + GitHub + </div> + </a> + <a + aria-label="X" + class="dropdown-menu-item dropdown-menu-item-base" + href="https://x.com/excalidraw" + rel="noreferrer" + target="_blank" + title="X" + > + <div + class="dropdown-menu-item__icon" + > + <svg + aria-hidden="true" + class="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + viewBox="0 0 24 24" + > + <g + stroke-width="1.25" + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M4 4l11.733 16h4.267l-11.733 -16z" + /> + <path + d="M4 20l6.768 -6.768m2.46 -2.46l6.772 -6.772" + /> + </g> + </svg> + </div> + <div + class="dropdown-menu-item__text" + > + Follow us + </div> + </a> + <a + aria-label="Discord" + class="dropdown-menu-item dropdown-menu-item-base" + href="https://discord.gg/UexuTaE" + rel="noreferrer" + target="_blank" + title="Discord" + > + <div + class="dropdown-menu-item__icon" + > + <svg + aria-hidden="true" + class="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + viewBox="0 0 20 20" + > + <g + stroke-width="1.25" + > + <path + d="M7.5 10.833a.833.833 0 1 0 0-1.666.833.833 0 0 0 0 1.666ZM12.5 10.833a.833.833 0 1 0 0-1.666.833.833 0 0 0 0 1.666ZM6.25 6.25c2.917-.833 4.583-.833 7.5 0M5.833 13.75c2.917.833 5.417.833 8.334 0" + /> + <path + d="M12.917 14.167c0 .833 1.25 2.5 1.666 2.5 1.25 0 2.361-1.39 2.917-2.5.556-1.39.417-4.861-1.25-9.584-1.214-.846-2.5-1.116-3.75-1.25l-.833 2.084M7.083 14.167c0 .833-1.13 2.5-1.526 2.5-1.191 0-2.249-1.39-2.778-2.5-.529-1.39-.397-4.861 1.19-9.584 1.157-.846 2.318-1.116 3.531-1.25l.833 2.084" + /> + </g> + </svg> + </div> + <div + class="dropdown-menu-item__text" + > + Discord chat + </div> + </a> + </div> + <div + style="height: 1px; margin: .5rem 0px;" + /> + <button + aria-label="Dark mode" + class="dropdown-menu-item dropdown-menu-item-base" + data-testid="toggle-dark-mode" + title="Dark mode" + > + <div + class="dropdown-menu-item__icon" + > + <svg + aria-hidden="true" + class="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + viewBox="0 0 20 20" + > + <path + clip-rule="evenodd" + d="M10 2.5h.328a6.25 6.25 0 0 0 6.6 10.372A7.5 7.5 0 1 1 10 2.493V2.5Z" + stroke="currentColor" + /> + </svg> + </div> + <div + class="dropdown-menu-item__text" + > + Dark mode + </div> + <div + class="dropdown-menu-item__shortcut" + > + Shift+Alt+D + </div> + </button> + <div + style="margin-top: 0.5rem;" + > + <div + data-testid="canvas-background-label" + style="font-size: .75rem; margin-bottom: .5rem;" + > + Canvas background + </div> + <div + style="padding: 0px 0.625rem;" + > + <div> + <div + aria-modal="true" + class="color-picker-container" + role="dialog" + > + <div + class="color-picker__top-picks" + > + <button + class="color-picker__button active" + data-testid="color-top-pick-#ffffff" + style="--swatch-color: #ffffff;" + title="#ffffff" + type="button" + > + <div + class="color-picker__button-outline" + /> + </button> + <button + class="color-picker__button" + data-testid="color-top-pick-#f8f9fa" + style="--swatch-color: #f8f9fa;" + title="#f8f9fa" + type="button" + > + <div + class="color-picker__button-outline" + /> + </button> + <button + class="color-picker__button" + data-testid="color-top-pick-#f5faff" + style="--swatch-color: #f5faff;" + title="#f5faff" + type="button" + > + <div + class="color-picker__button-outline" + /> + </button> + <button + class="color-picker__button" + data-testid="color-top-pick-#fffce8" + style="--swatch-color: #fffce8;" + title="#fffce8" + type="button" + > + <div + class="color-picker__button-outline" + /> + </button> + <button + class="color-picker__button" + data-testid="color-top-pick-#fdf8f6" + style="--swatch-color: #fdf8f6;" + title="#fdf8f6" + type="button" + > + <div + class="color-picker__button-outline" + /> + </button> + </div> + <div + style="width: 1px; height: 1rem; margin: 0px auto;" + /> + <button + aria-controls="radix-:r0:" + aria-expanded="false" + aria-haspopup="dialog" + aria-label="Canvas background" + class="color-picker__button active-color properties-trigger" + data-state="closed" + style="--swatch-color: #ffffff;" + title="Show background color picker" + type="button" + > + <div + class="color-picker__button-outline" + /> + </button> + </div> + </div> + </div> + </div> + </div> +</div> +`; diff --git a/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap new file mode 100644 index 0000000..7f766e1 --- /dev/null +++ b/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap @@ -0,0 +1,11 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`export > export svg-embedded scene > svg-embdedded scene export output 1`] = ` +"<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="36" height="36"><!-- svg-source:excalidraw --><metadata><!-- payload-type:application/vnd.excalidraw+json --><!-- payload-version:2 --><!-- payload-start -->eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nHVTTU/jMFx1MDAxML33V0TmulrSXCL20Fx1MDAxYrtcdTAwMGKCw+5hi8RcdTAwMDFxMPE0XHUwMDE51bUte0JbUCV+xt72L/JcdTAwMTNcdTAwMTi7JW5SNpFcIvnN15vnl5dRUVxi2jhcdTAwMTDTQsC6klx1MDAxYZWXK/El4k/gXHUwMDAzWsOhSTpcdTAwMDfb+iplNkRuenqqLVx1MDAxNzQ20PSsLMtdXHUwMDExaFiCocBp93wuipf05Vxiqlh6kdJcdTAwMTLwMZdgTVx1MDAxOV0zVHanTe+0QkVcciPjb1x1MDAxZNRcdTAwMDDWXHL1MWlqXHK9wkDeLuCH1dbHiSdjiG9cdTAwMWX6KKtF7W1rVJdDXprgpOdlct5cdTAwMWO1ntEmdWc9WC0xmHG3pzhcdTAwMTng/6vioXVjIETBxlx1MDAxZGqdrJDi8uMyb1x1MDAxMVx1MDAxObpcdTAwMWKVtH3InLxcXMJNXHUwMDE017RadzBcdTAwMWFcdTAwMDXrIZhW3E/7uJh8XHUwMDEzZ3tkm7lcdTAwMDOoXHUwMDFlseyJI+y3NVVfdVxmP9lcdTAwMGWUWsylXHUwMDBlkPWOPC6zVXokW6ckXHLmajSLYVx1MDAxZdtv8UnvZCdcdTAwMTb67d/f14Obs4Zm+Fx1MDAxY1x0TspcdTAwMWV6JZeoo9TnvVx1MDAxNlx1MDAxN1x1MDAxYeu4p9AwP3BcdTAwMDAvS8i278JkXY5W3E+iXHUwMDAxf3xcdTAwMWbWY41G6ttP6cmW7Fx1MDAxZlxiO4LkWzjcXHUwMDFjrjuTf52cp8CWv8lcdTAwMDJCOjcj1qu7UbZcdKrBqjuMwOU1XHUwMDEz9MsquDTyUVx1MDAwZnVcdTAwMTRPXGKr78d/xck8PWK0d0n8IyC5aTvavlx1MDAwM9lcdTAwMDIhXHUwMDFjIn0=<!-- payload-end --></metadata><defs><style class="style-fonts"> + </style></defs><rect x="0" y="0" width="36" height="36" fill="#ffffff"></rect><g transform="translate(10 10) rotate(0 8 8)" data-id="A"><text x="0" y="17.619999999999997" font-family="Excalifont, Xiaolai, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">😀</text></g></svg>" +`; + +exports[`export > exporting svg containing transformed images > svg export output 1`] = ` +"<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 201.06601717798213 261.4213562373095" width="201.06601717798213" height="261.4213562373095"><!-- svg-source:excalidraw --><metadata></metadata><defs><symbol id="image-file_A"><image href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAT4AAAFPCAYAAADURqJUAAAAAXNSR0IArs4c6QAAIABJREFUeF7tnXeYFUXWxl9yzpIkOOSgJF1UEFFEUQHzmhUVEyZEV7/dNQAiuooBMWBARcREEjAgriJZFBMgeWDIMOSBIcfvOcO96wxzQ3dV9b1dt996Hh74o86pU79T96W6q7qqAFhIgARIIGAECgSsv+wuCZAACYDCx0FAAiQQOAIUvsClnB0mARKg8HEMkAAJBI4AhS9wKWeHSYAEKHwcAyRAAoEjQOELXMrZYRIgAQofxwAJkEDgCFD4ApdydpgESIDCxzFAAiQQOAIUvsClnB0mARKg8HEMkAAJBI4AhS9wKWeHSYAEKHwcAyRAAoEjQOELXMrZYRIgAQofxwAJkEDgCFD4ApdydpgESIDCxzFAAiQQOAIUvsClnB0mARKg8HEMkAAJBI4AhS9wKWeHSYAEKHwcAyRAAoEjQOELXMrZYRIgAQofxwAJkEDgCFD4ApdydpgESIDCxzFAAiQQOAIUvsClnB0mARKg8HEMkAAJBI4AhS9wKWeHSYAEKHwcAyRAAoEjQOELXMrZYRIgAQofxwAJkEDgCFD4ApdydpgESIDCxzFAAiQQOAIUvsClnB0mARKg8HEMkAAJBI4AhS9wKWeHSYAEKHwcAyRAAoEjQOELXMrZYRIgAQofxwAJkEDgCFD4ApdydpgESIDCxzFAAiQQOAIUvsClnB0mARKg8HEMkAAJBI4AhS9wKWeHSYAEKHzJHQNNAbQF0A5AIwCVAVQCUB7AFgCbAawHMAvAD6G/9zkMuQuAZgBOBlAXQNWQ/7Ihn+J7OYCpAKYAmOfQb3UA54X8iu9aIb81AewFkAVgeyh2iXtiqA2H7lmNBLwnQOHznnGkFv4OoHdImNxEsBvAOwBeDInX8bYimvcBuDckdG58pwN4BcDgKEatADwE4GY3TkN19wAYAaAPgDUK9jQhAaMEKHxGccZ1djaAQQBERHSLCODjoZmh+LoHwAAApTUdy0zzWQADQ35KAXgVQHdNv2FzEdd+oVmhIZd0QwLuCFD43PFSrV0CwPMAHlB1EMVuB4CnAXQEcLFh3wtDgvcYgNqGfWcCuNDF47Xh5uku6AQofN6PAHn3Je/nGnjflFUtZAO4PMTGqsAZrP0EKHze5lBEbzqANG+bsdr7FQDGWd0DBm8dAQqfdymj6Dljux9AGwB/OKvOWiSgT4DCp88wkgfh+puhRQxvIvSX1w0AWoS2wPgrMkaTkgQofN6ktVeuVVFvWkg9r2MBXJl63WKP/EiAwmc+KxUBrDKwrcR8ZP73eA6Aaf4PkxHaToDCZz6DskftSfNuA+FRttCcAuBoIHrLTiaNAIXPLHrZPCxfJsgnZyxqBLoBGK5mSisScEaAwueMk9NatwF432ll1otI4BsAncmGBLwkQOEzS3ckgKvNugyctwMAKgCQ73tZSMATAhQ+c1gLAdgKoJw5l4H1dBWAzwPbe3bccwIUPmeIOwG4LnSaisxG5I+s3sr+swwAawHsBHCnM3eRa13eFnjwciCtKrByI/DUR8AUp4dFxWm4z03Auc2P+Z6TAdz2EpC1SyfaY7blSwMD7z7mO2v3sXglbk3fzwH4t3509EACkQlQ+KKPDDkb758A5GW7/NvT0vdmQMTp+HLFU8C4H/WaHtsHEFHNXUSY6tyiJ1AieiuGHRO/3GXOcqCVHIylXj5SPP5KvUVaBooAhS9/uuUYpv8D8A8A8m/PiwjH9jGRmxGBqiAPfopFZmKTX4hsPOw74FY52U+xfPAIcMsFkY0fegt4RbYkqxU5GLWDmimtSCA+AQpfXkZyvNOniZjh5W42ljhJPZk9ySxKpUSbSYov3ZnZH4OBlvU8EdUlABqr9Jc2JOCEAIXvL0qPAvgPAFmkSGiJJ3wdHlV/1xdL+KSTBeRUPMVy9NvohpqzSTmw4FTFsGhGAnEJUPiOIfowme+UUlH4ZIGjr/o25K8BdI07elmBBBQJUPiOnWD8hCI/I2Y2Cl+8mDWF713dFXIjiaGTlCUQdOGTZYPRyc5uPBHx46PuKz2AB+UI0ShFJ+bQnRxyMRELCXhCIMjCJ1tUVgIo6QlZF05tEz5Zhf7jDSCtWvROykq0xl6+HgDedoGQVUnAFYEgC588Tt3uipZHlW0TvngLJlPnAefKUpF6uRTAl+rmtCSB2ASCKnxykfd8WdT0wwCxSfhk+8rkAfk3LefmqLmHT1y1BvCrH3LDGFKTgC9++ElAO8GD6xiVu2GL8Dl5xN2xG0jrpvWYK5em694NrJwLGgaDQBCFr0zoMuuE79eLNqRsED6Z6Y3tHfu9nvRPczVXXMgJN9cG4+fHXiaLQBCFT769HZYs4JHa9bvwycEJ8l7v+G9yj++LgdmeuLwGwCg/5YexpB6BIAqfHHcUYyNG4pPsR+GTU1wuawP0uiL+LC9MzMCBCnsBVAIgf7OQgGcEgih8qwHUUiUqsx6ZAYlYyfFO8mmW6ne04Rh0hO+c5sdikSKPo+WPO1ZBBCzWtpMpc/OTOFcuenRZBo0Fer3l0ih/dflPSeNIBu326SAgBIImfNJfOeG3sEp+RUTkw/zjH/nkbLsP/qvi8ZiNW+GTE1HkmKnjj5pSj0DP0sD2lXAAN4QOidALiNYkEIdA0ITvBJ1Lq2UbR6TZkGzUlRNU5PBQleJU+ETw+t7k/NFTJRa3NiJ6lz+ltYobbnI9gLoA9ruNgfVJwC2BoAlfMwDKZxrHOo1EZ9YXT/jE9y3nRxZdtwk3Wd/Q4204JN6uZjI59BWTQNCErxWA31XHRCzh09nGEU/4VOP1yk5Wb+V9ns7j/XGxyX26J3sVL/2SwPEEgiZ8cmzmMtVhEEv4dGY/tgifCJ6cqix/NL7DjYT/XABTVfNCOxJwSyBowicHE2xyCylcP+tzoFyUw+h1TjP2u/DJezy59+OD74wLnqCVr2i6qOaEdiSgQiBowlcMwD4VUGIz5QVAto9EK3W6qS1wmBK+uRlmhCl8s5ss1ojgGZ7d5cZ3FEBDnVm4ai5pF2wCQRM+ybbcfStXQ7ou8c6gU33Ppyp88ugp79lkJqa7l9A1DDMGjwN41owreiEB5wSCKHzfA5BLhVyXeAKlemVjPL+RApWN07LA4OFszDUflwbj/PYFjcv4Wd1iAkEUvoEAeqnmLNZ7PvEpMzDZfuKmuBU+na0zbuLysK4cCXY6P03zkDBdxyQQROHrDuA91XER6y7ZsE+359G5ET4D38Oqdt2U3Q4Asp9yjSmH9EMCbgkEUfjqAMhwCypcXz5XW/lh9NXdcD25YUze+TkpToXPraA6aTvBdY4A6ARgUoLbZXMkkIdAEIVPACwG0Eh1LMQ7ej3sV1ZERazifcrmRPhkxbblPaoR+8JODhiVAwhi3MbrizgZRAAIBFX4BgBQvhXC6awvPH5kw2+sU1ycCJ/mrWXJHsqrAMjV5UuSHQjbJwEhEFThOxvANJ0hIEdAyb6+aBuaI/mWFdhI205ESMVftLJq47Hj3C0t0wHI5UFZlsbPsFOQQFCFT/otn67JaSDK5dZOwNB/KJs7NtT5HM5xI95UHAygJ4DD3rinVxJQIxBU4RNaDwB4VQ3bX1aJED8LFzXmhLYM8ftb3QFGe08IBFn45CLxTABy+ZBWkQNBZZuLm8deNw1a9H5PVsufDB0mKp+jsZCALwkEWfgkIS8AeMREZuQd3bg+wElVTXjL68MC4dsCoD8AebQ9aJ4APZKAWQJBF76nAPQ2iVQefeWbXpOzPx8Lnxy4L/dkfAIg2yRH+iIBLwkEWfjuB/CaF3BllfbWCwARwRZayyfHovOR8Im4yTFS4wF8CWCXF/zokwS8JhBU4fNM9I5PmFxQJPd0tKz715YVeSx2MyNMoPDJe7kNAFaE/sg7u/C/5e+1AOTrCxYSsJpAEIXvYQAujxFwl+MyJYuhZYMT0ah2FdStUQllSxZD8WJFULxoYZQI/V20SGHs3X8w50/Z4utwUfMfojbisfDJpuJzACheleSOTYrX/gzAtRp9lCPT5KDczbn+ls3f8krhDw2/ND2OQNCETxYyZEHDeClWpDDOb90AHU6rjxb1T3Tlv0LJ9Tittjw5Ri7PjGyAF0euRVa2Z/dsy8yuLcXPVdoiVR4B4BptL5EdyH3Q8opB/sg2oUMetRMIt0ESvn8CeM50VmUWd03HFriifTOULikHPLsv8YTvt9WXYPueEzH592UYN3U+Fsb7+Nd9CGLBmZ8at9xWujM+pxHIu9ahAPqFDtZ1asd6IQJBET5PZnod/9YAd112JiqWlS2B6sWp8IVb+HzKPLw5dpZ6g9EtOfPTo+rljC9SZHLEl5xgLRvxla9U0OuyndZBED7jole4UEH84/pzcH5ruS5Cv7gVPmlxdeZ2PP3B91i5YZt+AHk9UPzUiSZa+MKRymPwY6FtRdw47iB/qS58xkWvfOni6HfnRWgiy7WGiorwhZse8sVPGDlprqFI/ueG4qdGNFnCF472q9Diyh618INjlcrCZ1z0qlcqixfu74qqFbW/csszwnSETxx9OWMBXh01w/Sopfi5J5ps4ZOI54UOe+UqfYz8parwGRe9BjVPwPP3dYVsVYlVdu3Zj3nLNyBzWzaa16uO+jVPiPvzKVEkG9XLRT+qbsOORth7MLbYfjF9AV4b7Yn4tQewLm4nWEEI+EH4JA7JV+eQCDIzEQikovAZFz0Rrxfv74pSJWKL3tz09ej73rfYtffA/1Cf1SwNfe+QMzi9Lx7N/GQf2VkUP0f584vwSbDyuHslT7yOnLdUE76kiZ7M9G7u90ke0Qsjv/mi09Dt4r85+uXoVqL46RLUsveT8ElH5BzEcwEYfxTQouQD41QSvqSJnuTx25+X4MVPpkRMabWKZTC8zw0JSzfFL2Goj2/Ib8In8cmyf2udC7aSRtPDhlNF+IyLXlr1injlwUvjPt6Gc/PhN79i+MTfoqbqu0F3e5jG/K4pfgnFHW7Mj8InsckLZHnk4KESoUylgvB5InovPXAJypYq7vjX4zfhk8Apfo7TZ6qiX4VP+vcNgC4AuM8vBS4b8oXoyajyo/BR/EzpmWM/fhY+6cQzAJ5w3JsUrmjzjM83oudn4aP4JfTX63fhk9neaTzpxd7rJY2LXr0alTDgvq6uHm9z/6T8OuMLxzh+2ny8PmamaRWQrS5ypJX8zaKxj+/aji1zDrvI2rUXW7J246cFq3MOpfDgRB45Bkiu+wx0sXHG5zvR8/uMz2Pxk82yss+P4qchfNed3xK3X3JGPjGaOW8F3h73EzZs3WlSqE4N+qzPNuEzLnp1TqyIF+93t5ARaQT6fcYXjnns1D8x+PMfTf6IxBfF7xhR5UfdaMIXTtSISXMw/JvfsP+gkWP45GDTxOyqNz3SDPmzSfgeD93kZajrgGxZcbt6G61xW4TPw3d+cix9u4DP/JTP47v+glbo3vX0mGNbTuJ5/O1vsGm7kV0pMks3/j+gsR+nx45sEb4+APqaZFFXZnoPXBr321unbcY6I69UiaIY99xtTl0lpJ5HZ/oFfebn2YwvPCh2792PJ96ZiPkZciW0VnkPwB1aHiw2tkH4jN19G86TyZle2Gfm1uycT9YilU6nN8SjN3bw3TCh+BlPibLwOZnxhaPN3rMf974wJucgDI0i93tUDuq+Pr8LnxWiFx58kYSkasXSeOvRvysfS68xsB2ZUvwcYXJaKSHCJ8Gs3ZSFe14Yg30HtN75yXe8cn9H4Iqfhc8q0QuPHDmhZeafK7FxWzZa1K+OTqc38q3oxRJsA7+EID72Jkz4JD+jfpiLd8b/pJMqObL+QR0Httr6VfiMi16tKuXxSq/LlPfp2Zpgp3Fz5ueUVMx6CRW+/QcO4brewyOeCOSwN3KHsrsrAR069ns1vwqf0e8J06pVwIsPXIJypUv4PR9JjW/M5Hl4a5zxS4xk5ieHmcqJzqlelIUv3naWaODe/2o2Pv1O68rdmkE8azHlhU9mei8/eCnKU/QciY5HMz95kX4FgOmOgrC3krLwuVncyI0nY91W3D1gtA4xuU/Z+P92OgElwjalhY+ipzaEPBI/eQv/EIDX1aKywirhwidUbnrqY2zcpry377rQxmsrAJsKMmWFj6KnN0Q8Ej8JSj4Y7gXgV70IfWmdFOF7bvgPmPRruiqQ/wMg79QDVVJS+Ch6Zsawh+InAY4FIBvT/zQTrS+8ePrlRrQeDv16Nj75r/J7vtcA9PQFvQQGkXLCR9EzO3o8Fj8JVn6xwwB8DGCL2egT7i0pM74JsxZh4GfTVDv7BYDLVI1ttUs54Wt/RkvUqBr/SsdICbuoVXWcWLGkrbn0LG4D+8WcxrYjJH5yJ+xBp0Y+qtc09DWE65BUV3WloZ/mr8KTQya6bjNk8AuA2B8Jq3r2sZ1fhU+OOKqtwq1hw4aoWLGiiimevr6Fkl0QjBIw8wsCxqh9VF3VFYe/LFqDx96aoMpPLopJzBWAqhF6YOdX4ZPt6PkPJ3MAoGbNmpA/bku18sVx38WN3JoFqj7Fz7t0U/i8YxvJs1+Fb5zqe4dq1aohLS3NNcUOp1TFec2qubYLmkECH3sDhVbnUXf2wtU5x1UpFs74FMF5YfYmgB4qjkuWLInmzZu7MuVszxUucObnjpeT2pzxOaFkro5fZ3xa5+81bdoUZcuWdUSpWJGC6H5ePS5qOKL1VyXO/FwCi1NdZ8b388LVeIIzPlcJ8avwdZVrYV31JFflypUro169enHN06qUwg3t0lCiWOG4dVkhPwHZNDvgo8k4ctTop9WBRM0ZX2LT7lfhKwpATlmUv5VK2zPPwBHk756IXbXyJVC9QgmcWldt9VcpoBQ1+n3JWvQe8q2puyBSlFL8blH44jMyWcOvwid9HK9zDV7Xs5riwWvONsmKvqIQWJ25Hb2HTMS6LUZvAgsUbwpfYtPtZ+GT+wCG6OB46Lr26NymiY4L2joksHf/QQwaOV3nm1GHLaVmNb7jS2xe/Sx88vnFZh0chQoWxMAHL0WTtKo6bmjrgsCvi9fkfD5l6CYwFy3bXZUzvsTmz8/CJyR+1v2cpkSxIuh5dTuc37phYskGuLUDBw9hxKS5GD9tPnbs3hdgEs67rjPj4z4+55zDNf0ufDcC+Mh9t/JbtG9ZFw9f1x6lShQz4Y4+HBL44bd0fDljoYnrEB22aGc1HeHT3M4i5yReEzotx054ClH7XfikS/MANFPoWz6TE8qVwtUdW+DC0xtSAE0AdeFj647dmDlvJabPzcCc9PUuLINRNYnCJ4APA7g6SOJng/B1AvCtyeFftEghdDi1fs4NaI1qV0axotzHZ5JvPF9yL+ys+StzhHDZ2i18HwggycIXFr+bAMiZgilfbBA+ScL3ADp6lY2TqlVAvRqVUKNyORTwiEj32+9Elcpqx2VF6veujSuwY8Myr5Ak1O++AwexemMWtu7Ygx279mJ79l4cOiyTkOCUU+pWR6uGNZQ6rPmom7vNIwA6m55oKHXKYyOPfubGo5ZHXXnktbY83e8pPNhLrpwwU/Zsz8Sf414y44xerCageSzV8X2XyzvkZKSFVkOJE7wtwifdeBbAv21NRo0aNbBgwQIULFjQWBcWfPUadm1ebcwfHdlJwLDwCYQ1AORwyu12EokftU3CJ4oh7/rOj98tf9b47LPP0LmzPEmYKVuW/Ybl0wPxSsYMsBT14oHwCSm5FKqDpSdhx820TcInnSkXeuRVOp05Lg2PK3Ts2BFjx8odO2bKkcOH8PunfXH44H4zDunFSgKrMrej1yvjsGvvAdPxvwjgUdNO/eDPNuETZvK+T05otvJyjDlz5qBu3brGcr9q9pfIXKB80YyxOOgouQS279yDFz6ZknMMvcEix+6cCWC2QZ++cGWj8Ak42XM00hcEXQZxzz334Pnnn3dpFb36vp1bMHeMOX/GAqOjpBD47+wleGP0TOzZb+yupiUAGielMx42aqvwCRJ51zcGgLMTRz2E6MZ1mTJlkJ6eDjkp2lRZ/O072LFe+UJpU2HQj08IrNu8A4++/iU2Z+02FdFzNi8sRoJgs/BJf+oDkMsG5G9ryqBBg3DbbbcZi3fbynlInzzcmD86sp+AfCnz6OtfYc2mLBOdkc/a5GTflNlCYLvwSVJlxiczP2tWexs1aoRffpHrTM2Uo0cO44+Rz+DgXjm7lYUEjhHYuXsf7n9pLDZsNXJO4lAA3VOFbSoIn+RC+iFTKNnrZ8UZVBMnTkTbtm2NjaO1v3+LdXPlAxcWEviLwKbt2bjvpbHIyt6ri0VmfXK4ZUp8LpQqwhdOaikATwCQTyR8fQzLVVddhaFD5T9RM+XAnp34Y0R/ALz/wgzR1PGyYsM29Hx5LPYdEO3SKvI+pZuWB58Yp5rwhbHKxbqy1CnH7fiyFCpUCIsXL0bVquYmqEu+H4qsNSn9pZEvc2lDUN/+vAQvfjLFRKh1AKw04SiZPlJV+MJM/yYHX4S2v/hu0/Njjz2Gf/3rX8byn7V2MZZ8954xf3SUWgQefnU8/lyeqdupfgDk+lerS6oLX+7kNAVwWejP6aH3gklNXpUqVbB06VJj3+8ePXoUc0Y9iwO7jazkJZUNGzdPIHNrNm7/zwgcOKh18o0cpljT9ncqQRK+40eSJK/ycX/Kuxxuwq+3S5s81YcNG4YrrrhCx0Ue2/XzfsCa32SHDwsJ5CcwevI8vD1uli6ai2w/uirIwqeb/LC9nBJwraqzdu3aYcKECarm+ewO7tuN3z/rBxyVo9VYSCAvgcOHj6Db05/qHv46ys/vz53knMLnhFLsOu0BTNVxM3v2bDRubO6roGVTPsLWFXN1QqJtChP4+seFeGXEdJ0eyg1SZQBoLxPrBKFjS+HTofeX7XwAJ6u6uv322zFw4EBV83x2Ozcsx6KJbxnzR0epRcDQrO88AJNtJUPhM5O5ewAMVnVVokQJrFixwuj3u3NGP4f92VtVQ6JdihMYMWkO3v1Cbm9VLs+E9swqO0imIYXPDH05cUAuP1c+eWDAgAHo0aOHmWgAZC6cgVU/jzfmj45Si4Bc+H5j3491OiVHw7XRcZBMWwqfOfpvALhX1V2dOnUwd66593KHDuzLOaRUvuNlIYFIBDT39cnAkq+jrBxgFD5zvwl5xyfv+pTL+PHj0aGDnPZtpmTMGIHN6b+acUYvKUfgq5kLMWik1iKHfLu72EYwFD6zWZNvgs5RddmlSxd8+umnqub57HZmLseib7jIYQxoijmSU1u69dMab1cB+NxGLBQ+s1mTb4NHqLosUKBAzk1sNWvK3mr9Io+5v3z0BI4etnbXgT4EeohJ4PJ/DcVu9bs6ngQgJ2NYVyh8ZlNWOHQ1XzVVt4888gh699b6GCRP07KtRba3sJBAJAL/HPwVfl+yThWOTBdvUDVOph2Fzzz9p3Q+Y6tQoQKWL1+OwoVFQ/VLxoxR2JyecnfF6IOhhxwCsqVFtrYoFnlBKBv4rSsUPvMpk9me/BeqfHP4kCFDcO21yl/B5emR3Lsr9++ykEAkApoLHKKYrWwkS+HzJmtyFP6Vqq5bt26NSZMmqZrnseNFREYwpqyTyb8vw7PDlMeavEOx6r6bcCIpfN4MafmcR3k0SUjTpk1Dy5YttaI7euQIfvukNy8c16KY2sazF67G428rn+Yjm/ar2EiIwudd1mR/UyNV9zfffDPeeEP2RKsXfrOrzi4olvMzMvHQIOUvfOQiD+WvlZLJmMLnHf2eAAapui9evDiWLFkCWexQLRkzRmJzurnb3FTjoJ1/CSxdsxn3vai8FU++2jCzCpdgRBQ+74DLtZcbdP5H7N+/P3r2FP10X/bu2Ix5n79g+0G57jtOC1cEKHyucLGyQwJvA7jLYd181dLS0jBv3jwl80XfvImdmRlKtjQKDgEKX3Bynciean+/O3r0aHTq1MlVzKt+/gKZC7W+wXTVHivbS4DCZ2/u/B75DABnqQZ5wQUXYMwY2R3jrKz941usm8OLxZ3RYi0KH8eAVwTkkx6tg8/kuCo5tipWObg3G6tmf4GtGcq78L3qP/36mACFz8fJsTw0WfWSy0wrqfajZ88H0L+/HHibv+zZnpmzcpu5YJqqe9oFmACFL8DJT0DXnwXwb9V2TqhYHuNe+yeKFCmUx8XBPdnYu2OTqlvakQAofBwEXhKoDWCFzve7Pa9uh0vaKd9n5GXf6NtiAhQ+i5NnSeiyPf5S1VirVyqLD3tfr2pOOxKISIDCx4HhNYELAUzUaeS5e7rgtMZmDinViYO2qUOAwpc6ufRrT+QrmaU6p1k0q1cNL/e8zK/9Y1wWEqDwWZg0C0N+GMBLOnFz1qdDj7bHE6DwcUwkgoB8vyvLsHItn1JpVLsyXv+H8lF/Sm3SKHUJUPhSN7d+69l7ALrrBNWneye0axF7Q7OOf9oGhwCFLzi5TnZPTwWgdRZ8pXIlMfTx61CiWJFk9yVq+xVqNUXD82/zbXwqgR05fAhLvn8fO9enq5j70obC58u0pGxQPwM4Xad3Xdo2Qa9r/XvPSyoKn+TryJHDWPLdeykjfhQ+nV8hbd0SkA15n7g1Or7+Sw9ciub1q+u68cQ+VYUvLH7pkz5A1lo5ZNvuoil80nkrz/S0Mmi7h1pO9HID2wIAjXX6UrFsSQx+5EpUKldKx40ntqksfAJMLmtfmgLipyl8PIHZk19Pajv9O4BRul1sUOsEDH7kKl03xu1TXfhSRfwofMaHPh06ICBnSLVwUC9mlfYt6+LJ2y7QdWPUPgjCFxa/9MnDsX21TODtKxQ++3KWChF3BfCliY6c26oe/t2tIwoW9Mfbi6AI3zHxO4L0yR9aKX4UPhO/PvpQIaC9whtu9IyTa6N3904oWjjv8VUqQenaBEn4wuK3bNon2LZiri66hNpT+BKKm43lItARgLGz4k+uUxXP9uiMksWLJhVy0IQvR/yOHsWyqR9bJX4UvqT+TALf+DAA3Uz3H/FWAAAQYklEQVRRkCOs+tzeCfVqKB/6rB1KEIXPRvGj8GkPdTrQIFA89DVHUw0feUwLFyqIW7u0xjXntUCBAol/7xdU4QuLX8b0EdiyXOsDHVNDIaYfCl9CMLORGATqA/gdQBmTlGpVKY8bLzwV555aD4UKyvbBxJQgC59N4kfhS8zvga3EJiCH7Y3zAlK1SmVwY6dTcU6regn5xjfowhcWvxU/jsbmpbO9SKkRnxQ+IxjpxACBQQB6GvAT0YU8AjerVx1tTjkJbU5JgwiiF4XC9xfVjJmjfCt+FD4vRj99qhKYCaCtqrFTu/JlSmBUf2NrKnmaLV29EU6+6A6noaR8vSU/fIysVf6781hT+CRviX+BbGC0WBm0gX773YUcWCpbXFp7GaiXwjdr/ir0HqJ1xYiXXffEd7c2tdGyVvmovk+sWweVqlfzpG1Vp5rCx291VcHTLiqB0gC+BuDZ2VMUPrOjL57wSWt+Ez9N4ZMuye1X68yS9N4bZ3zeM9ZpQY6ol0/aPPkQl8Knk5r8tk6Ez2/iZ0D4VgE4B4D8bU2h8Pk/VXLM8lgAXUyHSuEzS9Sp8EmrNerXQ8WqVcwGoODNgPBJqzLjO8sm8aPwKQyWJJgUBjASwBUm26bwmaQJuBE+v4ifIeGzTvwofGbHvpfeZPdxLwD9AZQw0RCFzwTFv3y4FT4/iJ9B4QuLn7yTzjBL1rw3Cp95pl57lC88PgJwhm5DFD5dgnntVYQv2eJnWPikOxtDW7F8LX4UPrNjP1HejMz+ypcujlHP3OJJzLP+XIne737riW+/OlUVvmSK36rM7bjjP/IWxWgR8ZMFjyVGvRp0RuEzCDMJrrRmf5zxmc2YjvBJJLUa1kf5ypXNBuXA2/hp8/H6GNkzb7RsAdDOr+JH4TOa66Q4OxnAfJWWKXwq1KLb6ApfCorfGwDuN0vZjDcKnxmOyfRC4Usm/VxtmxC+ZIrfhB8XYeCIaSZpUvhM0qSvPAQofD4ZEKaET7pTu1FDlDsh8QfJGhY/Cp9PxmYqhkHh80lWTQpfMsXvu9lLMeDjySaoUvhMUKSPiAQofD4ZGKaFLwXEj8Lnk7GZimFQ+HySVS+Ez3Lxo/D5ZGymYhhNACxU6ZiP9/FtAnCtSp8M2HwoO0tU/Nx8Zm20qh39WCoVn2Jz8NAR7Cl/Itq1qKPqQtnuueE/YNKv6ar2FD5VcrSLSyAVZ3xr5P1+3J57U0E23TZUce3VjG/msi0YO2cD+nbvhDbN0lRCU7Z5+bOp+GbWYlV7Cp8qOdrFJUDhi4vIVQXfCV//rxdj2+4DKFiwQMLFj8LnauywcgIJUPjMwvaV8C3JzMbb01b8r4eJFj8Kn9nBRW/mCFD4zLEUT74Svnenr8DCDdl5eiji1/+ui9G6idKrSFe0KHyucLFyAglQ+MzC9o3wbd21H89MiPydv9yW1+/OizwXPwqf2cFFb+YIUPjMsfTVjG/cH+sxLV2+9Y9cEiF+FD6zg4vezBFQFz4Pj6Wa+sdy9P9ALopTKoFf1T1w6Aj6fLEQ+w8diQuw/90X44ym3iyCU/ji4meFJBFQFz6P7tXN3JqNngPHYnv2XlUkgRe+Gelb8Pkf6x3xk5nfsz06o1XDGo7qu6lE4XNDi3UTScBXwiei99Cg8diyY7cOg0AL39GjR3Pe7ckWFqelSOFCeObui42LH4XPaQZYL9EEfCN8m7N2oefL43RFT/gFWvgWb8jGO9P/2sLidEB5IX4UPqf0WS/RBHwhfCJ6MtPbuG2Xif4HWviGTFuBRZl5t7A4hWpa/Ch8TsmzXqIJNAWwQKVRUycwb9u5J+edniHRS/aMbwOAaio8TXyyFmsLi9OYRPwG3NcFp9St7tQkaj0KnzZCOvCIQFJnfFm79uLBgeOwfstOk91L1oxPLsiZotoRE8I39vd1mL5sq2oI/7MrVqQwnru3s7b4Ufi0U0EHHhFQP51Fc1VXRO/hQV9gzaYs0117G0AP004d+JNrO290UC9iFV3h23fwcM4WloOHj6qGkMfOhPhR+Iykgk48IJC0GZ/M9BaulJsEjRY5FupWAGZ+/c5DKwdgM4Aizk3y1rzj7DQ0rV5W1RzTlm7BuDnOtrA4baR40cJ477FrUaVCaacmeepR+JSw0SgBBJImfNf1Ho6tO/aY7GKyRE/68BCAl1U7U7JoITx1aVMUKqh2f5fKFhansb7xyJVoWEvt2koKn1PKrJdoAqkifEMA3J2EmV44X0sBNFBN3nmNKqNrC/XFhIXrd+LdGStVm49pR+HLj0ftvydP0kOnigRSQfhE9O5S7L8Js/YApuo4erJLY1QoVVTZxdtTM7Bko5GtQPlioPBR+JQHpo8NbRe+1wE8kGS+WosaDauWRo9z6ip3wcQWlliNU/gofMqD08eGNgufH0RPFjVkhaaYao5vaVMbLWqp37Ux5rd1mLlcfwtLtPgpfBQ+1bHtZ7ukbWfRXNzwg+hJXh8E8IpqgnUXNUxvYYnUDwofhU91fPvZztYZXxcAE3wAVmtRo2PjyujSXH1RY+rSzRg/Rz4W8a5Q+Ch83o2u5Hmm8KmzPxvANHVzQGdRQ7aw9PtqMXbsPagTQlxbCh+FL+4gsbAChU89acMB3KRq3qhaadzdXn1RY8H6nXjPoy0suftE4aPwqY5xP9tR+NSyo72ocVvbk9CsprhRK29NzcBSj7awUPhi54T7+NTGrJ+sKHxq2dBa1ChTvDD6XNIEBQuo/YQ27tyH5yfK60XvC2d8nPF5P8oS3wKFT4251qLGBU2q4OJmSqdX5UQ7+re1+HH5NrXIXVpR+Ch8LoeMFdUpfO7T1A7AdPdmf1n0vaQJypZQO88gEVtY+KjLR12d8W2DLYXPfZbkMISb3Zsds2hSvQzuPLuOqjmmLNmML+Z6u4WFwkfhUx6glhhS+NwlSntRo/tZJ+GUGmqLGkeOHsXTCdjCQuGj8Ln7WdhXm8LnLmdJXdSYv24H3p+5yl3EmrX5jo/v+DSHkC/NKXzu0qK1qHHhyVUhf1TLm1MykL7Jm1NYosVE4aPwqY5XP9tR+Jxn5ywAM5xXz1tTNq7IFhbVRY1EbmHhoy4fdVXHuS12FD7nmRoGoJvz6nlrnnxiWdzeLk3VHKN+XYtZGYnZwkLho/ApD1RLDCl8zhKlvahxR7s0ND1R7U4N2cLy5PiFOHwk0VeJAHzU5aOus5+IXbUofM7y1RPAIGdV89eSLzVk714BxS81Ji/ehC/nZao2r2VH4aPwaQ0gnxpT+JwlRmtR46JTqqJTU7VFjWRsYeGjLh91nf0s7K1l60GknQF8kyDsbQHMVG1Ld1Hjz7U7MPTHxG5hofBR+FTHuy12nPHFz9TQ0F298WtGqHFKjbLofpb6osbgycuxbPNupbZDRnMAVABwkoqTJD7q+uWU7XzY1I6WUKFPG68IUPhik5VFjSwd+HedXQeNq5dRcmFoC4usRPcHUFsliCQK3xsA7leJ2WsbCp/XhL33T+GLzVhucHtVNQ3lSxbJOWVZdVFj5C9r8dMKrS0sYiwvF5dT+FSzmN+OwmeOZbI8Ufhik9da1OjcrBrOb1JFKbd79h9Cny8X6W5hkZnekwDkJSFnfEqZoPAZwuYrNxS+6OloA+BH1WwVLAA8dWlTlCpWWMnFpEWb8PWfWltYDgGoGbr+ksKnlIXIRpzxGYSZJFcUvujgtRY1mtcsh1vbKq0nwNAWls8AXB/qHoXP4A+MwmcQZpJcUfgig9f+UuPu9nXQqJraosbcNVkYNmu17pCQbTizKHy6GPmoa55g8j1S+CLnQFYTX1NNj+6ixus/LEfGFu0tLK1yxc8Zn2oyI9hxxmcQZpJcUfgig58HoJlqTro0r4aOjdUWNQxuYZHrL8OFwqeaTAqfQXL+cUXhy5+LM3M9IrrOlO6ixmez12D2yu2u281lEN7CIosbFD4dklFsOePzAGqCXVL48gN/H8BtqnloWascurVRW9QwtIXlaQC9j4ufMz7VhHLGZ5Ccf1xR+PLmQntR455z6qJB1dJKGf5+0SZMMLeFJXcMFD6ljEQ24ozPIMwkuaLw5QV/HwD5RlSpnFC6KB7r3FjJVraw9P1iEXbtz/2E6trVpwBuiGBF4XONMroBhc8gzCS5ovDlBa+1qHFJi+ro0KiyUirnrMnCh/pbWGTT9U8UPqUUODai8DlG5duKysInPWpR/0Tljs1dtl7ZFkAXABN0HESwPSOKaDhupmaFEiheuKDj+rkrbsrej537tGZ7cgpL7i0sRh51G9Q6ASWLFVXq05pNWdi2c4+SLQAeUqBKjnZxCWgJX1zv3lXwQvjeA9Ddu5A99yyXnH8UpRXlR13Po47eAIUvifBTvWkK37EMlwKwFUAxSxMeaQuLkRlfEnlQ+JIIP9WbpvAdy/C9oUcrW/PdT26vjBE8Z3wGM8t3fAZhJskVhe8YeK1FjSTlLtxs7lNYooVC4TOYJAqfQZhJckXhA04H8HOS+Jto9hMAN8ZxROEzQTrkg8JnEGaSXFH4gHcB3J4k/iaalU/s4gk3hc8EaQqfQYrJdRV04bN9USPWFpbcI4vCZ/B3xhmfQZhJchV04bsHwOAksTfR7E0APnbgiMLnAJLTKhQ+p6T8Wy/owmfzosbG0NHyTnY9U/gM/gYpfAZhJslVkIWvNYDZSeJuotmnAPR16IjC5xCUk2oUPieU/F0nyMI3BMAd/k5P1OicbGHhOz6Pkkvh8whsAt0GVfhkUUMeFeVvG4u815P3e04LZ3xOSTmoR+FzAMnnVYIqfD0AvOnz3MQKTw4jkBVdp4XC55SUg3oUPgeQfF4lqMJn86KG7NmTvXtuCoXPDa04dSl8BmEmyZUcHieHb9pW5FEvXTFoufPxH4q2fjD7DsBMl4H0AlDepU2yq8vCk+mjx4z0icJnBCOdkAAJ2ESAwmdTthgrCZCAEQIUPiMY6YQESMAmAhQ+m7LFWEmABIwQoPAZwUgnJEACNhGg8NmULcZKAiRghACFzwhGOiEBErCJAIXPpmwxVhIgASMEKHxGMNIJCZCATQQofDZli7GSAAkYIUDhM4KRTkiABGwiQOGzKVuMlQRIwAgBCp8RjHRCAiRgEwEKn03ZYqwkQAJGCFD4jGCkExIgAZsIUPhsyhZjJQESMEKAwmcEI52QAAnYRIDCZ1O2GCsJkIARAhQ+IxjphARIwCYCFD6bssVYSYAEjBCg8BnBSCckQAI2EaDw2ZQtxkoCJGCEAIXPCEY6IQESsIkAhc+mbDFWEiABIwQofEYw0gkJkIBNBCh8NmWLsZIACRghQOEzgpFOSIAEbCJA4bMpW4yVBEjACAEKnxGMdEICJGATAQqfTdlirCRAAkYIUPiMYKQTEiABmwhQ+GzKFmMlARIwQoDCZwQjnZAACdhEgMJnU7YYKwmQgBECFD4jGOmEBEjAJgIUPpuyxVhJgASMEKDwGcFIJyRAAjYRoPDZlC3GSgIkYIQAhc8IRjohARKwiQCFz6ZsMVYSIAEjBCh8RjDSCQmQgE0EKHw2ZYuxkgAJGCFA4TOCkU5IgARsIkDhsylbjJUESMAIAQqfEYx0QgIkYBMBCp9N2WKsJEACRgj8P/9qkjFpAoX4AAAAAElFTkSuQmCC" preserveAspectRatio="none" width="100%" height="100%"></image></symbol><style class="style-fonts"> + </style></defs><clipPath id="image-clipPath-id1" data-id="id1"><rect width="100" height="100" rx="25" ry="25"></rect></clipPath><g transform="translate(30.710678118654755 30.710678118654755) rotate(315 50 50)" clip-path="url(#image-clipPath-id1)" data-id="id1"><use href="#image-file_A" width="100" height="100" opacity="1"></use></g><clipPath id="image-clipPath-id2" data-id="id2"><rect width="50" height="50" rx="12.5" ry="12.5"></rect></clipPath><g transform="translate(130.71067811865476 30.710678118654755) rotate(45 25 25)" clip-path="url(#image-clipPath-id2)" data-id="id2"><use href="#image-file_A" width="50" height="50" opacity="1" transform="translate(25 25) scale(-1 1) translate(-25 -25)"></use></g><clipPath id="image-clipPath-id3" data-id="id3"><rect width="100" height="100" rx="25" ry="25"></rect></clipPath><g transform="translate(30.710678118654755 130.71067811865476) rotate(45 50 50)" clip-path="url(#image-clipPath-id3)" data-id="id3"><use href="#image-file_A" width="100" height="100" opacity="1" transform="translate(50 50) scale(1 -1) translate(-50 -50)"></use></g><clipPath id="image-clipPath-id4" data-id="id4"><rect width="50" height="50" rx="12.5" ry="12.5"></rect></clipPath><g transform="translate(130.71067811865476 130.71067811865476) rotate(315 25 25)" clip-path="url(#image-clipPath-id4)" data-id="id4"><use href="#image-file_A" width="50" height="50" opacity="1" transform="translate(25 25) scale(-1 -1) translate(-25 -25)"></use></g></svg>" +`; diff --git a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap new file mode 100644 index 0000000..165c135 --- /dev/null +++ b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap @@ -0,0 +1,20131 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and the arrow got bound to a different element in the meantime > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id172": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id172": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and the arrow got bound to a different element in the meantime > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id170", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 18, + "width": 100, + "x": -100, + "y": -50, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and the arrow got bound to a different element in the meantime > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id171", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 19, + "width": 100, + "x": 100, + "y": -50, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and the arrow got bound to a different element in the meantime > [end of test] element 2 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": { + "elementId": "id175", + "fixedPoint": [ + "0.50000", + 1, + ], + "focus": 0, + "gap": 1, + }, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": "102.35417", + "id": "id172", + "index": "a2", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + "101.77517", + "102.35417", + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 40, + "width": "101.77517", + "x": "0.70711", + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and the arrow got bound to a different element in the meantime > [end of test] element 3 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id172", + "type": "arrow", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 50, + "id": "id175", + "index": "a3", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 6, + "width": 50, + "x": 100, + "y": 100, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and the arrow got bound to a different element in the meantime > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id172" => Delta { + "deleted": { + "endBinding": { + "elementId": "id171", + "focus": "0.00990", + "gap": 1, + }, + "height": "0.98586", + "points": [ + [ + 0, + 0, + ], + [ + "98.58579", + "-0.98586", + ], + ], + "startBinding": { + "elementId": "id170", + "focus": "0.02970", + "gap": 1, + }, + }, + "inserted": { + "endBinding": { + "elementId": "id171", + "focus": "-0.02000", + "gap": 1, + }, + "height": "0.00000", + "points": [ + [ + 0, + 0, + ], + [ + "98.58579", + "0.00000", + ], + ], + "startBinding": { + "elementId": "id170", + "focus": "0.02000", + "gap": 1, + }, + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id170" => Delta { + "deleted": { + "boundElements": [], + }, + "inserted": { + "boundElements": [ + { + "id": "id172", + "type": "arrow", + }, + ], + }, + }, + "id171" => Delta { + "deleted": { + "boundElements": [], + }, + "inserted": { + "boundElements": [ + { + "id": "id172", + "type": "arrow", + }, + ], + }, + }, + "id172" => Delta { + "deleted": { + "endBinding": { + "elementId": "id175", + "fixedPoint": [ + "0.50000", + 1, + ], + "focus": 0, + "gap": 1, + }, + "height": "102.35417", + "points": [ + [ + 0, + 0, + ], + [ + "101.77517", + "102.35417", + ], + ], + "startBinding": null, + "y": 0, + }, + "inserted": { + "endBinding": { + "elementId": "id171", + "focus": "0.00990", + "gap": 1, + }, + "height": "0.98586", + "points": [ + [ + 0, + 0, + ], + [ + "98.58579", + "-0.98586", + ], + ], + "startBinding": { + "elementId": "id170", + "focus": "0.02970", + "gap": 1, + }, + "y": "0.99364", + }, + }, + "id175" => Delta { + "deleted": { + "boundElements": [ + { + "id": "id172", + "type": "arrow", + }, + ], + }, + "inserted": { + "boundElements": [], + }, + }, + }, + }, + }, + ], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id170" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": -100, + "y": -50, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id171" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 100, + "y": -50, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id172": true, + }, + "selectedLinearElementId": "id172", + }, + "inserted": { + "selectedElementIds": {}, + "selectedLinearElementId": null, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id172" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 0, + "index": "a2", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 100, + 0, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "width": 100, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and the arrow got bound to a different element in the meantime > [end of test] number of elements 1`] = `4`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and the arrow got bound to a different element in the meantime > [end of test] number of renders 1`] = `21`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and there are no conflicting updates in the meantime > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id167": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id167": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and there are no conflicting updates in the meantime > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id165", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 9, + "width": 100, + "x": 150, + "y": -50, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and there are no conflicting updates in the meantime > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id166", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 9, + "width": 100, + "x": 150, + "y": -50, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and there are no conflicting updates in the meantime > [end of test] element 2 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 0, + "id": "id167", + "index": "a2", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 100, + 0, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 30, + "width": 50, + "x": 200, + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and there are no conflicting updates in the meantime > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id167" => Delta { + "deleted": { + "points": [ + [ + 0, + 0, + ], + [ + 50, + 0, + ], + ], + }, + "inserted": { + "points": [ + [ + 0, + 0, + ], + [ + 100, + 0, + ], + ], + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id165" => Delta { + "deleted": { + "boundElements": [], + }, + "inserted": { + "boundElements": [ + { + "id": "id167", + "type": "arrow", + }, + ], + }, + }, + "id166" => Delta { + "deleted": { + "boundElements": [], + }, + "inserted": { + "boundElements": [ + { + "id": "id167", + "type": "arrow", + }, + ], + }, + }, + "id167" => Delta { + "deleted": { + "endBinding": null, + "points": [ + [ + 0, + 0, + ], + [ + 100, + 0, + ], + ], + "startBinding": null, + }, + "inserted": { + "endBinding": { + "elementId": "id166", + "focus": -0, + "gap": 1, + }, + "points": [ + [ + 0, + 0, + ], + [ + 50, + 0, + ], + ], + "startBinding": { + "elementId": "id165", + "focus": 0, + "gap": 1, + }, + }, + }, + }, + }, + }, + ], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id165" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": -100, + "y": -50, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id166" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 100, + "y": -50, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id167": true, + }, + "selectedLinearElementId": "id167", + }, + "inserted": { + "selectedElementIds": {}, + "selectedLinearElementId": null, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id167" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 0, + "index": "a2", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 100, + 0, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "width": 100, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and there are no conflicting updates in the meantime > [end of test] number of elements 1`] = `3`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and there are no conflicting updates in the meantime > [end of test] number of renders 1`] = `23`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind remotely added arrow when it's bindable elements are added through the history > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind remotely added arrow when it's bindable elements are added through the history > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": null, + "endBinding": { + "elementId": "id177", + "fixedPoint": [ + "0.50000", + 1, + ], + "focus": 0, + "gap": 1, + }, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": "1.30038", + "id": "id178", + "index": "Zz", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + "98.58579", + "1.30038", + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": { + "elementId": "id176", + "fixedPoint": [ + 1, + "0.50000", + ], + "focus": 0, + "gap": 1, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 11, + "width": "98.58579", + "x": "0.70711", + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind remotely added arrow when it's bindable elements are added through the history > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id178", + "type": "arrow", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id176", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 7, + "width": 100, + "x": -100, + "y": -50, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind remotely added arrow when it's bindable elements are added through the history > [end of test] element 2 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id178", + "type": "arrow", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id177", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 7, + "width": 100, + "x": 100, + "y": -50, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind remotely added arrow when it's bindable elements are added through the history > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id176" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": -100, + "y": -50, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id177" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 100, + "y": -50, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map { + "id178" => Delta { + "deleted": { + "endBinding": { + "elementId": "id177", + "fixedPoint": [ + "0.50000", + 1, + ], + "focus": 0, + "gap": 1, + }, + "startBinding": { + "elementId": "id176", + "fixedPoint": [ + 1, + "0.50000", + ], + "focus": 0, + "gap": 1, + }, + }, + "inserted": { + "endBinding": null, + "startBinding": null, + }, + }, + }, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind remotely added arrow when it's bindable elements are added through the history > [end of test] number of elements 1`] = `3`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind remotely added arrow when it's bindable elements are added through the history > [end of test] number of renders 1`] = `9`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind remotely added bindable elements when it's arrow is added through the history > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind remotely added bindable elements when it's arrow is added through the history > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": null, + "endBinding": { + "elementId": "id180", + "fixedPoint": [ + 1, + "0.50000", + ], + "focus": 0, + "gap": 1, + }, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": "1.30038", + "id": "id181", + "index": "a0", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + "98.58579", + "1.30038", + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": { + "elementId": "id179", + "fixedPoint": [ + "0.50000", + 1, + ], + "focus": 0, + "gap": 1, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 11, + "width": "98.58579", + "x": "0.70711", + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind remotely added bindable elements when it's arrow is added through the history > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id181", + "type": "arrow", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id179", + "index": "a0V", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 12, + "width": 100, + "x": -100, + "y": -50, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind remotely added bindable elements when it's arrow is added through the history > [end of test] element 2 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id181", + "type": "arrow", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id180", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 11, + "width": 100, + "x": 100, + "y": -50, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind remotely added bindable elements when it's arrow is added through the history > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id181" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": null, + "endBinding": { + "elementId": "id180", + "fixedPoint": [ + 1, + "0.50000", + ], + "focus": 0, + "gap": 1, + }, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": "11.27227", + "index": "a0", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + "98.58579", + "11.27227", + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": { + "elementId": "id179", + "fixedPoint": [ + "0.50000", + 1, + ], + "focus": 0, + "gap": 1, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "width": "98.58579", + "x": "0.70711", + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map { + "id179" => Delta { + "deleted": { + "boundElements": [ + { + "id": "id181", + "type": "arrow", + }, + ], + }, + "inserted": { + "boundElements": [], + }, + }, + "id180" => Delta { + "deleted": { + "boundElements": [ + { + "id": "id181", + "type": "arrow", + }, + ], + }, + "inserted": { + "boundElements": [], + }, + }, + }, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind remotely added bindable elements when it's arrow is added through the history > [end of test] number of elements 1`] = `3`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind remotely added bindable elements when it's arrow is added through the history > [end of test] number of renders 1`] = `11`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should unbind remotely deleted bindable elements from arrow when the arrow is added through the history > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should unbind remotely deleted bindable elements from arrow when the arrow is added through the history > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id182", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "width": 100, + "x": -100, + "y": -50, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should unbind remotely deleted bindable elements from arrow when the arrow is added through the history > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id183", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "width": 100, + "x": 100, + "y": -50, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should unbind remotely deleted bindable elements from arrow when the arrow is added through the history > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id182" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": -100, + "y": -50, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id183" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 100, + "y": -50, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should unbind remotely deleted bindable elements from arrow when the arrow is added through the history > [end of test] number of elements 1`] = `2`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should unbind remotely deleted bindable elements from arrow when the arrow is added through the history > [end of test] number of renders 1`] = `4`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should update bound element points when rectangle was remotely moved and arrow is added back through the history > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id186": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should update bound element points when rectangle was remotely moved and arrow is added back through the history > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id186", + "type": "arrow", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id184", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 7, + "width": 100, + "x": -100, + "y": -50, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should update bound element points when rectangle was remotely moved and arrow is added back through the history > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id186", + "type": "arrow", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id185", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 8, + "width": 100, + "x": 500, + "y": -500, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should update bound element points when rectangle was remotely moved and arrow is added back through the history > [end of test] element 2 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": { + "elementId": "id185", + "focus": -0, + "gap": 1, + }, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": "374.05754", + "id": "id186", + "index": "a2", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + "502.78936", + "-374.05754", + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": { + "elementId": "id184", + "focus": 0, + "gap": 1, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 10, + "width": "502.78936", + "x": "-0.83465", + "y": "-36.58211", +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should update bound element points when rectangle was remotely moved and arrow is added back through the history > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id184" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": -100, + "y": -50, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id185" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 100, + "y": -50, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id186": true, + }, + "selectedLinearElementId": "id186", + }, + "inserted": { + "selectedElementIds": {}, + "selectedLinearElementId": null, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id186" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": { + "elementId": "id185", + "focus": -0, + "gap": 1, + }, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 0, + "index": "a2", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 100, + 0, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": { + "elementId": "id184", + "focus": 0, + "gap": 1, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "width": 100, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map { + "id184" => Delta { + "deleted": { + "boundElements": [ + { + "id": "id186", + "type": "arrow", + }, + ], + }, + "inserted": { + "boundElements": [], + }, + }, + "id185" => Delta { + "deleted": { + "boundElements": [ + { + "id": "id186", + "type": "arrow", + }, + ], + }, + "inserted": { + "boundElements": [], + }, + }, + }, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should update bound element points when rectangle was remotely moved and arrow is added back through the history > [end of test] number of elements 1`] = `3`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should update bound element points when rectangle was remotely moved and arrow is added back through the history > [end of test] number of renders 1`] = `9`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should preserve latest remotely added binding and unbind previous one when the container is added through the history > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should preserve latest remotely added binding and unbind previous one when the container is added through the history > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id153", + "type": "text", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id151", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 7, + "width": 100, + "x": 10, + "y": 10, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should preserve latest remotely added binding and unbind previous one when the container is added through the history > [end of test] element 1 1`] = ` +{ + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": null, + "containerId": null, + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 5, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id152", + "index": "a1", + "isDeleted": false, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "que pasa", + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "que pasa", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 6, + "verticalAlign": "top", + "width": 100, + "x": 15, + "y": 15, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should preserve latest remotely added binding and unbind previous one when the container is added through the history > [end of test] element 2 1`] = ` +{ + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": null, + "containerId": "id151", + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 5, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": "id153", + "index": "a2", + "isDeleted": false, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "ola", + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "ola", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 11, + "verticalAlign": "top", + "width": 30, + "x": 15, + "y": 15, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should preserve latest remotely added binding and unbind previous one when the container is added through the history > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id151" => Delta { + "deleted": { + "isDeleted": false, + }, + "inserted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 10, + "y": 10, + }, + }, + "id152" => Delta { + "deleted": { + "containerId": null, + }, + "inserted": { + "containerId": null, + }, + }, + }, + }, + }, + ], + "undoStack": [], +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should preserve latest remotely added binding and unbind previous one when the container is added through the history > [end of test] number of elements 1`] = `3`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should preserve latest remotely added binding and unbind previous one when the container is added through the history > [end of test] number of renders 1`] = `11`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should preserve latest remotely added binding and unbind previous one when the text is added through history > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should preserve latest remotely added binding and unbind previous one when the text is added through history > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id156", + "type": "text", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id154", + "index": "Zz", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 11, + "width": 100, + "x": 10, + "y": 10, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should preserve latest remotely added binding and unbind previous one when the text is added through history > [end of test] element 1 1`] = ` +{ + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": null, + "containerId": "id154", + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 5, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id155", + "index": "a0", + "isDeleted": true, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "que pasa", + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "que pasa", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 9, + "verticalAlign": "top", + "width": 100, + "x": 15, + "y": 15, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should preserve latest remotely added binding and unbind previous one when the text is added through history > [end of test] element 2 1`] = ` +{ + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": null, + "containerId": "id154", + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 5, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": "id156", + "index": "a1", + "isDeleted": false, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "ola", + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "ola", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 11, + "verticalAlign": "top", + "width": 30, + "x": 15, + "y": 15, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should preserve latest remotely added binding and unbind previous one when the text is added through history > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map { + "id155" => Delta { + "deleted": { + "containerId": "id154", + "isDeleted": true, + }, + "inserted": { + "containerId": null, + "isDeleted": false, + }, + }, + }, + "removed": Map {}, + "updated": Map { + "id154" => Delta { + "deleted": { + "boundElements": [], + }, + "inserted": { + "boundElements": [ + { + "id": "id155", + "type": "text", + }, + ], + }, + }, + }, + }, + }, + ], + "undoStack": [], +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should preserve latest remotely added binding and unbind previous one when the text is added through history > [end of test] number of elements 1`] = `3`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should preserve latest remotely added binding and unbind previous one when the text is added through history > [end of test] number of renders 1`] = `11`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind bindings when both are updated through the history and the container got bound to a different text in the meantime > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind bindings when both are updated through the history and the container got bound to a different text in the meantime > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id143", + "type": "text", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id141", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 10, + "width": 100, + "x": 10, + "y": 10, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind bindings when both are updated through the history and the container got bound to a different text in the meantime > [end of test] element 1 1`] = ` +{ + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": null, + "containerId": "id141", + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 5, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": "id143", + "index": "a0V", + "isDeleted": false, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "ola", + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "ola", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 7, + "verticalAlign": "top", + "width": 30, + "x": 15, + "y": 15, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind bindings when both are updated through the history and the container got bound to a different text in the meantime > [end of test] element 2 1`] = ` +{ + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": null, + "containerId": null, + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 5, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": "id142", + "index": "a1", + "isDeleted": false, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "que pasa", + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "que pasa", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 9, + "verticalAlign": "top", + "width": 80, + "x": 15, + "y": 15, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind bindings when both are updated through the history and the container got bound to a different text in the meantime > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id141" => Delta { + "deleted": { + "boundElements": [ + { + "id": "id143", + "type": "text", + }, + ], + }, + "inserted": { + "boundElements": [ + { + "id": "id142", + "type": "text", + }, + ], + }, + }, + "id142" => Delta { + "deleted": { + "containerId": null, + }, + "inserted": { + "containerId": "id141", + }, + }, + "id143" => Delta { + "deleted": { + "containerId": "id141", + }, + "inserted": { + "containerId": null, + }, + }, + }, + }, + }, + ], + "undoStack": [], +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind bindings when both are updated through the history and the container got bound to a different text in the meantime > [end of test] number of elements 1`] = `3`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind bindings when both are updated through the history and the container got bound to a different text in the meantime > [end of test] number of renders 1`] = `11`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind bindings when both are updated through the history and the text got bound to a different container in the meantime > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind bindings when both are updated through the history and the text got bound to a different container in the meantime > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id144", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 8, + "width": 100, + "x": 10, + "y": 10, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind bindings when both are updated through the history and the text got bound to a different container in the meantime > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id145", + "type": "text", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 60, + "id": "id146", + "index": "a0V", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 8, + "width": 50, + "x": 100, + "y": 100, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind bindings when both are updated through the history and the text got bound to a different container in the meantime > [end of test] element 2 1`] = ` +{ + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": null, + "containerId": "id146", + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 5, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 50, + "id": "id145", + "index": "a1", + "isDeleted": false, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "que pasa", + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "que +pasa", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 13, + "verticalAlign": "top", + "width": 40, + "x": 105, + "y": 105, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind bindings when both are updated through the history and the text got bound to a different container in the meantime > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id144" => Delta { + "deleted": { + "boundElements": [], + }, + "inserted": { + "boundElements": [ + { + "id": "id145", + "type": "text", + }, + ], + }, + }, + "id145" => Delta { + "deleted": { + "containerId": "id146", + }, + "inserted": { + "containerId": "id144", + }, + }, + "id146" => Delta { + "deleted": { + "boundElements": [ + { + "id": "id145", + "type": "text", + }, + ], + }, + "inserted": { + "boundElements": [], + }, + }, + }, + }, + }, + ], + "undoStack": [], +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind bindings when both are updated through the history and the text got bound to a different container in the meantime > [end of test] number of elements 1`] = `3`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind bindings when both are updated through the history and the text got bound to a different container in the meantime > [end of test] number of renders 1`] = `11`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind bindings when both are updated through the history and there no conflicting updates in the meantime > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind bindings when both are updated through the history and there no conflicting updates in the meantime > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id139", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 9, + "width": 100, + "x": 35, + "y": 10, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind bindings when both are updated through the history and there no conflicting updates in the meantime > [end of test] element 1 1`] = ` +{ + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": null, + "containerId": null, + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 5, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": "id140", + "index": "a1", + "isDeleted": false, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "que pasa", + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "que pasa", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 10, + "verticalAlign": "top", + "width": 80, + "x": 40, + "y": 15, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind bindings when both are updated through the history and there no conflicting updates in the meantime > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id139" => Delta { + "deleted": { + "boundElements": [], + }, + "inserted": { + "boundElements": [ + { + "id": "id140", + "type": "text", + }, + ], + }, + }, + "id140" => Delta { + "deleted": { + "containerId": null, + }, + "inserted": { + "containerId": "id139", + }, + }, + }, + }, + }, + ], + "undoStack": [], +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind bindings when both are updated through the history and there no conflicting updates in the meantime > [end of test] number of elements 1`] = `2`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind bindings when both are updated through the history and there no conflicting updates in the meantime > [end of test] number of renders 1`] = `11`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind remotely added bound text when it's container is added through the history > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind remotely added bound text when it's container is added through the history > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id148", + "type": "text", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id147", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 7, + "width": 100, + "x": 10, + "y": 10, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind remotely added bound text when it's container is added through the history > [end of test] element 1 1`] = ` +{ + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": null, + "containerId": "id147", + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 5, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": "id148", + "index": "a1", + "isDeleted": false, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "que pasa", + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "que pasa", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 12, + "verticalAlign": "top", + "width": 80, + "x": 15, + "y": 15, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind remotely added bound text when it's container is added through the history > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id147" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map { + "id148" => Delta { + "deleted": { + "containerId": "id147", + }, + "inserted": { + "containerId": null, + }, + }, + }, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind remotely added bound text when it's container is added through the history > [end of test] number of elements 1`] = `2`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind remotely added bound text when it's container is added through the history > [end of test] number of renders 1`] = `9`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind remotely added container when it's bound text is added through the history > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind remotely added container when it's bound text is added through the history > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id150", + "type": "text", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id149", + "index": "Zz", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 11, + "width": 100, + "x": 10, + "y": 10, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind remotely added container when it's bound text is added through the history > [end of test] element 1 1`] = ` +{ + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": null, + "containerId": "id149", + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 5, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": "id150", + "index": "a0", + "isDeleted": false, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "que pasa", + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "que pasa", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 8, + "verticalAlign": "top", + "width": 80, + "x": 15, + "y": 15, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind remotely added container when it's bound text is added through the history > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id150" => Delta { + "deleted": { + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": null, + "containerId": "id149", + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 5, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "index": "a0", + "isDeleted": false, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "que pasa", + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "que pasa", + "textAlign": "left", + "type": "text", + "verticalAlign": "top", + "width": 80, + "x": 15, + "y": 15, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map { + "id149" => Delta { + "deleted": { + "boundElements": [ + { + "id": "id150", + "type": "text", + }, + ], + }, + "inserted": { + "boundElements": [], + }, + }, + }, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind remotely added container when it's bound text is added through the history > [end of test] number of elements 1`] = `2`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should rebind remotely added container when it's bound text is added through the history > [end of test] number of renders 1`] = `9`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should redraw bound text to match container dimensions when the bound text is updated through the history > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should redraw bound text to match container dimensions when the bound text is updated through the history > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id164", + "type": "text", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id163", + "index": "Zz", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 3, + "width": 100, + "x": 10, + "y": 10, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should redraw bound text to match container dimensions when the bound text is updated through the history > [end of test] element 1 1`] = ` +{ + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": null, + "containerId": "id163", + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 5, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": "id164", + "index": "a0", + "isDeleted": false, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "que pasa", + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "que pasa", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 7, + "verticalAlign": "top", + "width": 80, + "x": 15, + "y": 15, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should redraw bound text to match container dimensions when the bound text is updated through the history > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id164" => Delta { + "deleted": { + "angle": 0, + "x": 15, + "y": 15, + }, + "inserted": { + "angle": 0, + "x": 15, + "y": 15, + }, + }, + }, + }, + }, + ], + "undoStack": [], +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should redraw bound text to match container dimensions when the bound text is updated through the history > [end of test] number of elements 1`] = `2`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should redraw bound text to match container dimensions when the bound text is updated through the history > [end of test] number of renders 1`] = `9`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should redraw remotely added bound text when it's container is updated through the history > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should redraw remotely added bound text when it's container is updated through the history > [end of test] element 0 1`] = ` +{ + "angle": 90, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id162", + "type": "text", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id161", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 8, + "width": 100, + "x": 200, + "y": 200, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should redraw remotely added bound text when it's container is updated through the history > [end of test] element 1 1`] = ` +{ + "angle": 90, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": null, + "containerId": "id161", + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 5, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": "id162", + "index": "a1", + "isDeleted": false, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "que pasa", + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "que pasa", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 6, + "verticalAlign": "top", + "width": 80, + "x": 205, + "y": 205, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should redraw remotely added bound text when it's container is updated through the history > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id161" => Delta { + "deleted": { + "angle": 90, + "x": 200, + "y": 200, + }, + "inserted": { + "angle": 0, + "x": 10, + "y": 10, + }, + }, + }, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should redraw remotely added bound text when it's container is updated through the history > [end of test] number of elements 1`] = `2`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should redraw remotely added bound text when it's container is updated through the history > [end of test] number of renders 1`] = `10`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should unbind remotely deleted bound text from container when the container is added through the history > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should unbind remotely deleted bound text from container when the container is added through the history > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id157", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 8, + "width": 100, + "x": 10, + "y": 10, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should unbind remotely deleted bound text from container when the container is added through the history > [end of test] element 1 1`] = ` +{ + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": null, + "containerId": "id157", + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 5, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id158", + "index": "a1", + "isDeleted": true, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "que pasa", + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "que pasa", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 3, + "verticalAlign": "top", + "width": 100, + "x": 15, + "y": 15, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should unbind remotely deleted bound text from container when the container is added through the history > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id157" => Delta { + "deleted": { + "boundElements": [], + "isDeleted": false, + }, + "inserted": { + "boundElements": [ + { + "id": "id158", + "type": "text", + }, + ], + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should unbind remotely deleted bound text from container when the container is added through the history > [end of test] number of elements 1`] = `2`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should unbind remotely deleted bound text from container when the container is added through the history > [end of test] number of renders 1`] = `9`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should unbind remotely deleted container from bound text when the text is added through the history > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should unbind remotely deleted container from bound text when the text is added through the history > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id160", + "type": "text", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id159", + "index": "Zz", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 3, + "width": 100, + "x": 10, + "y": 10, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should unbind remotely deleted container from bound text when the text is added through the history > [end of test] element 1 1`] = ` +{ + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": null, + "containerId": null, + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 5, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id160", + "index": "a0", + "isDeleted": false, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "que pasa", + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "que pasa", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 8, + "verticalAlign": "top", + "width": 100, + "x": 15, + "y": 15, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should unbind remotely deleted container from bound text when the text is added through the history > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id160" => Delta { + "deleted": { + "containerId": null, + "isDeleted": false, + }, + "inserted": { + "containerId": "id159", + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should unbind remotely deleted container from bound text when the text is added through the history > [end of test] number of elements 1`] = `2`; + +exports[`history > multiplayer undo/redo > conflicts in bound text elements and their containers > should unbind remotely deleted container from bound text when the text is added through the history > [end of test] number of renders 1`] = `9`; + +exports[`history > multiplayer undo/redo > conflicts in frames and their children > should not rebind frame child with frame when frame was remotely deleted and frame child is added back through the history > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in frames and their children > should not rebind frame child with frame when frame was remotely deleted and frame child is added back through the history > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id188", + "index": "Zz", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 10, + "width": 100, + "x": 10, + "y": 10, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in frames and their children > should not rebind frame child with frame when frame was remotely deleted and frame child is added back through the history > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 500, + "id": "id187", + "index": "a0", + "isDeleted": true, + "link": null, + "locked": false, + "name": null, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "frame", + "updated": 1, + "version": 3, + "width": 500, + "x": 0, + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > conflicts in frames and their children > should not rebind frame child with frame when frame was remotely deleted and frame child is added back through the history > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id188" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "Zz", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id188" => Delta { + "deleted": { + "frameId": "id187", + }, + "inserted": { + "frameId": null, + }, + }, + }, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > conflicts in frames and their children > should not rebind frame child with frame when frame was remotely deleted and frame child is added back through the history > [end of test] number of elements 1`] = `2`; + +exports[`history > multiplayer undo/redo > conflicts in frames and their children > should not rebind frame child with frame when frame was remotely deleted and frame child is added back through the history > [end of test] number of renders 1`] = `13`; + +exports[`history > multiplayer undo/redo > should iterate through the history when editing group contains only remotely deleted elements > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id116": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when editing group contains only remotely deleted elements > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "A", + ], + "height": 100, + "id": "id115", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 5, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when editing group contains only remotely deleted elements > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "A", + ], + "height": 100, + "id": "id116", + "index": "a1", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "width": 100, + "x": 100, + "y": 100, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when editing group contains only remotely deleted elements > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id115": true, + "id116": true, + }, + "selectedGroupIds": { + "A": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + "selectedGroupIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id115" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "A", + ], + "height": 100, + "index": "a0", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id116" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "A", + ], + "height": 100, + "index": "a1", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 100, + "y": 100, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "editingGroupId": "A", + "selectedElementIds": {}, + "selectedGroupIds": {}, + }, + "inserted": { + "editingGroupId": null, + "selectedElementIds": { + "id115": true, + }, + "selectedGroupIds": { + "A": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "editingGroupId": null, + "selectedElementIds": {}, + }, + "inserted": { + "editingGroupId": "A", + "selectedElementIds": { + "id116": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when editing group contains only remotely deleted elements > [end of test] number of elements 1`] = `2`; + +exports[`history > multiplayer undo/redo > should iterate through the history when editing group contains only remotely deleted elements > [end of test] number of renders 1`] = `13`; + +exports[`history > multiplayer undo/redo > should iterate through the history when element changes relate only to remotely deleted elements > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "#ffc9c9", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": "elementBackground", + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id103": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when element changes relate only to remotely deleted elements > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id101", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 3, + "width": 10, + "x": 10, + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when element changes relate only to remotely deleted elements > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "#ffc9c9", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id102", + "index": "a1", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 7, + "width": 10, + "x": 20, + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when element changes relate only to remotely deleted elements > [end of test] element 2 1`] = ` +{ + "angle": 0, + "backgroundColor": "#ffc9c9", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id103", + "index": "a2", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 7, + "width": 10, + "x": 50, + "y": 50, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when element changes relate only to remotely deleted elements > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id101": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id101" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id102": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id101": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id102" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "#ffc9c9", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 20, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id102" => Delta { + "deleted": { + "backgroundColor": "#ffc9c9", + }, + "inserted": { + "backgroundColor": "transparent", + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id103": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id102": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id103" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "#ffc9c9", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a2", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 50, + "y": 50, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id103" => Delta { + "deleted": { + "x": 50, + "y": 50, + }, + "inserted": { + "x": 30, + "y": 30, + }, + }, + }, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when element changes relate only to remotely deleted elements > [end of test] number of elements 1`] = `3`; + +exports[`history > multiplayer undo/redo > should iterate through the history when element changes relate only to remotely deleted elements > [end of test] number of renders 1`] = `17`; + +exports[`history > multiplayer undo/redo > should iterate through the history when selected elements relate only to remotely deleted elements > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id106": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id106": true, + "id107": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when selected elements relate only to remotely deleted elements > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id105", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "width": 100, + "x": 10, + "y": 10, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when selected elements relate only to remotely deleted elements > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id106", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "width": 100, + "x": 20, + "y": 20, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when selected elements relate only to remotely deleted elements > [end of test] element 2 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id107", + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "width": 100, + "x": 30, + "y": 30, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when selected elements relate only to remotely deleted elements > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id105": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id105" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id106" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 20, + "y": 20, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id107" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 30, + "y": 30, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id106": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id105": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id107": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when selected elements relate only to remotely deleted elements > [end of test] number of elements 1`] = `3`; + +exports[`history > multiplayer undo/redo > should iterate through the history when selected elements relate only to remotely deleted elements > [end of test] number of renders 1`] = `15`; + +exports[`history > multiplayer undo/redo > should iterate through the history when selected groups contain only remotely deleted elements > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id111": true, + "id112": true, + "id113": true, + "id114": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": { + "A": true, + "B": true, + }, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when selected groups contain only remotely deleted elements > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "A", + ], + "height": 100, + "id": "id111", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when selected groups contain only remotely deleted elements > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "A", + ], + "height": 100, + "id": "id112", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when selected groups contain only remotely deleted elements > [end of test] element 2 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "B", + ], + "height": 100, + "id": "id113", + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when selected groups contain only remotely deleted elements > [end of test] element 3 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "B", + ], + "height": 100, + "id": "id114", + "index": "a3", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when selected groups contain only remotely deleted elements > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id111": true, + "id112": true, + }, + "selectedGroupIds": { + "A": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + "selectedGroupIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id113": true, + "id114": true, + }, + "selectedGroupIds": { + "B": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + "selectedGroupIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when selected groups contain only remotely deleted elements > [end of test] number of elements 1`] = `4`; + +exports[`history > multiplayer undo/redo > should iterate through the history when selected groups contain only remotely deleted elements > [end of test] number of renders 1`] = `15`; + +exports[`history > multiplayer undo/redo > should iterate through the history when selected or editing linear element was remotely deleted > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id119": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when selected or editing linear element was remotely deleted > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id119", + "index": "a0", + "isDeleted": true, + "lastCommittedPoint": [ + 10, + 10, + ], + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 10, + 10, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 7, + "width": 10, + "x": -10, + "y": -10, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when selected or editing linear element was remotely deleted > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id119": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id119" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "lastCommittedPoint": [ + 10, + 10, + ], + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 10, + 10, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "width": 10, + "x": -10, + "y": -10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedLinearElementId": "id119", + }, + "inserted": { + "selectedLinearElementId": null, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "editingLinearElementId": "id119", + }, + "inserted": { + "editingLinearElementId": null, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "editingLinearElementId": null, + }, + "inserted": { + "editingLinearElementId": "id119", + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when selected or editing linear element was remotely deleted > [end of test] number of elements 1`] = `1`; + +exports[`history > multiplayer undo/redo > should iterate through the history when selected or editing linear element was remotely deleted > [end of test] number of renders 1`] = `10`; + +exports[`history > multiplayer undo/redo > should iterate through the history when when element change relates to remotely deleted element > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "#ffc9c9", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": "elementBackground", + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when when element change relates to remotely deleted element > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "#ffec99", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id100", + "index": "a0", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 7, + "width": 10, + "x": 10, + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when when element change relates to remotely deleted element > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id100": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id100" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "#ffec99", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id100" => Delta { + "deleted": { + "backgroundColor": "#ffec99", + }, + "inserted": { + "backgroundColor": "transparent", + }, + }, + }, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when when element change relates to remotely deleted element > [end of test] number of elements 1`] = `1`; + +exports[`history > multiplayer undo/redo > should iterate through the history when when element change relates to remotely deleted element > [end of test] number of renders 1`] = `9`; + +exports[`history > multiplayer undo/redo > should iterate through the history when z-index changes do not produce visible change and we synced all indices > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when z-index changes do not produce visible change and we synced all indices > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id125", + "index": "a1", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 8, + "width": 100, + "x": 20, + "y": 20, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when z-index changes do not produce visible change and we synced all indices > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id126", + "index": "a3V", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "width": 100, + "x": 30, + "y": 30, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when z-index changes do not produce visible change and we synced all indices > [end of test] element 2 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id124", + "index": "a4", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "width": 100, + "x": 10, + "y": 10, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when z-index changes do not produce visible change and we synced all indices > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id125" => Delta { + "deleted": { + "index": "a1", + }, + "inserted": { + "index": "a3", + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": {}, + }, + "inserted": { + "selectedElementIds": { + "id125": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map { + "id124" => Delta { + "deleted": { + "isDeleted": true, + }, + "inserted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a4", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 10, + "y": 10, + }, + }, + "id125" => Delta { + "deleted": { + "isDeleted": true, + }, + "inserted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a3", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 20, + "y": 20, + }, + }, + "id126" => Delta { + "deleted": { + "isDeleted": true, + }, + "inserted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a3V", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 30, + "y": 30, + }, + }, + }, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], + "undoStack": [], +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when z-index changes do not produce visible change and we synced all indices > [end of test] number of elements 1`] = `3`; + +exports[`history > multiplayer undo/redo > should iterate through the history when z-index changes do not produce visible change and we synced all indices > [end of test] number of renders 1`] = `11`; + +exports[`history > multiplayer undo/redo > should iterate through the history when z-index changes do not produce visible change and we synced changed indices > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when z-index changes do not produce visible change and we synced changed indices > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id120", + "index": "Zx", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "width": 100, + "x": 10, + "y": 10, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when z-index changes do not produce visible change and we synced changed indices > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id122", + "index": "Zy", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "width": 100, + "x": 30, + "y": 30, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when z-index changes do not produce visible change and we synced changed indices > [end of test] element 2 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id121", + "index": "a1", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 7, + "width": 100, + "x": 20, + "y": 20, +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when z-index changes do not produce visible change and we synced changed indices > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id121" => Delta { + "deleted": { + "index": "a1", + }, + "inserted": { + "index": "Zz", + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": {}, + }, + "inserted": { + "selectedElementIds": { + "id121": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map { + "id120" => Delta { + "deleted": { + "isDeleted": true, + }, + "inserted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "Zx", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 10, + "y": 10, + }, + }, + "id121" => Delta { + "deleted": { + "isDeleted": true, + }, + "inserted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "Zz", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 20, + "y": 20, + }, + }, + "id122" => Delta { + "deleted": { + "isDeleted": true, + }, + "inserted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "Zy", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 30, + "y": 30, + }, + }, + }, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], + "undoStack": [], +} +`; + +exports[`history > multiplayer undo/redo > should iterate through the history when z-index changes do not produce visible change and we synced changed indices > [end of test] number of elements 1`] = `3`; + +exports[`history > multiplayer undo/redo > should iterate through the history when z-index changes do not produce visible change and we synced changed indices > [end of test] number of renders 1`] = `11`; + +exports[`history > multiplayer undo/redo > should not let remote changes to interfere with in progress dragging > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id133": true, + "id134": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id133": true, + "id134": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > should not let remote changes to interfere with in progress dragging > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id133", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 9, + "width": 10, + "x": 90, + "y": 90, +} +`; + +exports[`history > multiplayer undo/redo > should not let remote changes to interfere with in progress dragging > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id134", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 9, + "width": 10, + "x": 110, + "y": 110, +} +`; + +exports[`history > multiplayer undo/redo > should not let remote changes to interfere with in progress dragging > [end of test] element 2 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id138", + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#a5d8ff", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > should not let remote changes to interfere with in progress dragging > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id133": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id133" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id134": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id133": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id134" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 30, + "y": 30, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id133": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id134": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id134": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id133" => Delta { + "deleted": { + "x": 90, + "y": 90, + }, + "inserted": { + "x": 10, + "y": 10, + }, + }, + "id134" => Delta { + "deleted": { + "x": 110, + "y": 110, + }, + "inserted": { + "x": 30, + "y": 30, + }, + }, + }, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > should not let remote changes to interfere with in progress dragging > [end of test] number of elements 1`] = `3`; + +exports[`history > multiplayer undo/redo > should not let remote changes to interfere with in progress dragging > [end of test] number of renders 1`] = `25`; + +exports[`history > multiplayer undo/redo > should not let remote changes to interfere with in progress freedraw > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "freedraw", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > should not let remote changes to interfere with in progress freedraw > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 50, + "id": "id128", + "index": "a0", + "isDeleted": false, + "lastCommittedPoint": [ + 50, + 50, + ], + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 20, + 20, + ], + [ + 50, + 50, + ], + [ + 50, + 50, + ], + ], + "pressures": [ + 0, + 0, + 0, + 0, + ], + "roughness": 1, + "roundness": null, + "simulatePressure": false, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "freedraw", + "updated": 1, + "version": 7, + "width": 50, + "x": 10, + "y": 10, +} +`; + +exports[`history > multiplayer undo/redo > should not let remote changes to interfere with in progress freedraw > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id129", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#a5d8ff", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > should not let remote changes to interfere with in progress freedraw > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id128" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 50, + "index": "a0", + "isDeleted": false, + "lastCommittedPoint": [ + 50, + 50, + ], + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 20, + 20, + ], + [ + 50, + 50, + ], + [ + 50, + 50, + ], + ], + "pressures": [ + 0, + 0, + 0, + 0, + ], + "roughness": 1, + "roundness": null, + "simulatePressure": false, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "freedraw", + "width": 50, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > should not let remote changes to interfere with in progress freedraw > [end of test] number of elements 1`] = `2`; + +exports[`history > multiplayer undo/redo > should not let remote changes to interfere with in progress freedraw > [end of test] number of renders 1`] = `8`; + +exports[`history > multiplayer undo/redo > should not let remote changes to interfere with in progress resizing > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id130": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > should not let remote changes to interfere with in progress resizing > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 90, + "id": "id130", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 9, + "width": 90, + "x": 10, + "y": 10, +} +`; + +exports[`history > multiplayer undo/redo > should not let remote changes to interfere with in progress resizing > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id132", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#a5d8ff", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > should not let remote changes to interfere with in progress resizing > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id130": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id130" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id130" => Delta { + "deleted": { + "height": 90, + "width": 90, + }, + "inserted": { + "height": 10, + "width": 10, + }, + }, + }, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > should not let remote changes to interfere with in progress resizing > [end of test] number of elements 1`] = `2`; + +exports[`history > multiplayer undo/redo > should not let remote changes to interfere with in progress resizing > [end of test] number of renders 1`] = `13`; + +exports[`history > multiplayer undo/redo > should not override remote changes on different elements > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "#ffc9c9", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": "elementBackground", + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id87": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > should not override remote changes on different elements > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id87", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 7, + "width": 10, + "x": 10, + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > should not override remote changes on different elements > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "#ffc9c9", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id88", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#a5d8ff", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > should not override remote changes on different elements > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id87" => Delta { + "deleted": { + "backgroundColor": "transparent", + }, + "inserted": { + "backgroundColor": "#ffc9c9", + }, + }, + }, + }, + }, + ], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id87": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id87" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > should not override remote changes on different elements > [end of test] number of elements 1`] = `2`; + +exports[`history > multiplayer undo/redo > should not override remote changes on different elements > [end of test] number of renders 1`] = `10`; + +exports[`history > multiplayer undo/redo > should not override remote changes on different properties > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "#ffc9c9", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": "elementBackground", + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id89": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > should not override remote changes on different properties > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "#ffc9c9", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id89", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#ffec99", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 7, + "width": 10, + "x": 10, + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > should not override remote changes on different properties > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id89": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id89" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id89" => Delta { + "deleted": { + "backgroundColor": "#ffc9c9", + }, + "inserted": { + "backgroundColor": "transparent", + }, + }, + }, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > should not override remote changes on different properties > [end of test] number of elements 1`] = `1`; + +exports[`history > multiplayer undo/redo > should not override remote changes on different properties > [end of test] number of renders 1`] = `9`; + +exports[`history > multiplayer undo/redo > should override remotely added groups on undo, but restore them on redo > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > should override remotely added groups on undo, but restore them on redo > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "A", + "B", + ], + "height": 100, + "id": "id94", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 6, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > should override remotely added groups on undo, but restore them on redo > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "A", + "B", + ], + "height": 100, + "id": "id95", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 6, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > should override remotely added groups on undo, but restore them on redo > [end of test] element 2 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "B", + ], + "height": 100, + "id": "id96", + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > should override remotely added groups on undo, but restore them on redo > [end of test] element 3 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "B", + ], + "height": 100, + "id": "id97", + "index": "a3", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > should override remotely added groups on undo, but restore them on redo > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id94" => Delta { + "deleted": { + "groupIds": [ + "A", + "B", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + "id95" => Delta { + "deleted": { + "groupIds": [ + "A", + "B", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + }, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > should override remotely added groups on undo, but restore them on redo > [end of test] number of elements 1`] = `4`; + +exports[`history > multiplayer undo/redo > should override remotely added groups on undo, but restore them on redo > [end of test] number of renders 1`] = `8`; + +exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id98": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 30, + "id": "id98", + "index": "a0", + "isDeleted": false, + "lastCommittedPoint": [ + 30, + 30, + ], + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 5, + 5, + ], + [ + 10, + 10, + ], + [ + 15, + 15, + ], + [ + 20, + 20, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 13, + "width": 30, + "x": 0, + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id98": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id98" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "lastCommittedPoint": [ + 10, + 10, + ], + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 10, + 10, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "width": 10, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id98" => Delta { + "deleted": { + "height": 30, + "lastCommittedPoint": [ + 30, + 30, + ], + "points": [ + [ + 0, + 0, + ], + [ + 5, + 5, + ], + [ + 10, + 10, + ], + [ + 15, + 15, + ], + [ + 20, + 20, + ], + ], + "width": 30, + }, + "inserted": { + "height": 10, + "lastCommittedPoint": [ + 10, + 10, + ], + "points": [ + [ + 0, + 0, + ], + [ + 10, + 10, + ], + ], + "width": 10, + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedLinearElementId": "id98", + }, + "inserted": { + "selectedLinearElementId": null, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] number of elements 1`] = `1`; + +exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] number of renders 1`] = `15`; + +exports[`history > multiplayer undo/redo > should redistribute deltas when element gets removed locally but is restored remotely > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > should redistribute deltas when element gets removed locally but is restored remotely > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "#ffec99", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id99", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 7, + "width": 10, + "x": 10, + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > should redistribute deltas when element gets removed locally but is restored remotely > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id99": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id99" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "#ffec99", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": {}, + }, + "inserted": { + "selectedElementIds": { + "id99": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id99" => Delta { + "deleted": { + "isDeleted": false, + }, + "inserted": { + "isDeleted": false, + }, + }, + }, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > should redistribute deltas when element gets removed locally but is restored remotely > [end of test] number of elements 1`] = `1`; + +exports[`history > multiplayer undo/redo > should redistribute deltas when element gets removed locally but is restored remotely > [end of test] number of renders 1`] = `11`; + +exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 126, + "id": "KPrBI4g_v9qUB1XxYLgSz", + "index": "a0", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "width": 157, + "x": 600, + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 129, + "id": "u2JGnnmoJ0VATV4vCNJE5", + "index": "a1", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "diamond", + "updated": 1, + "version": 4, + "width": 124, + "x": 1152, + "y": 516, +} +`; + +exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end of test] element 2 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": true, + "endArrowhead": null, + "endBinding": { + "elementId": "u2JGnnmoJ0VATV4vCNJE5", + "fixedPoint": [ + "0.49919", + "-0.03875", + ], + "focus": "-0.00161", + "gap": "3.53708", + }, + "endIsSpecial": false, + "fillStyle": "solid", + "fixedSegments": [], + "frameId": null, + "groupIds": [], + "height": "236.10000", + "id": "6Rm4g567UQM4WjLwej2Vc", + "index": "a2", + "isDeleted": true, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + "178.90000", + 0, + ], + [ + "178.90000", + "236.10000", + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": { + "elementId": "KPrBI4g_v9qUB1XxYLgSz", + "fixedPoint": [ + "1.03185", + "0.49921", + ], + "focus": "-0.00159", + "gap": 5, + }, + "startIsSpecial": false, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 3, + "width": "178.90000", + "x": 1035, + "y": "274.90000", +} +`; + +exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map { + "6Rm4g567UQM4WjLwej2Vc" => Delta { + "deleted": { + "isDeleted": true, + }, + "inserted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": true, + "endArrowhead": null, + "endBinding": { + "elementId": "u2JGnnmoJ0VATV4vCNJE5", + "fixedPoint": [ + "0.49919", + "-0.03875", + ], + "focus": "-0.00161", + "gap": "3.53708", + }, + "endIsSpecial": false, + "fillStyle": "solid", + "fixedSegments": [], + "frameId": null, + "groupIds": [], + "height": "236.10000", + "index": "a2", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + "178.90000", + 0, + ], + [ + "178.90000", + "236.10000", + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": { + "elementId": "KPrBI4g_v9qUB1XxYLgSz", + "fixedPoint": [ + "1.03185", + "0.49921", + ], + "focus": "-0.00159", + "gap": 5, + }, + "startIsSpecial": false, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "width": "178.90000", + "x": 1035, + "y": "274.90000", + }, + }, + }, + "removed": Map {}, + "updated": Map { + "KPrBI4g_v9qUB1XxYLgSz" => Delta { + "deleted": { + "boundElements": [], + }, + "inserted": { + "boundElements": [ + { + "id": "6Rm4g567UQM4WjLwej2Vc", + "type": "arrow", + }, + ], + }, + }, + "u2JGnnmoJ0VATV4vCNJE5" => Delta { + "deleted": { + "boundElements": [], + }, + "inserted": { + "boundElements": [ + { + "id": "6Rm4g567UQM4WjLwej2Vc", + "type": "arrow", + }, + ], + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map { + "KPrBI4g_v9qUB1XxYLgSz" => Delta { + "deleted": { + "isDeleted": true, + }, + "inserted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 126, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 157, + "x": 600, + "y": 0, + }, + }, + "u2JGnnmoJ0VATV4vCNJE5" => Delta { + "deleted": { + "isDeleted": true, + }, + "inserted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 129, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "diamond", + "width": 124, + "x": 1152, + "y": 516, + }, + }, + }, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], + "undoStack": [], +} +`; + +exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end of test] number of elements 1`] = `3`; + +exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end of test] number of renders 1`] = `8`; + +exports[`history > multiplayer undo/redo > should update history entries after remote changes on the same properties > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "#a5d8ff", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": "elementBackground", + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id90": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > multiplayer undo/redo > should update history entries after remote changes on the same properties > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id90", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 13, + "width": 10, + "x": 10, + "y": 0, +} +`; + +exports[`history > multiplayer undo/redo > should update history entries after remote changes on the same properties > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id90" => Delta { + "deleted": { + "backgroundColor": "#d0bfff", + }, + "inserted": { + "backgroundColor": "#ffec99", + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id90" => Delta { + "deleted": { + "backgroundColor": "transparent", + }, + "inserted": { + "backgroundColor": "#d0bfff", + }, + }, + }, + }, + }, + ], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id90": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id90" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > multiplayer undo/redo > should update history entries after remote changes on the same properties > [end of test] number of elements 1`] = `1`; + +exports[`history > multiplayer undo/redo > should update history entries after remote changes on the same properties > [end of test] number of renders 1`] = `15`; + +exports[`history > singleplayer undo/redo > remounting undo/redo buttons should initialize undo/redo state correctly > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": false, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > remounting undo/redo buttons should initialize undo/redo state correctly > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "A", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > remounting undo/redo buttons should initialize undo/redo state correctly > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id86", + "index": "a1", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "width": 10, + "x": 0, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > remounting undo/redo buttons should initialize undo/redo state correctly > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": {}, + }, + "inserted": { + "selectedElementIds": { + "id86": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map { + "id86" => Delta { + "deleted": { + "isDeleted": true, + }, + "inserted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 0, + "y": 0, + }, + }, + }, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], + "undoStack": [], +} +`; + +exports[`history > singleplayer undo/redo > remounting undo/redo buttons should initialize undo/redo state correctly > [end of test] number of elements 1`] = `2`; + +exports[`history > singleplayer undo/redo > remounting undo/redo buttons should initialize undo/redo state correctly > [end of test] number of renders 1`] = `11`; + +exports[`history > singleplayer undo/redo > should clear the redo stack on elements change > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id14": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should clear the redo stack on elements change > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id13", + "index": "a0", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "width": 10, + "x": 10, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should clear the redo stack on elements change > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id14", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 3, + "width": 10, + "x": 20, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should clear the redo stack on elements change > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id14": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id14" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 20, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > singleplayer undo/redo > should clear the redo stack on elements change > [end of test] number of elements 1`] = `2`; + +exports[`history > singleplayer undo/redo > should clear the redo stack on elements change > [end of test] number of renders 1`] = `8`; + +exports[`history > singleplayer undo/redo > should create entry when selecting freedraw > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "freedraw", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#e03131", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": "elementStroke", + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should create entry when selecting freedraw > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id32", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 3, + "width": 10, + "x": 10, + "y": 10, +} +`; + +exports[`history > singleplayer undo/redo > should create entry when selecting freedraw > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id33", + "index": "a1", + "isDeleted": true, + "lastCommittedPoint": [ + 50, + 10, + ], + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 50, + 10, + ], + [ + 50, + 10, + ], + ], + "pressures": [ + 0, + 0, + 0, + ], + "roughness": 1, + "roundness": null, + "simulatePressure": false, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "freedraw", + "updated": 1, + "version": 5, + "width": 50, + "x": 60, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should create entry when selecting freedraw > [end of test] element 2 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id34", + "index": "a2", + "isDeleted": false, + "lastCommittedPoint": [ + 50, + 10, + ], + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 50, + 10, + ], + [ + 50, + 10, + ], + ], + "pressures": [ + 0, + 0, + 0, + ], + "roughness": 1, + "roundness": null, + "simulatePressure": false, + "strokeColor": "#e03131", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "freedraw", + "updated": 1, + "version": 4, + "width": 50, + "x": 150, + "y": -10, +} +`; + +exports[`history > singleplayer undo/redo > should create entry when selecting freedraw > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id32": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id32" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": {}, + }, + "inserted": { + "selectedElementIds": { + "id32": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id34" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a2", + "isDeleted": false, + "lastCommittedPoint": [ + 50, + 10, + ], + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 50, + 10, + ], + [ + 50, + 10, + ], + ], + "pressures": [ + 0, + 0, + 0, + ], + "roughness": 1, + "roundness": null, + "simulatePressure": false, + "strokeColor": "#e03131", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "freedraw", + "width": 50, + "x": 150, + "y": -10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > singleplayer undo/redo > should create entry when selecting freedraw > [end of test] number of elements 1`] = `3`; + +exports[`history > singleplayer undo/redo > should create entry when selecting freedraw > [end of test] number of renders 1`] = `12`; + +exports[`history > singleplayer undo/redo > should create new history entry on scene import via drag&drop > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": -50, + "scrollY": -50, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": false, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#000", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should create new history entry on scene import via drag&drop > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "A", + "index": "a0", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 5, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should create new history entry on scene import via drag&drop > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [], + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "B", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 5, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should create new history entry on scene import via drag&drop > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "viewBackgroundColor": "#000", + }, + "inserted": { + "viewBackgroundColor": "#FFF", + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map { + "A" => Delta { + "deleted": { + "isDeleted": true, + }, + "inserted": { + "isDeleted": false, + }, + }, + }, + "removed": Map { + "B" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [], + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > singleplayer undo/redo > should create new history entry on scene import via drag&drop > [end of test] number of elements 1`] = `2`; + +exports[`history > singleplayer undo/redo > should create new history entry on scene import via drag&drop > [end of test] number of renders 1`] = `6`; + +exports[`history > singleplayer undo/redo > should disable undo/redo buttons when stacks empty > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id84": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": false, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should disable undo/redo buttons when stacks empty > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "A", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should disable undo/redo buttons when stacks empty > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id84", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 5, + "width": 10, + "x": 0, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should disable undo/redo buttons when stacks empty > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id84": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id84" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > singleplayer undo/redo > should disable undo/redo buttons when stacks empty > [end of test] number of elements 1`] = `2`; + +exports[`history > singleplayer undo/redo > should disable undo/redo buttons when stacks empty > [end of test] number of renders 1`] = `7`; + +exports[`history > singleplayer undo/redo > should end up with no history entry after initializing scene > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id19": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": false, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": true, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should end up with no history entry after initializing scene > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "A", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should end up with no history entry after initializing scene > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id19", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 5, + "width": 10, + "x": 0, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should end up with no history entry after initializing scene > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id19": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id19" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > singleplayer undo/redo > should end up with no history entry after initializing scene > [end of test] number of elements 1`] = `2`; + +exports[`history > singleplayer undo/redo > should end up with no history entry after initializing scene > [end of test] number of renders 1`] = `7`; + +exports[`history > singleplayer undo/redo > should iterate through the history when selection changes do not produce visible change > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id15": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should iterate through the history when selection changes do not produce visible change > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id15", + "index": "a0", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "width": 10, + "x": 10, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should iterate through the history when selection changes do not produce visible change > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id15": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id15": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": {}, + }, + "inserted": { + "selectedElementIds": { + "id15": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map { + "id15" => Delta { + "deleted": { + "isDeleted": true, + }, + "inserted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 0, + }, + }, + }, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], + "undoStack": [], +} +`; + +exports[`history > singleplayer undo/redo > should iterate through the history when selection changes do not produce visible change > [end of test] number of elements 1`] = `1`; + +exports[`history > singleplayer undo/redo > should iterate through the history when selection changes do not produce visible change > [end of test] number of renders 1`] = `13`; + +exports[`history > singleplayer undo/redo > should not clear the redo stack on standalone appstate change > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id8": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should not clear the redo stack on standalone appstate change > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id7", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 3, + "width": 10, + "x": 10, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should not clear the redo stack on standalone appstate change > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id8", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 5, + "width": 10, + "x": 20, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should not clear the redo stack on standalone appstate change > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id7": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id7" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": {}, + }, + "inserted": { + "selectedElementIds": { + "id7": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id7": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id8": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id7": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id8" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 20, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > singleplayer undo/redo > should not clear the redo stack on standalone appstate change > [end of test] number of elements 1`] = `2`; + +exports[`history > singleplayer undo/redo > should not clear the redo stack on standalone appstate change > [end of test] number of renders 1`] = `12`; + +exports[`history > singleplayer undo/redo > should not collapse when applying corrupted history entry > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should not collapse when applying corrupted history entry > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should not collapse when applying corrupted history entry > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > singleplayer undo/redo > should not collapse when applying corrupted history entry > [end of test] number of elements 1`] = `1`; + +exports[`history > singleplayer undo/redo > should not collapse when applying corrupted history entry > [end of test] number of renders 1`] = `4`; + +exports[`history > singleplayer undo/redo > should not end up with history entry when there are no appstate changes > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id1": true, + "id2": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": { + "A": true, + }, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should not end up with history entry when there are no appstate changes > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "A", + ], + "height": 100, + "id": "id1", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should not end up with history entry when there are no appstate changes > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "A", + ], + "height": 100, + "id": "id2", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should not end up with history entry when there are no appstate changes > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + "id2": true, + }, + "selectedGroupIds": { + "A": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + "selectedGroupIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "A", + ], + "height": 100, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id2" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "A", + ], + "height": 100, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > singleplayer undo/redo > should not end up with history entry when there are no appstate changes > [end of test] number of elements 1`] = `2`; + +exports[`history > singleplayer undo/redo > should not end up with history entry when there are no appstate changes > [end of test] number of renders 1`] = `7`; + +exports[`history > singleplayer undo/redo > should not end up with history entry when there are no elements changes > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should not end up with history entry when there are no elements changes > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id5", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should not end up with history entry when there are no elements changes > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id6", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should not end up with history entry when there are no elements changes > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id5" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id6" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > singleplayer undo/redo > should not end up with history entry when there are no elements changes > [end of test] number of elements 1`] = `2`; + +exports[`history > singleplayer undo/redo > should not end up with history entry when there are no elements changes > [end of test] number of renders 1`] = `5`; + +exports[`history > singleplayer undo/redo > should not override appstate changes when redo stack is not cleared > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "#a5d8ff", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": "elementBackground", + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id11": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id11": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should not override appstate changes when redo stack is not cleared > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id11", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 11, + "width": 10, + "x": 10, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should not override appstate changes when redo stack is not cleared > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id11" => Delta { + "deleted": { + "backgroundColor": "#ffc9c9", + }, + "inserted": { + "backgroundColor": "#a5d8ff", + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id11" => Delta { + "deleted": { + "backgroundColor": "transparent", + }, + "inserted": { + "backgroundColor": "#ffc9c9", + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id11": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id11": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id11" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > singleplayer undo/redo > should not override appstate changes when redo stack is not cleared > [end of test] number of elements 1`] = `1`; + +exports[`history > singleplayer undo/redo > should not override appstate changes when redo stack is not cleared > [end of test] number of renders 1`] = `15`; + +exports[`history > singleplayer undo/redo > should support appstate name or viewBackgroundColor change > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#000", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should support appstate name or viewBackgroundColor change > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "name": "New name", + }, + "inserted": { + "name": "Old name", + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "viewBackgroundColor": "#000", + }, + "inserted": { + "viewBackgroundColor": "#FFF", + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > singleplayer undo/redo > should support appstate name or viewBackgroundColor change > [end of test] number of elements 1`] = `0`; + +exports[`history > singleplayer undo/redo > should support appstate name or viewBackgroundColor change > [end of test] number of renders 1`] = `8`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind arrow from non deleted bindable elements on deletion and rebind on undo > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id56": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id61": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind arrow from non deleted bindable elements on deletion and rebind on undo > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id57", + "type": "text", + }, + { + "id": "id61", + "type": "arrow", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id56", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 8, + "width": 100, + "x": -100, + "y": -50, +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind arrow from non deleted bindable elements on deletion and rebind on undo > [end of test] element 1 1`] = ` +{ + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": null, + "containerId": "id56", + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 5, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": "id57", + "index": "a1", + "isDeleted": false, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "ola", + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "ola", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 6, + "verticalAlign": "middle", + "width": 30, + "x": -65, + "y": "-12.50000", +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind arrow from non deleted bindable elements on deletion and rebind on undo > [end of test] element 2 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id61", + "type": "arrow", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id58", + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 7, + "width": 100, + "x": 100, + "y": -50, +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind arrow from non deleted bindable elements on deletion and rebind on undo > [end of test] element 3 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": { + "elementId": "id58", + "focus": -0, + "gap": 1, + }, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 0, + "id": "id61", + "index": "a3", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + "98.58579", + 0, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": { + "elementId": "id56", + "focus": 0, + "gap": 1, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 10, + "width": "98.58579", + "x": "0.70711", + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind arrow from non deleted bindable elements on deletion and rebind on undo > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id61": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id61" => Delta { + "deleted": { + "isDeleted": false, + "points": [ + [ + 0, + 0, + ], + [ + 100, + 0, + ], + ], + }, + "inserted": { + "isDeleted": true, + "points": [ + [ + 0, + 0, + ], + [ + 100, + 0, + ], + ], + }, + }, + }, + "updated": Map { + "id56" => Delta { + "deleted": { + "boundElements": [ + { + "id": "id61", + "type": "arrow", + }, + ], + }, + "inserted": { + "boundElements": [], + }, + }, + "id58" => Delta { + "deleted": { + "boundElements": [ + { + "id": "id61", + "type": "arrow", + }, + ], + }, + "inserted": { + "boundElements": [], + }, + }, + }, + }, + }, + ], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id56" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": -100, + "y": -50, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id57" => Delta { + "deleted": { + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": null, + "containerId": null, + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 5, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a1", + "isDeleted": false, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "ola", + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "ola", + "textAlign": "left", + "type": "text", + "verticalAlign": "top", + "width": 100, + "x": -200, + "y": -200, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id58" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 100, + "y": -50, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id56": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id57": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": {}, + }, + "inserted": { + "selectedElementIds": { + "id57": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id56" => Delta { + "deleted": { + "boundElements": [ + { + "id": "id57", + "type": "text", + }, + ], + }, + "inserted": { + "boundElements": [], + }, + }, + "id57" => Delta { + "deleted": { + "containerId": "id56", + "height": 25, + "textAlign": "center", + "verticalAlign": "middle", + "width": 30, + "x": -65, + "y": "-12.50000", + }, + "inserted": { + "containerId": null, + "height": 100, + "textAlign": "left", + "verticalAlign": "top", + "width": 100, + "x": -200, + "y": -200, + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id61": true, + }, + "selectedLinearElementId": "id61", + }, + "inserted": { + "selectedElementIds": { + "id56": true, + }, + "selectedLinearElementId": null, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id61" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": { + "elementId": "id58", + "focus": -0, + "gap": 1, + }, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 0, + "index": "a3", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 100, + 0, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": { + "elementId": "id56", + "focus": 0, + "gap": 1, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "width": 100, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map { + "id56" => Delta { + "deleted": { + "boundElements": [ + { + "id": "id61", + "type": "arrow", + }, + ], + }, + "inserted": { + "boundElements": [], + }, + }, + "id58" => Delta { + "deleted": { + "boundElements": [ + { + "id": "id61", + "type": "arrow", + }, + ], + }, + "inserted": { + "boundElements": [], + }, + }, + }, + }, + }, + ], +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind arrow from non deleted bindable elements on deletion and rebind on undo > [end of test] number of elements 1`] = `4`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind arrow from non deleted bindable elements on deletion and rebind on undo > [end of test] number of renders 1`] = `12`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind arrow from non deleted bindable elements on undo and rebind on redo > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id50": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id55": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind arrow from non deleted bindable elements on undo and rebind on redo > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id51", + "type": "text", + }, + { + "id": "id55", + "type": "arrow", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id50", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 8, + "width": 100, + "x": -100, + "y": -50, +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind arrow from non deleted bindable elements on undo and rebind on redo > [end of test] element 1 1`] = ` +{ + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": null, + "containerId": "id50", + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 5, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": "id51", + "index": "a1", + "isDeleted": false, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "ola", + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "ola", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 8, + "verticalAlign": "middle", + "width": 30, + "x": -65, + "y": "-12.50000", +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind arrow from non deleted bindable elements on undo and rebind on redo > [end of test] element 2 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id55", + "type": "arrow", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id52", + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 7, + "width": 100, + "x": 100, + "y": -50, +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind arrow from non deleted bindable elements on undo and rebind on redo > [end of test] element 3 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": { + "elementId": "id52", + "focus": -0, + "gap": 1, + }, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 0, + "id": "id55", + "index": "a3", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + "98.58579", + 0, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": { + "elementId": "id50", + "focus": 0, + "gap": 1, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 10, + "width": "98.58579", + "x": "0.70711", + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind arrow from non deleted bindable elements on undo and rebind on redo > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id50" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": -100, + "y": -50, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id51" => Delta { + "deleted": { + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": null, + "containerId": null, + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 5, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a1", + "isDeleted": false, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "ola", + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "ola", + "textAlign": "left", + "type": "text", + "verticalAlign": "top", + "width": 100, + "x": -200, + "y": -200, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id52" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 100, + "y": -50, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id50": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id51": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": {}, + }, + "inserted": { + "selectedElementIds": { + "id51": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id50" => Delta { + "deleted": { + "boundElements": [ + { + "id": "id51", + "type": "text", + }, + ], + }, + "inserted": { + "boundElements": [], + }, + }, + "id51" => Delta { + "deleted": { + "containerId": "id50", + "height": 25, + "textAlign": "center", + "verticalAlign": "middle", + "width": 30, + "x": -65, + "y": "-12.50000", + }, + "inserted": { + "containerId": null, + "height": 100, + "textAlign": "left", + "verticalAlign": "top", + "width": 100, + "x": -200, + "y": -200, + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id55": true, + }, + "selectedLinearElementId": "id55", + }, + "inserted": { + "selectedElementIds": { + "id50": true, + }, + "selectedLinearElementId": null, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id55" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": { + "elementId": "id52", + "focus": -0, + "gap": 1, + }, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 0, + "index": "a3", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 100, + 0, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": { + "elementId": "id50", + "focus": 0, + "gap": 1, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "width": 100, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map { + "id50" => Delta { + "deleted": { + "boundElements": [ + { + "id": "id55", + "type": "arrow", + }, + ], + }, + "inserted": { + "boundElements": [], + }, + }, + "id52" => Delta { + "deleted": { + "boundElements": [ + { + "id": "id55", + "type": "arrow", + }, + ], + }, + "inserted": { + "boundElements": [], + }, + }, + }, + }, + }, + ], +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind arrow from non deleted bindable elements on undo and rebind on redo > [end of test] number of elements 1`] = `4`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind arrow from non deleted bindable elements on undo and rebind on redo > [end of test] number of renders 1`] = `12`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind everything from non deleted elements when iterating through the whole undo stack and vice versa rebind everything on redo > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id62": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id67": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind everything from non deleted elements when iterating through the whole undo stack and vice versa rebind everything on redo > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id63", + "type": "text", + }, + { + "id": "id67", + "type": "arrow", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id62", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 12, + "width": 100, + "x": -100, + "y": -50, +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind everything from non deleted elements when iterating through the whole undo stack and vice versa rebind everything on redo > [end of test] element 1 1`] = ` +{ + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": null, + "containerId": "id62", + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 5, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": "id63", + "index": "a1", + "isDeleted": false, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "ola", + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "ola", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 12, + "verticalAlign": "middle", + "width": 30, + "x": -65, + "y": "-12.50000", +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind everything from non deleted elements when iterating through the whole undo stack and vice versa rebind everything on redo > [end of test] element 2 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id67", + "type": "arrow", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id64", + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 9, + "width": 100, + "x": 100, + "y": -50, +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind everything from non deleted elements when iterating through the whole undo stack and vice versa rebind everything on redo > [end of test] element 3 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": { + "elementId": "id64", + "focus": -0, + "gap": 1, + }, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 0, + "id": "id67", + "index": "a3", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + "98.58579", + 0, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": { + "elementId": "id62", + "focus": 0, + "gap": 1, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 10, + "width": "98.58579", + "x": "0.70711", + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind everything from non deleted elements when iterating through the whole undo stack and vice versa rebind everything on redo > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id62" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": -100, + "y": -50, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id63" => Delta { + "deleted": { + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": null, + "containerId": null, + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 5, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a1", + "isDeleted": false, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "ola", + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "ola", + "textAlign": "left", + "type": "text", + "verticalAlign": "top", + "width": 100, + "x": -200, + "y": -200, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id64" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 100, + "y": -50, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id62": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id63": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": {}, + }, + "inserted": { + "selectedElementIds": { + "id63": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id62" => Delta { + "deleted": { + "boundElements": [ + { + "id": "id63", + "type": "text", + }, + ], + }, + "inserted": { + "boundElements": [], + }, + }, + "id63" => Delta { + "deleted": { + "containerId": "id62", + "height": 25, + "textAlign": "center", + "verticalAlign": "middle", + "width": 30, + "x": -65, + "y": "-12.50000", + }, + "inserted": { + "containerId": null, + "height": 100, + "textAlign": "left", + "verticalAlign": "top", + "width": 100, + "x": -200, + "y": -200, + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id67": true, + }, + "selectedLinearElementId": "id67", + }, + "inserted": { + "selectedElementIds": { + "id62": true, + }, + "selectedLinearElementId": null, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id67" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": { + "elementId": "id64", + "focus": -0, + "gap": 1, + }, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 0, + "index": "a3", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 100, + 0, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": { + "elementId": "id62", + "focus": 0, + "gap": 1, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "width": 100, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map { + "id62" => Delta { + "deleted": { + "boundElements": [ + { + "id": "id67", + "type": "arrow", + }, + ], + }, + "inserted": { + "boundElements": [], + }, + }, + "id64" => Delta { + "deleted": { + "boundElements": [ + { + "id": "id67", + "type": "arrow", + }, + ], + }, + "inserted": { + "boundElements": [], + }, + }, + }, + }, + }, + ], +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind everything from non deleted elements when iterating through the whole undo stack and vice versa rebind everything on redo > [end of test] number of elements 1`] = `4`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind everything from non deleted elements when iterating through the whole undo stack and vice versa rebind everything on redo > [end of test] number of renders 1`] = `20`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind rectangle from arrow on deletion and rebind on undo > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id68": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind rectangle from arrow on deletion and rebind on undo > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id73", + "type": "arrow", + }, + { + "id": "id69", + "type": "text", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id68", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 8, + "width": 100, + "x": -100, + "y": -50, +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind rectangle from arrow on deletion and rebind on undo > [end of test] element 1 1`] = ` +{ + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": null, + "containerId": "id68", + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 5, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": "id69", + "index": "a1", + "isDeleted": false, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "ola", + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "ola", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 8, + "verticalAlign": "middle", + "width": 30, + "x": -65, + "y": "-12.50000", +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind rectangle from arrow on deletion and rebind on undo > [end of test] element 2 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id73", + "type": "arrow", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id70", + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 5, + "width": 100, + "x": 100, + "y": -50, +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind rectangle from arrow on deletion and rebind on undo > [end of test] element 3 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": { + "elementId": "id70", + "focus": -0, + "gap": 1, + }, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 0, + "id": "id73", + "index": "a3", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + "98.58579", + 0, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": { + "elementId": "id68", + "focus": 0, + "gap": 1, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 10, + "width": "98.58579", + "x": "0.70711", + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind rectangle from arrow on deletion and rebind on undo > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id68": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id68" => Delta { + "deleted": { + "isDeleted": false, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id69" => Delta { + "deleted": { + "isDeleted": false, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map { + "id73" => Delta { + "deleted": { + "points": [ + [ + 0, + 0, + ], + [ + 100, + 0, + ], + ], + "startBinding": { + "elementId": "id68", + "focus": 0, + "gap": 1, + }, + }, + "inserted": { + "points": [ + [ + 0, + 0, + ], + [ + 100, + 0, + ], + ], + "startBinding": null, + }, + }, + }, + }, + }, + ], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id68" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": -100, + "y": -50, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id69" => Delta { + "deleted": { + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": null, + "containerId": null, + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 5, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a1", + "isDeleted": false, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "ola", + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "ola", + "textAlign": "left", + "type": "text", + "verticalAlign": "top", + "width": 100, + "x": -200, + "y": -200, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id70" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 100, + "y": -50, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id68": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id69": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": {}, + }, + "inserted": { + "selectedElementIds": { + "id69": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id68" => Delta { + "deleted": { + "boundElements": [ + { + "id": "id69", + "type": "text", + }, + ], + }, + "inserted": { + "boundElements": [], + }, + }, + "id69" => Delta { + "deleted": { + "containerId": "id68", + "height": 25, + "textAlign": "center", + "verticalAlign": "middle", + "width": 30, + "x": -65, + "y": "-12.50000", + }, + "inserted": { + "containerId": null, + "height": 100, + "textAlign": "left", + "verticalAlign": "top", + "width": 100, + "x": -200, + "y": -200, + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id73": true, + }, + "selectedLinearElementId": "id73", + }, + "inserted": { + "selectedElementIds": { + "id68": true, + }, + "selectedLinearElementId": null, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id73" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": { + "elementId": "id70", + "focus": -0, + "gap": 1, + }, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 0, + "index": "a3", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 100, + 0, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": { + "elementId": "id68", + "focus": 0, + "gap": 1, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "width": 100, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map { + "id68" => Delta { + "deleted": { + "boundElements": [ + { + "id": "id73", + "type": "arrow", + }, + ], + }, + "inserted": { + "boundElements": [], + }, + }, + "id70" => Delta { + "deleted": { + "boundElements": [ + { + "id": "id73", + "type": "arrow", + }, + ], + }, + "inserted": { + "boundElements": [], + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id68": true, + }, + "selectedLinearElementId": null, + }, + "inserted": { + "selectedElementIds": { + "id73": true, + }, + "selectedLinearElementId": "id73", + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind rectangle from arrow on deletion and rebind on undo > [end of test] number of elements 1`] = `4`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind rectangle from arrow on deletion and rebind on undo > [end of test] number of renders 1`] = `14`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind rectangles from arrow on deletion and rebind on undo > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id75": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id75": true, + "id77": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind rectangles from arrow on deletion and rebind on undo > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id80", + "type": "arrow", + }, + { + "id": "id76", + "type": "text", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id75", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 8, + "width": 100, + "x": -100, + "y": -50, +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind rectangles from arrow on deletion and rebind on undo > [end of test] element 1 1`] = ` +{ + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": null, + "containerId": "id75", + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 5, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": "id76", + "index": "a1", + "isDeleted": false, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "ola", + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "ola", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 8, + "verticalAlign": "middle", + "width": 30, + "x": -65, + "y": "-12.50000", +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind rectangles from arrow on deletion and rebind on undo > [end of test] element 2 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id80", + "type": "arrow", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id77", + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 5, + "width": 100, + "x": 100, + "y": -50, +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind rectangles from arrow on deletion and rebind on undo > [end of test] element 3 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": { + "elementId": "id77", + "focus": -0, + "gap": 1, + }, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 0, + "id": "id80", + "index": "a3", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + "98.58579", + 0, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": { + "elementId": "id75", + "focus": 0, + "gap": 1, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 11, + "width": "98.58579", + "x": "0.70711", + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind rectangles from arrow on deletion and rebind on undo > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id75": true, + "id77": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id75" => Delta { + "deleted": { + "isDeleted": false, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id76" => Delta { + "deleted": { + "isDeleted": false, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id77" => Delta { + "deleted": { + "isDeleted": false, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map { + "id80" => Delta { + "deleted": { + "endBinding": { + "elementId": "id77", + "focus": -0, + "gap": 1, + }, + "points": [ + [ + 0, + 0, + ], + [ + 100, + 0, + ], + ], + "startBinding": { + "elementId": "id75", + "focus": 0, + "gap": 1, + }, + }, + "inserted": { + "endBinding": null, + "points": [ + [ + 0, + 0, + ], + [ + 100, + 0, + ], + ], + "startBinding": null, + }, + }, + }, + }, + }, + ], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id75" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": -100, + "y": -50, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id76" => Delta { + "deleted": { + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": null, + "containerId": null, + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 5, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a1", + "isDeleted": false, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "ola", + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "ola", + "textAlign": "left", + "type": "text", + "verticalAlign": "top", + "width": 100, + "x": -200, + "y": -200, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id77" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 100, + "y": -50, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id75": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id76": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": {}, + }, + "inserted": { + "selectedElementIds": { + "id76": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id75" => Delta { + "deleted": { + "boundElements": [ + { + "id": "id76", + "type": "text", + }, + ], + }, + "inserted": { + "boundElements": [], + }, + }, + "id76" => Delta { + "deleted": { + "containerId": "id75", + "height": 25, + "textAlign": "center", + "verticalAlign": "middle", + "width": 30, + "x": -65, + "y": "-12.50000", + }, + "inserted": { + "containerId": null, + "height": 100, + "textAlign": "left", + "verticalAlign": "top", + "width": 100, + "x": -200, + "y": -200, + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id80": true, + }, + "selectedLinearElementId": "id80", + }, + "inserted": { + "selectedElementIds": { + "id75": true, + }, + "selectedLinearElementId": null, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id80" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": { + "elementId": "id77", + "focus": -0, + "gap": 1, + }, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 0, + "index": "a3", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 100, + 0, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": { + "elementId": "id75", + "focus": 0, + "gap": 1, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "width": 100, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map { + "id75" => Delta { + "deleted": { + "boundElements": [ + { + "id": "id80", + "type": "arrow", + }, + ], + }, + "inserted": { + "boundElements": [], + }, + }, + "id77" => Delta { + "deleted": { + "boundElements": [ + { + "id": "id80", + "type": "arrow", + }, + ], + }, + "inserted": { + "boundElements": [], + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id75": true, + }, + "selectedLinearElementId": null, + }, + "inserted": { + "selectedElementIds": { + "id80": true, + }, + "selectedLinearElementId": "id80", + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id77": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind rectangles from arrow on deletion and rebind on undo > [end of test] number of elements 1`] = `4`; + +exports[`history > singleplayer undo/redo > should support bidirectional bindings > should unbind rectangles from arrow on deletion and rebind on undo > [end of test] number of renders 1`] = `15`; + +exports[`history > singleplayer undo/redo > should support changes in elements' order > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id45": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id45": true, + "id47": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should support changes in elements' order > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id46", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 3, + "width": 10, + "x": 20, + "y": 20, +} +`; + +exports[`history > singleplayer undo/redo > should support changes in elements' order > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id45", + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 7, + "width": 10, + "x": 10, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should support changes in elements' order > [end of test] element 2 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id47", + "index": "a3", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 10, + "width": 10, + "x": 40, + "y": 40, +} +`; + +exports[`history > singleplayer undo/redo > should support changes in elements' order > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id45": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id45" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id46": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id45": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id46" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 20, + "y": 20, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id47": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id46": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id47" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 40, + "y": 40, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id47" => Delta { + "deleted": { + "index": "a0V", + }, + "inserted": { + "index": "a2", + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id45": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id47": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id47": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id45" => Delta { + "deleted": { + "index": "a2", + }, + "inserted": { + "index": "Zz", + }, + }, + "id47" => Delta { + "deleted": { + "index": "a3", + }, + "inserted": { + "index": "a0", + }, + }, + }, + }, + }, + ], +} +`; + +exports[`history > singleplayer undo/redo > should support changes in elements' order > [end of test] number of elements 1`] = `3`; + +exports[`history > singleplayer undo/redo > should support changes in elements' order > [end of test] number of renders 1`] = `20`; + +exports[`history > singleplayer undo/redo > should support duplication of groups, appstate group selection and editing group > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id36": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id42": true, + "id44": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": { + "id43": true, + }, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should support duplication of groups, appstate group selection and editing group > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "A", + ], + "height": 100, + "id": "id35", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should support duplication of groups, appstate group selection and editing group > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "A", + ], + "height": 100, + "id": "id36", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "width": 100, + "x": 100, + "y": 100, +} +`; + +exports[`history > singleplayer undo/redo > should support duplication of groups, appstate group selection and editing group > [end of test] element 2 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "id43", + ], + "height": 100, + "id": "id42", + "index": "a1G", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 3, + "width": 100, + "x": 10, + "y": 10, +} +`; + +exports[`history > singleplayer undo/redo > should support duplication of groups, appstate group selection and editing group > [end of test] element 3 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "id43", + ], + "height": 100, + "id": "id44", + "index": "a1V", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 3, + "width": 100, + "x": 110, + "y": 110, +} +`; + +exports[`history > singleplayer undo/redo > should support duplication of groups, appstate group selection and editing group > [end of test] element 4 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "id40", + ], + "height": 100, + "id": "id39", + "index": "a2", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 6, + "width": 100, + "x": 10, + "y": 10, +} +`; + +exports[`history > singleplayer undo/redo > should support duplication of groups, appstate group selection and editing group > [end of test] element 5 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "id40", + ], + "height": 100, + "id": "id41", + "index": "a3", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 6, + "width": 100, + "x": 110, + "y": 110, +} +`; + +exports[`history > singleplayer undo/redo > should support duplication of groups, appstate group selection and editing group > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id35": true, + "id36": true, + }, + "selectedGroupIds": { + "A": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + "selectedGroupIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id35" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "A", + ], + "height": 100, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id36" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "A", + ], + "height": 100, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 100, + "y": 100, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id42": true, + "id44": true, + }, + "selectedGroupIds": { + "id43": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id35": true, + "id36": true, + }, + "selectedGroupIds": { + "A": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id42" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "id43", + ], + "height": 100, + "index": "a1G", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id44" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "id43", + ], + "height": 100, + "index": "a1V", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 110, + "y": 110, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > singleplayer undo/redo > should support duplication of groups, appstate group selection and editing group > [end of test] number of elements 1`] = `6`; + +exports[`history > singleplayer undo/redo > should support duplication of groups, appstate group selection and editing group > [end of test] number of renders 1`] = `18`; + +exports[`history > singleplayer undo/redo > should support element creation, deletion and appstate element selection change > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id23": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should support element creation, deletion and appstate element selection change > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id22", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 5, + "width": 10, + "x": 10, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should support element creation, deletion and appstate element selection change > [end of test] element 1 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id23", + "index": "a1", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 8, + "width": 10, + "x": 20, + "y": 20, +} +`; + +exports[`history > singleplayer undo/redo > should support element creation, deletion and appstate element selection change > [end of test] element 2 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id24", + "index": "a2", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 8, + "width": 10, + "x": 40, + "y": 40, +} +`; + +exports[`history > singleplayer undo/redo > should support element creation, deletion and appstate element selection change > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id22": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id22" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id23": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id22": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id23" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 20, + "y": 20, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id24": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id23": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id24" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 40, + "y": 40, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id23": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id24": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id24": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": {}, + }, + "inserted": { + "selectedElementIds": { + "id23": true, + "id24": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map { + "id23" => Delta { + "deleted": { + "isDeleted": true, + }, + "inserted": { + "isDeleted": false, + }, + }, + "id24" => Delta { + "deleted": { + "isDeleted": true, + }, + "inserted": { + "isDeleted": false, + }, + }, + }, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > singleplayer undo/redo > should support element creation, deletion and appstate element selection change > [end of test] number of elements 1`] = `3`; + +exports[`history > singleplayer undo/redo > should support element creation, deletion and appstate element selection change > [end of test] number of renders 1`] = `27`; + +exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id27": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": [], + "selectedElementIds": { + "id27": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id27", + "index": "a0", + "isDeleted": false, + "lastCommittedPoint": [ + 20, + 0, + ], + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 10, + 10, + ], + [ + 20, + 20, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 15, + "width": 20, + "x": 0, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id27": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id27" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "lastCommittedPoint": [ + 10, + 10, + ], + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 10, + 10, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "width": 10, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id27" => Delta { + "deleted": { + "lastCommittedPoint": [ + 20, + 0, + ], + "points": [ + [ + 0, + 0, + ], + [ + 10, + 10, + ], + [ + 20, + 0, + ], + ], + "width": 20, + }, + "inserted": { + "lastCommittedPoint": [ + 10, + 10, + ], + "points": [ + [ + 0, + 0, + ], + [ + 10, + 10, + ], + ], + "width": 10, + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedLinearElementId": "id27", + }, + "inserted": { + "selectedLinearElementId": null, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "editingLinearElementId": "id27", + }, + "inserted": { + "editingLinearElementId": null, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id27" => Delta { + "deleted": { + "height": 20, + "points": [ + [ + 0, + 0, + ], + [ + 10, + 10, + ], + [ + 20, + 20, + ], + ], + }, + "inserted": { + "height": 10, + "points": [ + [ + 0, + 0, + ], + [ + 10, + 10, + ], + [ + 20, + 0, + ], + ], + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "editingLinearElementId": null, + }, + "inserted": { + "editingLinearElementId": "id27", + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] number of elements 1`] = `1`; + +exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] number of renders 1`] = `21`; diff --git a/packages/excalidraw/tests/__snapshots__/linearElementEditor.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/linearElementEditor.test.tsx.snap new file mode 100644 index 0000000..0085798 --- /dev/null +++ b/packages/excalidraw/tests/__snapshots__/linearElementEditor.test.tsx.snap @@ -0,0 +1,57 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Test Linear Elements > Test bound text element > should bind text to arrow when clicked on arrow and enter pressed 1`] = ` +"Online whiteboard +collaboration made +easy" +`; + +exports[`Test Linear Elements > Test bound text element > should bind text to arrow when double clicked 1`] = ` +"Online whiteboard +collaboration made +easy" +`; + +exports[`Test Linear Elements > Test bound text element > should match styles for text editor 1`] = ` +<textarea + class="excalidraw-wysiwyg" + data-type="wysiwyg" + dir="auto" + style="position: absolute; display: inline-block; min-height: 1em; backface-visibility: hidden; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 26.25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(30, 30, 30); opacity: 1; filter: var(--theme-filter); max-height: 992.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Excalifont, Xiaolai, Segoe UI Emoji;" + tabindex="0" + wrap="off" +/> +`; + +exports[`Test Linear Elements > Test bound text element > should resize and position the bound text and bounding box correctly when 3 pointer arrow element resized 2`] = ` +"Online whiteboard +collaboration made +easy" +`; + +exports[`Test Linear Elements > Test bound text element > should resize and position the bound text and bounding box correctly when 3 pointer arrow element resized 6`] = ` +"Online whiteboard +collaboration made easy" +`; + +exports[`Test Linear Elements > Test bound text element > should resize and position the bound text correctly when 2 pointer linear element resized 2`] = ` +"Online whiteboard +collaboration made +easy" +`; + +exports[`Test Linear Elements > Test bound text element > should resize and position the bound text correctly when 2 pointer linear element resized 5`] = ` +"Online whiteboard +collaboration made easy" +`; + +exports[`Test Linear Elements > Test bound text element > should wrap the bound text when arrow bound container moves 1`] = ` +"Online whiteboard +collaboration made easy" +`; + +exports[`Test Linear Elements > Test bound text element > should wrap the bound text when arrow bound container moves 2`] = ` +"Online whiteboard +collaboration made +easy" +`; diff --git a/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap new file mode 100644 index 0000000..90236a4 --- /dev/null +++ b/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap @@ -0,0 +1,239 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`duplicate element on move when ALT is clicked > rectangle 5`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 50, + "id": "id2", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 238820263, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 5, + "versionNonce": 400692809, + "width": 30, + "x": 30, + "y": 20, +} +`; + +exports[`duplicate element on move when ALT is clicked > rectangle 6`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 50, + "id": "id0", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1278240551, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 6, + "versionNonce": 23633383, + "width": 30, + "x": -10, + "y": 60, +} +`; + +exports[`move element > rectangle 5`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 50, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1278240551, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "versionNonce": 1116226695, + "width": 30, + "x": 0, + "y": 40, +} +`; + +exports[`move element > rectangles with binding arrow 5`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id2", + "type": "arrow", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1278240551, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "versionNonce": 1723083209, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`move element > rectangles with binding arrow 6`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id2", + "type": "arrow", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 300, + "id": "id1", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1150084233, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 7, + "versionNonce": 745419401, + "width": 300, + "x": 201, + "y": 2, +} +`; + +exports[`move element > rectangles with binding arrow 7`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": { + "elementId": "id1", + "focus": "-0.46667", + "gap": 10, + }, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": "87.29887", + "id": "id2", + "index": "a2", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + "86.85786", + "87.29887", + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "seed": 1604849351, + "startArrowhead": null, + "startBinding": { + "elementId": "id0", + "focus": "-0.60000", + "gap": 10, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 11, + "versionNonce": 1051383431, + "width": "86.85786", + "x": "107.07107", + "y": "47.07107", +} +`; diff --git a/packages/excalidraw/tests/__snapshots__/multiPointCreate.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/multiPointCreate.test.tsx.snap new file mode 100644 index 0000000..1b312a5 --- /dev/null +++ b/packages/excalidraw/tests/__snapshots__/multiPointCreate.test.tsx.snap @@ -0,0 +1,114 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`multi point mode in linear elements > arrow 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 110, + "id": "id0", + "index": "a0", + "isDeleted": false, + "lastCommittedPoint": [ + 70, + 110, + ], + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 20, + 30, + ], + [ + 70, + 110, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "seed": 1278240551, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 8, + "versionNonce": 1604849351, + "width": 70, + "x": 30, + "y": 30, +} +`; + +exports[`multi point mode in linear elements > line 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "endArrowhead": null, + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 110, + "id": "id0", + "index": "a0", + "isDeleted": false, + "lastCommittedPoint": [ + 70, + 110, + ], + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 20, + 30, + ], + [ + 70, + 110, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "seed": 1278240551, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "line", + "updated": 1, + "version": 8, + "versionNonce": 1604849351, + "width": 70, + "x": 30, + "y": 30, +} +`; diff --git a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap new file mode 100644 index 0000000..5d48ead --- /dev/null +++ b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap @@ -0,0 +1,14782 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`given element A and group of elements B and given both are selected when user clicks on B, on pointer up only elements from B should be selected > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id0": true, + "id1": true, + "id2": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + "id2": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": { + "id5": true, + }, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`given element A and group of elements B and given both are selected when user clicks on B, on pointer up only elements from B should be selected > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 0, + "y": 30, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id2": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id2" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 0, + "y": 60, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id2": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id2": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedGroupIds": { + "id5": true, + }, + }, + "inserted": { + "selectedGroupIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "groupIds": [ + "id5", + ], + "index": "a2", + }, + "inserted": { + "groupIds": [], + "index": "a0", + }, + }, + "id2" => Delta { + "deleted": { + "groupIds": [ + "id5", + ], + "index": "a3", + }, + "inserted": { + "groupIds": [], + "index": "a2", + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": {}, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`given element A and group of elements B and given both are selected when user clicks on B, on pointer up only elements from B should be selected > [end of test] number of elements 1`] = `0`; + +exports[`given element A and group of elements B and given both are selected when user clicks on B, on pointer up only elements from B should be selected > [end of test] number of renders 1`] = `18`; + +exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id0": true, + "id1": true, + "id2": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id1": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": { + "id0": true, + "id1": true, + "id2": true, + "id4": false, + }, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 110, + "y": 110, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id2": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id2" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 100, + "x": 220, + "y": 220, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedGroupIds": { + "id4": true, + }, + }, + "inserted": { + "selectedGroupIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "groupIds": [ + "id4", + ], + "index": "a2", + }, + "inserted": { + "groupIds": [], + "index": "a0", + }, + }, + "id2" => Delta { + "deleted": { + "groupIds": [ + "id4", + ], + "index": "a3", + }, + "inserted": { + "groupIds": [], + "index": "a2", + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": {}, + "selectedGroupIds": { + "id0": true, + "id1": true, + "id2": true, + "id4": false, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + "id2": true, + }, + "selectedGroupIds": { + "id4": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected > [end of test] number of elements 1`] = `0`; + +exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected > [end of test] number of renders 1`] = `16`; + +exports[`regression tests > Cmd/Ctrl-click exclusively select element under pointer > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": "id10", + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": false, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id7": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > Cmd/Ctrl-click exclusively select element under pointer > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 30, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedGroupIds": { + "id4": true, + }, + }, + "inserted": { + "selectedGroupIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "groupIds": [ + "id4", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + "id1" => Delta { + "deleted": { + "groupIds": [ + "id4", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "editingGroupId": "id4", + "selectedElementIds": {}, + "selectedGroupIds": {}, + }, + "inserted": { + "editingGroupId": null, + "selectedElementIds": { + "id1": true, + }, + "selectedGroupIds": { + "id4": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "editingGroupId": null, + "selectedElementIds": { + "id7": true, + }, + }, + "inserted": { + "editingGroupId": "id4", + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id7" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 60, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + "id1": true, + }, + "selectedGroupIds": { + "id4": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id7": true, + }, + "selectedGroupIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id7": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedGroupIds": { + "id10": true, + }, + }, + "inserted": { + "selectedGroupIds": { + "id4": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "groupIds": [ + "id4", + "id10", + ], + }, + "inserted": { + "groupIds": [ + "id4", + ], + }, + }, + "id1" => Delta { + "deleted": { + "groupIds": [ + "id4", + "id10", + ], + }, + "inserted": { + "groupIds": [ + "id4", + ], + }, + }, + "id7" => Delta { + "deleted": { + "groupIds": [ + "id10", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "editingGroupId": "id4", + "selectedElementIds": {}, + "selectedGroupIds": {}, + }, + "inserted": { + "editingGroupId": null, + "selectedElementIds": { + "id1": true, + "id7": true, + }, + "selectedGroupIds": { + "id10": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "editingGroupId": "id10", + "selectedElementIds": { + "id7": true, + }, + }, + "inserted": { + "editingGroupId": "id4", + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > Cmd/Ctrl-click exclusively select element under pointer > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > Cmd/Ctrl-click exclusively select element under pointer > [end of test] number of renders 1`] = `26`; + +exports[`regression tests > Drags selected element when hitting only bounding box and keeps element selected > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > Drags selected element when hitting only bounding box and keeps element selected > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "ellipse", + "width": 10, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "x": 25, + "y": 25, + }, + "inserted": { + "x": 0, + "y": 0, + }, + }, + }, + }, + }, + ], +} +`; + +exports[`regression tests > Drags selected element when hitting only bounding box and keeps element selected > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > Drags selected element when hitting only bounding box and keeps element selected > [end of test] number of renders 1`] = `10`; + +exports[`regression tests > adjusts z order when grouping > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id0": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + "id2": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": { + "id5": true, + }, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > adjusts z order when grouping > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 30, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id2": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id2" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 50, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id2": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id2": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedGroupIds": { + "id5": true, + }, + }, + "inserted": { + "selectedGroupIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "groupIds": [ + "id5", + ], + "index": "a2", + }, + "inserted": { + "groupIds": [], + "index": "a0", + }, + }, + "id2" => Delta { + "deleted": { + "groupIds": [ + "id5", + ], + "index": "a3", + }, + "inserted": { + "groupIds": [], + "index": "a2", + }, + }, + }, + }, + }, + ], +} +`; + +exports[`regression tests > adjusts z order when grouping > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > adjusts z order when grouping > [end of test] number of renders 1`] = `15`; + +exports[`regression tests > alt-drag duplicates an element > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id0": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > alt-drag duplicates an element > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id2" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map { + "id0" => Delta { + "deleted": { + "index": "a1", + "x": 20, + "y": 20, + }, + "inserted": { + "index": "a0", + "x": 10, + "y": 10, + }, + }, + }, + }, + }, + ], +} +`; + +exports[`regression tests > alt-drag duplicates an element > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > alt-drag duplicates an element > [end of test] number of renders 1`] = `8`; + +exports[`regression tests > arrow keys > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > arrow keys > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > arrow keys > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > arrow keys > [end of test] number of renders 1`] = `12`; + +exports[`regression tests > can drag element that covers another element, while another elem is selected > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id2": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id1": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > can drag element that covers another element, while another elem is selected > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 200, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 200, + "x": 100, + "y": 100, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 200, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 200, + "x": 100, + "y": 100, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id2": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id2" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 350, + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "ellipse", + "width": 350, + "x": 300, + "y": 300, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id2": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id1" => Delta { + "deleted": { + "x": 300, + "y": 300, + }, + "inserted": { + "x": 100, + "y": 100, + }, + }, + }, + }, + }, + ], +} +`; + +exports[`regression tests > can drag element that covers another element, while another elem is selected > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > can drag element that covers another element, while another elem is selected > [end of test] number of renders 1`] = `15`; + +exports[`regression tests > change the properties of a shape > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "#ffc9c9", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1971c2", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": "elementStroke", + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > change the properties of a shape > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "backgroundColor": "#ffec99", + }, + "inserted": { + "backgroundColor": "transparent", + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "backgroundColor": "#ffc9c9", + }, + "inserted": { + "backgroundColor": "#ffec99", + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "strokeColor": "#1971c2", + }, + "inserted": { + "strokeColor": "#1e1e1e", + }, + }, + }, + }, + }, + ], +} +`; + +exports[`regression tests > change the properties of a shape > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > change the properties of a shape > [end of test] number of renders 1`] = `9`; + +exports[`regression tests > click on an element and drag it > [dragged] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id0": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > click on an element and drag it > [dragged] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1278240551, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 4, + "versionNonce": 1116226695, + "width": 10, + "x": 20, + "y": 20, +} +`; + +exports[`regression tests > click on an element and drag it > [dragged] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [ + [Function], + [Function], + ], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "x": 20, + "y": 20, + }, + "inserted": { + "x": 10, + "y": 10, + }, + }, + }, + }, + }, + ], +} +`; + +exports[`regression tests > click on an element and drag it > [dragged] number of elements 1`] = `1`; + +exports[`regression tests > click on an element and drag it > [dragged] number of renders 1`] = `8`; + +exports[`regression tests > click on an element and drag it > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id0": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > click on an element and drag it > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "x": 20, + "y": 20, + }, + "inserted": { + "x": 10, + "y": 10, + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "x": 10, + "y": 10, + }, + "inserted": { + "x": 20, + "y": 20, + }, + }, + }, + }, + }, + ], +} +`; + +exports[`regression tests > click on an element and drag it > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > click on an element and drag it > [end of test] number of renders 1`] = `10`; + +exports[`regression tests > click to select a shape > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id1": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > click to select a shape > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 30, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > click to select a shape > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > click to select a shape > [end of test] number of renders 1`] = `10`; + +exports[`regression tests > click-drag to select a group > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id2": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + "id1": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > click-drag to select a group > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 30, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id2": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id2" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 50, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id2": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > click-drag to select a group > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > click-drag to select a group > [end of test] number of renders 1`] = `14`; + +exports[`regression tests > deleting last but one element in editing group should unselect the group > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id1": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": { + "id4": false, + }, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > deleting last but one element in editing group should unselect the group > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 50, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedGroupIds": { + "id4": true, + }, + }, + "inserted": { + "selectedGroupIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "groupIds": [ + "id4", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + "id1" => Delta { + "deleted": { + "groupIds": [ + "id4", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "editingGroupId": "id4", + "selectedElementIds": {}, + "selectedGroupIds": {}, + }, + "inserted": { + "editingGroupId": null, + "selectedElementIds": { + "id1": true, + }, + "selectedGroupIds": { + "id4": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "editingGroupId": null, + "selectedElementIds": { + "id1": true, + }, + "selectedGroupIds": { + "id4": false, + }, + }, + "inserted": { + "editingGroupId": "id4", + "selectedElementIds": { + "id0": true, + }, + "selectedGroupIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map { + "id0" => Delta { + "deleted": { + "isDeleted": true, + }, + "inserted": { + "isDeleted": false, + }, + }, + }, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": {}, + "selectedGroupIds": {}, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + "selectedGroupIds": { + "id4": false, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + "selectedGroupIds": { + "id4": false, + }, + }, + "inserted": { + "selectedElementIds": {}, + "selectedGroupIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > deleting last but one element in editing group should unselect the group > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > deleting last but one element in editing group should unselect the group > [end of test] number of renders 1`] = `17`; + +exports[`regression tests > deselects group of selected elements on pointer down when pointer doesn't hit any element > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "down", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id0": true, + "id1": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 0, + "id": "id3", + "index": null, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "seed": 1505387817, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "selection", + "updated": 1, + "version": 1, + "versionNonce": 0, + "width": 0, + "x": 500, + "y": 500, + }, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > deselects group of selected elements on pointer down when pointer doesn't hit any element > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "ellipse", + "width": 10, + "x": 110, + "y": 110, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > deselects group of selected elements on pointer down when pointer doesn't hit any element > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > deselects group of selected elements on pointer down when pointer doesn't hit any element > [end of test] number of renders 1`] = `11`; + +exports[`regression tests > deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id0": true, + "id1": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "ellipse", + "width": 10, + "x": 110, + "y": 110, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element > [end of test] number of renders 1`] = `11`; + +exports[`regression tests > deselects selected element on pointer down when pointer doesn't hit any element > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "down", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id0": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 0, + "id": "id1", + "index": null, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "seed": 1150084233, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "selection", + "updated": 1, + "version": 1, + "versionNonce": 0, + "width": 0, + "x": 110, + "y": 110, + }, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > deselects selected element on pointer down when pointer doesn't hit any element > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > deselects selected element on pointer down when pointer doesn't hit any element > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > deselects selected element on pointer down when pointer doesn't hit any element > [end of test] number of renders 1`] = `7`; + +exports[`regression tests > deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id0": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "ellipse", + "width": 100, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": {}, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element > [end of test] number of renders 1`] = `7`; + +exports[`regression tests > double click to edit a group > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": "id3", + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id2": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > double click to edit a group > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 30, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id2": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id2" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 50, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedGroupIds": { + "id3": true, + }, + }, + "inserted": { + "selectedGroupIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "groupIds": [ + "id3", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + "id1" => Delta { + "deleted": { + "groupIds": [ + "id3", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + "id2" => Delta { + "deleted": { + "groupIds": [ + "id3", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "editingGroupId": "id3", + "selectedElementIds": {}, + "selectedGroupIds": {}, + }, + "inserted": { + "editingGroupId": null, + "selectedElementIds": { + "id0": true, + "id1": true, + }, + "selectedGroupIds": { + "id3": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > double click to edit a group > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > double click to edit a group > [end of test] number of renders 1`] = `15`; + +exports[`regression tests > drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id0": true, + "id1": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + "id1": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "ellipse", + "width": 10, + "x": 110, + "y": 110, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "x": 25, + "y": 25, + }, + "inserted": { + "x": 0, + "y": 0, + }, + }, + "id1" => Delta { + "deleted": { + "x": 135, + "y": 135, + }, + "inserted": { + "x": 110, + "y": 110, + }, + }, + }, + }, + }, + ], +} +`; + +exports[`regression tests > drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging > [end of test] number of renders 1`] = `12`; + +exports[`regression tests > draw every type of shape > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "freedraw", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > draw every type of shape > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 20, + "x": 10, + "y": -10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "diamond", + "width": 20, + "x": 40, + "y": -10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id2": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id2" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "ellipse", + "width": 20, + "x": 70, + "y": -10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id3": true, + }, + "selectedLinearElementId": "id3", + }, + "inserted": { + "selectedElementIds": { + "id2": true, + }, + "selectedLinearElementId": null, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id3" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a3", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 50, + 10, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "width": 50, + "x": 130, + "y": -10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id4": true, + }, + "selectedLinearElementId": "id4", + }, + "inserted": { + "selectedElementIds": { + "id3": true, + }, + "selectedLinearElementId": "id3", + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id4" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "endArrowhead": null, + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a4", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 50, + 10, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "line", + "width": 50, + "x": 220, + "y": -10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id5": true, + }, + "selectedLinearElementId": null, + }, + "inserted": { + "selectedElementIds": { + "id4": true, + }, + "selectedLinearElementId": "id4", + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id5" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a5", + "isDeleted": false, + "lastCommittedPoint": [ + 50, + 10, + ], + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 50, + 10, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "width": 50, + "x": 310, + "y": -10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id5" => Delta { + "deleted": { + "height": 20, + "lastCommittedPoint": [ + 80, + 20, + ], + "points": [ + [ + 0, + 0, + ], + [ + 50, + 10, + ], + [ + 80, + 20, + ], + ], + "width": 80, + }, + "inserted": { + "height": 10, + "lastCommittedPoint": [ + 50, + 10, + ], + "points": [ + [ + 0, + 0, + ], + [ + 50, + 10, + ], + ], + "width": 50, + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedLinearElementId": "id5", + }, + "inserted": { + "selectedLinearElementId": null, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id6": true, + }, + "selectedLinearElementId": null, + }, + "inserted": { + "selectedElementIds": { + "id5": true, + }, + "selectedLinearElementId": "id5", + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id6" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "endArrowhead": null, + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a6", + "isDeleted": false, + "lastCommittedPoint": [ + 50, + 10, + ], + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 50, + 10, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "line", + "width": 50, + "x": 430, + "y": -10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id6" => Delta { + "deleted": { + "height": 20, + "lastCommittedPoint": [ + 80, + 20, + ], + "points": [ + [ + 0, + 0, + ], + [ + 50, + 10, + ], + [ + 80, + 20, + ], + ], + "width": 80, + }, + "inserted": { + "height": 10, + "lastCommittedPoint": [ + 50, + 10, + ], + "points": [ + [ + 0, + 0, + ], + [ + 50, + 10, + ], + ], + "width": 50, + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedLinearElementId": "id6", + }, + "inserted": { + "selectedLinearElementId": null, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": {}, + }, + "inserted": { + "selectedElementIds": { + "id6": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedLinearElementId": null, + }, + "inserted": { + "selectedLinearElementId": "id6", + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id7" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a7", + "isDeleted": false, + "lastCommittedPoint": [ + 50, + 10, + ], + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 50, + 10, + ], + [ + 50, + 10, + ], + ], + "pressures": [ + 0, + 0, + 0, + ], + "roughness": 1, + "roundness": null, + "simulatePressure": false, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "freedraw", + "width": 50, + "x": 550, + "y": -10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > draw every type of shape > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > draw every type of shape > [end of test] number of renders 1`] = `33`; + +exports[`regression tests > given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id0": true, + "id2": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id1": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "ellipse", + "width": 100, + "x": 110, + "y": 110, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id2": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id2" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "diamond", + "width": 100, + "x": 310, + "y": 310, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + "id2": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up > [end of test] number of renders 1`] = `14`; + +exports[`regression tests > given a selected element A and a not selected element B with higher z-index than A and given B partially overlaps A when there's a shift-click on the overlapped section B is added to the selection > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "#ffc9c9", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": "elementBackground", + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id0": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + "id1": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > given a selected element A and a not selected element B with higher z-index than A and given B partially overlaps A when there's a shift-click on the overlapped section B is added to the selection > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "#ffc9c9", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 1000, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 1000, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "#ffc9c9", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 1000, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "ellipse", + "width": 1000, + "x": 500, + "y": 500, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > given a selected element A and a not selected element B with higher z-index than A and given B partially overlaps A when there's a shift-click on the overlapped section B is added to the selection > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > given a selected element A and a not selected element B with higher z-index than A and given B partially overlaps A when there's a shift-click on the overlapped section B is added to the selection > [end of test] number of renders 1`] = `12`; + +exports[`regression tests > given selected element A with lower z-index than unselected element B and given B is partially over A when clicking intersection between A and B B should be selected on pointer up > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id0": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id1": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > given selected element A with lower z-index than unselected element B and given B is partially over A when clicking intersection between A and B B should be selected on pointer up > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "red", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 1000, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 1000, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "red", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 500, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 500, + "x": 500, + "y": 500, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > given selected element A with lower z-index than unselected element B and given B is partially over A when clicking intersection between A and B B should be selected on pointer up > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > given selected element A with lower z-index than unselected element B and given B is partially over A when clicking intersection between A and B B should be selected on pointer up > [end of test] number of renders 1`] = `7`; + +exports[`regression tests > given selected element A with lower z-index than unselected element B and given B is partially over A when dragging on intersection between A and B A should be dragged and keep being selected > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id0": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > given selected element A with lower z-index than unselected element B and given B is partially over A when dragging on intersection between A and B A should be dragged and keep being selected > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "red", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 1000, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 1000, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "red", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 500, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 500, + "x": 500, + "y": 500, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "x": 100, + "y": 100, + }, + "inserted": { + "x": 0, + "y": 0, + }, + }, + }, + }, + }, + ], +} +`; + +exports[`regression tests > given selected element A with lower z-index than unselected element B and given B is partially over A when dragging on intersection between A and B A should be dragged and keep being selected > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > given selected element A with lower z-index than unselected element B and given B is partially over A when dragging on intersection between A and B A should be dragged and keep being selected > [end of test] number of renders 1`] = `8`; + +exports[`regression tests > key 2 selects rectangle tool > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > key 2 selects rectangle tool > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > key 2 selects rectangle tool > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > key 2 selects rectangle tool > [end of test] number of renders 1`] = `6`; + +exports[`regression tests > key 3 selects diamond tool > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > key 3 selects diamond tool > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "diamond", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > key 3 selects diamond tool > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > key 3 selects diamond tool > [end of test] number of renders 1`] = `6`; + +exports[`regression tests > key 4 selects ellipse tool > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > key 4 selects ellipse tool > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "ellipse", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > key 4 selects ellipse tool > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > key 4 selects ellipse tool > [end of test] number of renders 1`] = `6`; + +exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": LinearElementEditor { + "elbowed": false, + "elementId": "id0", + "endBindingElement": "keep", + "hoverPointIndex": -1, + "isDragging": false, + "lastUncommittedPoint": null, + "pointerDownState": { + "lastClickedIsEndPoint": false, + "lastClickedPoint": -1, + "origin": null, + "prevSelectedPointsIndices": null, + "segmentMidpoint": { + "added": false, + "index": null, + "value": null, + }, + }, + "pointerOffset": { + "x": 0, + "y": 0, + }, + "segmentMidPointHoveredCoords": null, + "selectedPointsIndices": null, + "startBindingElement": "keep", + }, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > key 5 selects arrow tool > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + "selectedLinearElementId": "id0", + }, + "inserted": { + "selectedElementIds": {}, + "selectedLinearElementId": null, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 10, + 10, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > key 5 selects arrow tool > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > key 5 selects arrow tool > [end of test] number of renders 1`] = `6`; + +exports[`regression tests > key 6 selects line tool > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": LinearElementEditor { + "elbowed": false, + "elementId": "id0", + "endBindingElement": "keep", + "hoverPointIndex": -1, + "isDragging": false, + "lastUncommittedPoint": null, + "pointerDownState": { + "lastClickedIsEndPoint": false, + "lastClickedPoint": -1, + "origin": null, + "prevSelectedPointsIndices": null, + "segmentMidpoint": { + "added": false, + "index": null, + "value": null, + }, + }, + "pointerOffset": { + "x": 0, + "y": 0, + }, + "segmentMidPointHoveredCoords": null, + "selectedPointsIndices": null, + "startBindingElement": "keep", + }, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > key 6 selects line tool > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + "selectedLinearElementId": "id0", + }, + "inserted": { + "selectedElementIds": {}, + "selectedLinearElementId": null, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "endArrowhead": null, + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 10, + 10, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "line", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > key 6 selects line tool > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > key 6 selects line tool > [end of test] number of renders 1`] = `6`; + +exports[`regression tests > key 7 selects freedraw tool > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "freedraw", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > key 7 selects freedraw tool > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "lastCommittedPoint": [ + 10, + 10, + ], + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 10, + 10, + ], + [ + 10, + 10, + ], + ], + "pressures": [ + 0, + 0, + 0, + ], + "roughness": 1, + "roundness": null, + "simulatePressure": false, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "freedraw", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > key 7 selects freedraw tool > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > key 7 selects freedraw tool > [end of test] number of renders 1`] = `6`; + +exports[`regression tests > key a selects arrow tool > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": LinearElementEditor { + "elbowed": false, + "elementId": "id0", + "endBindingElement": "keep", + "hoverPointIndex": -1, + "isDragging": false, + "lastUncommittedPoint": null, + "pointerDownState": { + "lastClickedIsEndPoint": false, + "lastClickedPoint": -1, + "origin": null, + "prevSelectedPointsIndices": null, + "segmentMidpoint": { + "added": false, + "index": null, + "value": null, + }, + }, + "pointerOffset": { + "x": 0, + "y": 0, + }, + "segmentMidPointHoveredCoords": null, + "selectedPointsIndices": null, + "startBindingElement": "keep", + }, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > key a selects arrow tool > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + "selectedLinearElementId": "id0", + }, + "inserted": { + "selectedElementIds": {}, + "selectedLinearElementId": null, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 10, + 10, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > key a selects arrow tool > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > key a selects arrow tool > [end of test] number of renders 1`] = `6`; + +exports[`regression tests > key d selects diamond tool > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > key d selects diamond tool > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "diamond", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > key d selects diamond tool > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > key d selects diamond tool > [end of test] number of renders 1`] = `6`; + +exports[`regression tests > key l selects line tool > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": LinearElementEditor { + "elbowed": false, + "elementId": "id0", + "endBindingElement": "keep", + "hoverPointIndex": -1, + "isDragging": false, + "lastUncommittedPoint": null, + "pointerDownState": { + "lastClickedIsEndPoint": false, + "lastClickedPoint": -1, + "origin": null, + "prevSelectedPointsIndices": null, + "segmentMidpoint": { + "added": false, + "index": null, + "value": null, + }, + }, + "pointerOffset": { + "x": 0, + "y": 0, + }, + "segmentMidPointHoveredCoords": null, + "selectedPointsIndices": null, + "startBindingElement": "keep", + }, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > key l selects line tool > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + "selectedLinearElementId": "id0", + }, + "inserted": { + "selectedElementIds": {}, + "selectedLinearElementId": null, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "endArrowhead": null, + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 10, + 10, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "line", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > key l selects line tool > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > key l selects line tool > [end of test] number of renders 1`] = `6`; + +exports[`regression tests > key o selects ellipse tool > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > key o selects ellipse tool > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "ellipse", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > key o selects ellipse tool > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > key o selects ellipse tool > [end of test] number of renders 1`] = `6`; + +exports[`regression tests > key p selects freedraw tool > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "freedraw", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > key p selects freedraw tool > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "lastCommittedPoint": [ + 10, + 10, + ], + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 10, + 10, + ], + [ + 10, + 10, + ], + ], + "pressures": [ + 0, + 0, + 0, + ], + "roughness": 1, + "roundness": null, + "simulatePressure": false, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "freedraw", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > key p selects freedraw tool > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > key p selects freedraw tool > [end of test] number of renders 1`] = `6`; + +exports[`regression tests > key r selects rectangle tool > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > key r selects rectangle tool > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > key r selects rectangle tool > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > key r selects rectangle tool > [end of test] number of renders 1`] = `6`; + +exports[`regression tests > make a group and duplicate it > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id0": true, + "id1": true, + "id2": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + "id1": true, + "id2": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": { + "id4": true, + }, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > make a group and duplicate it > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 30, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id2": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id2" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 50, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedGroupIds": { + "id4": true, + }, + }, + "inserted": { + "selectedGroupIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "groupIds": [ + "id4", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + "id1" => Delta { + "deleted": { + "groupIds": [ + "id4", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + "id2" => Delta { + "deleted": { + "groupIds": [ + "id4", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id6" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "id7", + ], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id8" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "id7", + ], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 30, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + "id9" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [ + "id7", + ], + "height": 10, + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 50, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map { + "id0" => Delta { + "deleted": { + "index": "a3", + "x": 20, + "y": 20, + }, + "inserted": { + "index": "a0", + "x": 10, + "y": 10, + }, + }, + "id1" => Delta { + "deleted": { + "index": "a4", + "x": 40, + "y": 20, + }, + "inserted": { + "index": "a1", + "x": 30, + "y": 10, + }, + }, + "id2" => Delta { + "deleted": { + "index": "a5", + "x": 60, + "y": 20, + }, + "inserted": { + "index": "a2", + "x": 50, + "y": 10, + }, + }, + }, + }, + }, + ], +} +`; + +exports[`regression tests > make a group and duplicate it > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > make a group and duplicate it > [end of test] number of renders 1`] = `17`; + +exports[`regression tests > noop interaction after undo shouldn't create history entry > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id0": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id1": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > noop interaction after undo shouldn't create history entry > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 30, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > noop interaction after undo shouldn't create history entry > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > noop interaction after undo shouldn't create history entry > [end of test] number of renders 1`] = `14`; + +exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "down", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "touch", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": "-6.25000", + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": true, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > pinch-to-zoom works > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [], +} +`; + +exports[`regression tests > pinch-to-zoom works > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > pinch-to-zoom works > [end of test] number of renders 1`] = `7`; + +exports[`regression tests > shift click on selected element should deselect it on pointer up > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id0": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > shift click on selected element should deselect it on pointer up > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": {}, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > shift click on selected element should deselect it on pointer up > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > shift click on selected element should deselect it on pointer up > [end of test] number of renders 1`] = `7`; + +exports[`regression tests > shift-click to multiselect, then drag > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id0": true, + "id1": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + "id1": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > shift-click to multiselect, then drag > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 30, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "x": 20, + "y": 20, + }, + "inserted": { + "x": 10, + "y": 10, + }, + }, + "id1" => Delta { + "deleted": { + "x": 40, + "y": 20, + }, + "inserted": { + "x": 30, + "y": 10, + }, + }, + }, + }, + }, + ], +} +`; + +exports[`regression tests > shift-click to multiselect, then drag > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > shift-click to multiselect, then drag > [end of test] number of renders 1`] = `13`; + +exports[`regression tests > should group elements and ungroup them > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id0": true, + "id1": true, + "id2": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + "id1": true, + "id2": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > should group elements and ungroup them > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 30, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id2": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id2" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 50, + "y": 10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedGroupIds": { + "id4": true, + }, + }, + "inserted": { + "selectedGroupIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "groupIds": [ + "id4", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + "id1" => Delta { + "deleted": { + "groupIds": [ + "id4", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + "id2" => Delta { + "deleted": { + "groupIds": [ + "id4", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedGroupIds": {}, + }, + "inserted": { + "selectedGroupIds": { + "id4": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "groupIds": [], + }, + "inserted": { + "groupIds": [ + "id4", + ], + }, + }, + "id1" => Delta { + "deleted": { + "groupIds": [], + }, + "inserted": { + "groupIds": [ + "id4", + ], + }, + }, + "id2" => Delta { + "deleted": { + "groupIds": [], + }, + "inserted": { + "groupIds": [ + "id4", + ], + }, + }, + }, + }, + }, + ], +} +`; + +exports[`regression tests > should group elements and ungroup them > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > should group elements and ungroup them > [end of test] number of renders 1`] = `18`; + +exports[`regression tests > single-clicking on a subgroup of a selected group should not alter selection > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id0": true, + "id1": true, + "id5": true, + "id6": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + "id1": true, + "id5": true, + "id6": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": { + "id10": true, + }, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > single-clicking on a subgroup of a selected group should not alter selection > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 50, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedGroupIds": { + "id4": true, + }, + }, + "inserted": { + "selectedGroupIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "groupIds": [ + "id4", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + "id1" => Delta { + "deleted": { + "groupIds": [ + "id4", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id5": true, + }, + "selectedGroupIds": {}, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + "id1": true, + }, + "selectedGroupIds": { + "id4": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id5" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 10, + "y": 50, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id6": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id5": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id6" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a3", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 50, + "y": 50, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id5": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id6": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id6": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedGroupIds": { + "id9": true, + }, + }, + "inserted": { + "selectedGroupIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id5" => Delta { + "deleted": { + "groupIds": [ + "id9", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + "id6" => Delta { + "deleted": { + "groupIds": [ + "id9", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + "id1": true, + }, + "selectedGroupIds": { + "id4": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + "selectedGroupIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedGroupIds": { + "id10": true, + }, + }, + "inserted": { + "selectedGroupIds": { + "id4": true, + "id9": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "groupIds": [ + "id4", + "id10", + ], + }, + "inserted": { + "groupIds": [ + "id4", + ], + }, + }, + "id1" => Delta { + "deleted": { + "groupIds": [ + "id4", + "id10", + ], + }, + "inserted": { + "groupIds": [ + "id4", + ], + }, + }, + "id5" => Delta { + "deleted": { + "groupIds": [ + "id9", + "id10", + ], + }, + "inserted": { + "groupIds": [ + "id9", + ], + }, + }, + "id6" => Delta { + "deleted": { + "groupIds": [ + "id9", + "id10", + ], + }, + "inserted": { + "groupIds": [ + "id9", + ], + }, + }, + }, + }, + }, + ], +} +`; + +exports[`regression tests > single-clicking on a subgroup of a selected group should not alter selection > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > single-clicking on a subgroup of a selected group should not alter selection > [end of test] number of renders 1`] = `25`; + +exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 60, + "scrollY": 60, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [], +} +`; + +exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] number of renders 1`] = `5`; + +exports[`regression tests > supports nested groups > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": "id3", + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id0": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id1": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > supports nested groups > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 50, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 50, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 50, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 50, + "x": 100, + "y": 100, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id2": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id2" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 50, + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 50, + "x": 200, + "y": 200, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedGroupIds": { + "id3": true, + }, + }, + "inserted": { + "selectedGroupIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "groupIds": [ + "id3", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + "id1" => Delta { + "deleted": { + "groupIds": [ + "id3", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + "id2" => Delta { + "deleted": { + "groupIds": [ + "id3", + ], + }, + "inserted": { + "groupIds": [], + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "editingGroupId": "id3", + "selectedElementIds": {}, + "selectedGroupIds": {}, + }, + "inserted": { + "editingGroupId": null, + "selectedElementIds": { + "id0": true, + "id1": true, + }, + "selectedGroupIds": { + "id3": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedGroupIds": { + "id5": true, + }, + }, + "inserted": { + "selectedGroupIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id0" => Delta { + "deleted": { + "groupIds": [ + "id5", + "id3", + ], + "index": "a2", + }, + "inserted": { + "groupIds": [ + "id3", + ], + "index": "a0", + }, + }, + "id2" => Delta { + "deleted": { + "groupIds": [ + "id5", + "id3", + ], + "index": "a3", + }, + "inserted": { + "groupIds": [ + "id3", + ], + "index": "a2", + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "editingGroupId": null, + "selectedElementIds": { + "id1": true, + }, + "selectedGroupIds": { + "id3": true, + }, + }, + "inserted": { + "editingGroupId": "id3", + "selectedElementIds": {}, + "selectedGroupIds": { + "id5": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "editingGroupId": "id3", + "selectedElementIds": {}, + "selectedGroupIds": { + "id5": true, + }, + }, + "inserted": { + "editingGroupId": null, + "selectedElementIds": { + "id1": true, + }, + "selectedGroupIds": { + "id3": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "editingGroupId": "id5", + "selectedElementIds": {}, + "selectedGroupIds": {}, + }, + "inserted": { + "editingGroupId": "id3", + "selectedElementIds": { + "id2": true, + }, + "selectedGroupIds": { + "id5": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "editingGroupId": null, + "selectedElementIds": { + "id1": true, + "id2": true, + }, + "selectedGroupIds": { + "id3": true, + }, + }, + "inserted": { + "editingGroupId": "id5", + "selectedElementIds": {}, + "selectedGroupIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "editingGroupId": "id3", + "selectedElementIds": {}, + "selectedGroupIds": {}, + }, + "inserted": { + "editingGroupId": null, + "selectedElementIds": { + "id0": true, + "id2": true, + }, + "selectedGroupIds": { + "id3": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > supports nested groups > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > supports nested groups > [end of test] number of renders 1`] = `23`; + +exports[`regression tests > switches from group of selected elements to another element on pointer down > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "down", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id1": true, + "id2": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 0, + "id": "id4", + "index": null, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "seed": 1723083209, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "selection", + "updated": 1, + "version": 1, + "versionNonce": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > switches from group of selected elements to another element on pointer down > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "ellipse", + "width": 100, + "x": 110, + "y": 110, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id2": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id1": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id2" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "diamond", + "width": 100, + "x": 310, + "y": 310, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > switches from group of selected elements to another element on pointer down > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > switches from group of selected elements to another element on pointer down > [end of test] number of renders 1`] = `14`; + +exports[`regression tests > switches selected element on pointer down > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "down", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": { + "id1": true, + }, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 0, + "id": "id2", + "index": null, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "seed": 1604849351, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "selection", + "updated": 1, + "version": 1, + "versionNonce": 0, + "width": 0, + "x": 0, + "y": 0, + }, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > switches selected element on pointer down > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 10, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "ellipse", + "width": 10, + "x": 20, + "y": 20, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > switches selected element on pointer down > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > switches selected element on pointer down > [end of test] number of renders 1`] = `10`; + +exports[`regression tests > two-finger scroll works > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "down", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "touch", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 20, + "scrollY": "-18.53553", + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": true, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > two-finger scroll works > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [], +} +`; + +exports[`regression tests > two-finger scroll works > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > two-finger scroll works > [end of test] number of renders 1`] = `8`; + +exports[`regression tests > undo/redo drawing an element > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": { + "id1": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > undo/redo drawing an element > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedLinearElementId": null, + }, + "inserted": { + "selectedLinearElementId": "id2", + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map {}, + "updated": Map { + "id2" => Delta { + "deleted": { + "height": 10, + "lastCommittedPoint": [ + 60, + 10, + ], + "points": [ + [ + 0, + 0, + ], + [ + 60, + 10, + ], + ], + "width": 60, + }, + "inserted": { + "height": 20, + "lastCommittedPoint": [ + 100, + 20, + ], + "points": [ + [ + 0, + 0, + ], + [ + 60, + 10, + ], + [ + 100, + 20, + ], + ], + "width": 100, + }, + }, + }, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id2": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map { + "id2" => Delta { + "deleted": { + "isDeleted": true, + }, + "inserted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a2", + "isDeleted": false, + "lastCommittedPoint": [ + 60, + 10, + ], + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 60, + 10, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "width": 60, + "x": 130, + "y": 10, + }, + }, + }, + "removed": Map {}, + "updated": Map {}, + }, + }, + ], + "undoStack": [ + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id0" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 10, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 20, + "x": 10, + "y": -10, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + HistoryEntry { + "appStateChange": AppStateChange { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id1": true, + }, + }, + "inserted": { + "selectedElementIds": { + "id0": true, + }, + }, + }, + }, + "elementsChange": ElementsChange { + "added": Map {}, + "removed": Map { + "id1" => Delta { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 20, + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "width": 30, + "x": 40, + "y": 0, + }, + "inserted": { + "isDeleted": true, + }, + }, + }, + "updated": Map {}, + }, + }, + ], +} +`; + +exports[`regression tests > undo/redo drawing an element > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > undo/redo drawing an element > [end of test] number of renders 1`] = `20`; + +exports[`regression tests > updates fontSize & fontFamily appState > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "text", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 8, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > updates fontSize & fontFamily appState > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [], +} +`; + +exports[`regression tests > updates fontSize & fontFamily appState > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > updates fontSize & fontFamily appState > [end of test] number of renders 1`] = `5`; + +exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 768, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "searchMatches": [], + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > zoom hotkeys > [end of test] history 1`] = ` +History { + "onHistoryChangedEmitter": Emitter { + "subscribers": [], + }, + "redoStack": [], + "undoStack": [], +} +`; + +exports[`regression tests > zoom hotkeys > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > zoom hotkeys > [end of test] number of renders 1`] = `6`; diff --git a/packages/excalidraw/tests/__snapshots__/selection.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/selection.test.tsx.snap new file mode 100644 index 0000000..2eea908 --- /dev/null +++ b/packages/excalidraw/tests/__snapshots__/selection.test.tsx.snap @@ -0,0 +1,202 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`select single element on the scene > arrow 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "elbowed": false, + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 50, + "id": "id0", + "index": "a0", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 30, + 50, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "seed": 1278240551, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 4, + "versionNonce": 2019559783, + "width": 30, + "x": 10, + "y": 10, +} +`; + +exports[`select single element on the scene > arrow escape 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "endArrowhead": null, + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 50, + "id": "id0", + "index": "a0", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 30, + 50, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "seed": 1278240551, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "line", + "updated": 1, + "version": 4, + "versionNonce": 2019559783, + "width": 30, + "x": 10, + "y": 10, +} +`; + +exports[`select single element on the scene > diamond 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 50, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "seed": 1278240551, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "diamond", + "updated": 1, + "version": 3, + "versionNonce": 401146281, + "width": 30, + "x": 10, + "y": 10, +} +`; + +exports[`select single element on the scene > ellipse 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 50, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 2, + }, + "seed": 1278240551, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "ellipse", + "updated": 1, + "version": 3, + "versionNonce": 401146281, + "width": 30, + "x": 10, + "y": 10, +} +`; + +exports[`select single element on the scene > rectangle 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 50, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1278240551, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 3, + "versionNonce": 401146281, + "width": 30, + "x": 10, + "y": 10, +} +`; diff --git a/packages/excalidraw/tests/actionStyles.test.tsx b/packages/excalidraw/tests/actionStyles.test.tsx new file mode 100644 index 0000000..abe4a7c --- /dev/null +++ b/packages/excalidraw/tests/actionStyles.test.tsx @@ -0,0 +1,84 @@ +import React from "react"; +import { Excalidraw } from "../index"; +import { CODES } from "../keys"; +import { API } from "../tests/helpers/api"; +import { Keyboard, Pointer, UI } from "../tests/helpers/ui"; +import { + act, + fireEvent, + render, + screen, + togglePopover, +} from "../tests/test-utils"; +import { copiedStyles } from "../actions/actionStyles"; + +const { h } = window; + +const mouse = new Pointer("mouse"); + +describe("actionStyles", () => { + beforeEach(async () => { + await render(<Excalidraw handleKeyboardGlobally={true} />); + }); + + afterEach(async () => { + // https://github.com/floating-ui/floating-ui/issues/1908#issuecomment-1301553793 + // affects node v16+ + await act(async () => {}); + }); + + it("should copy & paste styles via keyboard", async () => { + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(20, 20); + + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(20, 20); + + // Change some styles of second rectangle + togglePopover("Stroke"); + UI.clickOnTestId("color-red"); + togglePopover("Background"); + UI.clickOnTestId("color-blue"); + // Fill style + fireEvent.click(screen.getByTitle("Cross-hatch")); + // Stroke width + fireEvent.click(screen.getByTitle("Bold")); + // Stroke style + fireEvent.click(screen.getByTitle("Dotted")); + // Roughness + fireEvent.click(screen.getByTitle("Cartoonist")); + // Opacity + fireEvent.change(screen.getByTestId("opacity"), { + target: { value: "60" }, + }); + + mouse.reset(); + + API.setSelectedElements([h.elements[1]]); + + Keyboard.withModifierKeys({ ctrl: true, alt: true }, () => { + Keyboard.codeDown(CODES.C); + }); + const secondRect = JSON.parse(copiedStyles)[0]; + expect(secondRect.id).toBe(h.elements[1].id); + + mouse.reset(); + // Paste styles to first rectangle + API.setSelectedElements([h.elements[0]]); + Keyboard.withModifierKeys({ ctrl: true, alt: true }, () => { + Keyboard.codeDown(CODES.V); + }); + + const firstRect = API.getSelectedElement(); + expect(firstRect.id).toBe(h.elements[0].id); + expect(firstRect.strokeColor).toBe("#e03131"); + expect(firstRect.backgroundColor).toBe("#a5d8ff"); + expect(firstRect.fillStyle).toBe("cross-hatch"); + expect(firstRect.strokeWidth).toBe(2); // Bold: 2 + expect(firstRect.strokeStyle).toBe("dotted"); + expect(firstRect.roughness).toBe(2); // Cartoonist: 2 + expect(firstRect.opacity).toBe(60); + }); +}); diff --git a/packages/excalidraw/tests/align.test.tsx b/packages/excalidraw/tests/align.test.tsx new file mode 100644 index 0000000..47624e8 --- /dev/null +++ b/packages/excalidraw/tests/align.test.tsx @@ -0,0 +1,579 @@ +import React from "react"; +import { act, unmountComponent, render } from "./test-utils"; +import { Excalidraw } from "../index"; +import { defaultLang, setLanguage } from "../i18n"; +import { UI, Pointer, Keyboard } from "./helpers/ui"; +import { API } from "./helpers/api"; +import { KEYS } from "../keys"; +import { + actionAlignVerticallyCentered, + actionAlignHorizontallyCentered, + actionGroup, + actionAlignTop, + actionAlignBottom, + actionAlignLeft, + actionAlignRight, +} from "../actions"; + +const mouse = new Pointer("mouse"); + +const createAndSelectTwoRectangles = () => { + UI.clickTool("rectangle"); + mouse.down(); + mouse.up(100, 100); + + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(100, 100); + + // Select the first element. + // The second rectangle is already reselected because it was the last element created + mouse.reset(); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.click(); + }); +}; + +const createAndSelectTwoRectanglesWithDifferentSizes = () => { + UI.clickTool("rectangle"); + mouse.down(); + mouse.up(100, 100); + + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(110, 110); + + // Select the first element. + // The second rectangle is already reselected because it was the last element created + mouse.reset(); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.click(); + }); +}; + +describe("aligning", () => { + beforeEach(async () => { + unmountComponent(); + mouse.reset(); + + await act(() => { + return setLanguage(defaultLang); + }); + await render(<Excalidraw handleKeyboardGlobally={true} />); + }); + + it("aligns two objects correctly to the top", () => { + createAndSelectTwoRectangles(); + + expect(API.getSelectedElements()[0].x).toEqual(0); + expect(API.getSelectedElements()[1].x).toEqual(110); + + expect(API.getSelectedElements()[0].y).toEqual(0); + expect(API.getSelectedElements()[1].y).toEqual(110); + + Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => { + Keyboard.keyPress(KEYS.ARROW_UP); + }); + + // Check if x position did not change + expect(API.getSelectedElements()[0].x).toEqual(0); + expect(API.getSelectedElements()[1].x).toEqual(110); + + expect(API.getSelectedElements()[0].y).toEqual(0); + expect(API.getSelectedElements()[1].y).toEqual(0); + }); + + it("aligns two objects correctly to the bottom", () => { + createAndSelectTwoRectangles(); + + expect(API.getSelectedElements()[0].x).toEqual(0); + expect(API.getSelectedElements()[1].x).toEqual(110); + + expect(API.getSelectedElements()[0].y).toEqual(0); + expect(API.getSelectedElements()[1].y).toEqual(110); + + Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => { + Keyboard.keyPress(KEYS.ARROW_DOWN); + }); + + // Check if x position did not change + expect(API.getSelectedElements()[0].x).toEqual(0); + expect(API.getSelectedElements()[1].x).toEqual(110); + + expect(API.getSelectedElements()[0].y).toEqual(110); + expect(API.getSelectedElements()[1].y).toEqual(110); + }); + + it("aligns two objects correctly to the left", () => { + createAndSelectTwoRectangles(); + + expect(API.getSelectedElements()[0].x).toEqual(0); + expect(API.getSelectedElements()[1].x).toEqual(110); + + expect(API.getSelectedElements()[0].y).toEqual(0); + expect(API.getSelectedElements()[1].y).toEqual(110); + + Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => { + Keyboard.keyPress(KEYS.ARROW_LEFT); + }); + + expect(API.getSelectedElements()[0].x).toEqual(0); + expect(API.getSelectedElements()[1].x).toEqual(0); + + // Check if y position did not change + expect(API.getSelectedElements()[0].y).toEqual(0); + expect(API.getSelectedElements()[1].y).toEqual(110); + }); + + it("aligns two objects correctly to the right", () => { + createAndSelectTwoRectangles(); + + expect(API.getSelectedElements()[0].x).toEqual(0); + expect(API.getSelectedElements()[1].x).toEqual(110); + + expect(API.getSelectedElements()[0].y).toEqual(0); + expect(API.getSelectedElements()[1].y).toEqual(110); + + Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => { + Keyboard.keyPress(KEYS.ARROW_RIGHT); + }); + + expect(API.getSelectedElements()[0].x).toEqual(110); + expect(API.getSelectedElements()[1].x).toEqual(110); + + // Check if y position did not change + expect(API.getSelectedElements()[0].y).toEqual(0); + expect(API.getSelectedElements()[1].y).toEqual(110); + }); + + it("centers two objects with different sizes correctly vertically", () => { + createAndSelectTwoRectanglesWithDifferentSizes(); + + expect(API.getSelectedElements()[0].x).toEqual(0); + expect(API.getSelectedElements()[1].x).toEqual(110); + + expect(API.getSelectedElements()[0].y).toEqual(0); + expect(API.getSelectedElements()[1].y).toEqual(110); + + API.executeAction(actionAlignVerticallyCentered); + + // Check if x position did not change + expect(API.getSelectedElements()[0].x).toEqual(0); + expect(API.getSelectedElements()[1].x).toEqual(110); + + expect(API.getSelectedElements()[0].y).toEqual(60); + expect(API.getSelectedElements()[1].y).toEqual(55); + }); + + it("centers two objects with different sizes correctly horizontally", () => { + createAndSelectTwoRectanglesWithDifferentSizes(); + + expect(API.getSelectedElements()[0].x).toEqual(0); + expect(API.getSelectedElements()[1].x).toEqual(110); + + expect(API.getSelectedElements()[0].y).toEqual(0); + expect(API.getSelectedElements()[1].y).toEqual(110); + + API.executeAction(actionAlignHorizontallyCentered); + + expect(API.getSelectedElements()[0].x).toEqual(60); + expect(API.getSelectedElements()[1].x).toEqual(55); + + // Check if y position did not change + expect(API.getSelectedElements()[0].y).toEqual(0); + expect(API.getSelectedElements()[1].y).toEqual(110); + }); + + const createAndSelectGroupAndRectangle = () => { + UI.clickTool("rectangle"); + mouse.down(); + mouse.up(100, 100); + + UI.clickTool("rectangle"); + mouse.down(0, 0); + mouse.up(100, 100); + + // Select the first element. + // The second rectangle is already reselected because it was the last element created + mouse.reset(); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.click(); + }); + + API.executeAction(actionGroup); + + mouse.reset(); + UI.clickTool("rectangle"); + mouse.down(200, 200); + mouse.up(100, 100); + + // Add the created group to the current selection + mouse.restorePosition(0, 0); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.click(); + }); + }; + + it("aligns a group with another element correctly to the top", () => { + createAndSelectGroupAndRectangle(); + + expect(API.getSelectedElements()[0].y).toEqual(0); + expect(API.getSelectedElements()[1].y).toEqual(100); + expect(API.getSelectedElements()[2].y).toEqual(200); + + API.executeAction(actionAlignTop); + + expect(API.getSelectedElements()[0].y).toEqual(0); + expect(API.getSelectedElements()[1].y).toEqual(100); + expect(API.getSelectedElements()[2].y).toEqual(0); + }); + + it("aligns a group with another element correctly to the bottom", () => { + createAndSelectGroupAndRectangle(); + + expect(API.getSelectedElements()[0].y).toEqual(0); + expect(API.getSelectedElements()[1].y).toEqual(100); + expect(API.getSelectedElements()[2].y).toEqual(200); + + API.executeAction(actionAlignBottom); + + expect(API.getSelectedElements()[0].y).toEqual(100); + expect(API.getSelectedElements()[1].y).toEqual(200); + expect(API.getSelectedElements()[2].y).toEqual(200); + }); + + it("aligns a group with another element correctly to the left", () => { + createAndSelectGroupAndRectangle(); + + expect(API.getSelectedElements()[0].x).toEqual(0); + expect(API.getSelectedElements()[1].x).toEqual(100); + expect(API.getSelectedElements()[2].x).toEqual(200); + + API.executeAction(actionAlignLeft); + + expect(API.getSelectedElements()[0].x).toEqual(0); + expect(API.getSelectedElements()[1].x).toEqual(100); + expect(API.getSelectedElements()[2].x).toEqual(0); + }); + + it("aligns a group with another element correctly to the right", () => { + createAndSelectGroupAndRectangle(); + + expect(API.getSelectedElements()[0].x).toEqual(0); + expect(API.getSelectedElements()[1].x).toEqual(100); + expect(API.getSelectedElements()[2].x).toEqual(200); + + API.executeAction(actionAlignRight); + + expect(API.getSelectedElements()[0].x).toEqual(100); + expect(API.getSelectedElements()[1].x).toEqual(200); + expect(API.getSelectedElements()[2].x).toEqual(200); + }); + + it("centers a group with another element correctly vertically", () => { + createAndSelectGroupAndRectangle(); + + expect(API.getSelectedElements()[0].y).toEqual(0); + expect(API.getSelectedElements()[1].y).toEqual(100); + expect(API.getSelectedElements()[2].y).toEqual(200); + + API.executeAction(actionAlignVerticallyCentered); + + expect(API.getSelectedElements()[0].y).toEqual(50); + expect(API.getSelectedElements()[1].y).toEqual(150); + expect(API.getSelectedElements()[2].y).toEqual(100); + }); + + it("centers a group with another element correctly horizontally", () => { + createAndSelectGroupAndRectangle(); + + expect(API.getSelectedElements()[0].x).toEqual(0); + expect(API.getSelectedElements()[1].x).toEqual(100); + expect(API.getSelectedElements()[2].x).toEqual(200); + + API.executeAction(actionAlignHorizontallyCentered); + + expect(API.getSelectedElements()[0].x).toEqual(50); + expect(API.getSelectedElements()[1].x).toEqual(150); + expect(API.getSelectedElements()[2].x).toEqual(100); + }); + + const createAndSelectTwoGroups = () => { + UI.clickTool("rectangle"); + mouse.down(); + mouse.up(100, 100); + + UI.clickTool("rectangle"); + mouse.down(0, 0); + mouse.up(100, 100); + + // Select the first element. + // The second rectangle is already selected because it was the last element created + mouse.reset(); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.click(); + }); + + API.executeAction(actionGroup); + + mouse.reset(); + UI.clickTool("rectangle"); + mouse.down(200, 200); + mouse.up(100, 100); + + UI.clickTool("rectangle"); + mouse.down(); + mouse.up(100, 100); + + mouse.restorePosition(200, 200); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.click(); + }); + + API.executeAction(actionGroup); + + // Select the first group. + // The second group is already selected because it was the last group created + mouse.reset(); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.click(); + }); + }; + + it("aligns two groups correctly to the top", () => { + createAndSelectTwoGroups(); + + expect(API.getSelectedElements()[0].y).toEqual(0); + expect(API.getSelectedElements()[1].y).toEqual(100); + expect(API.getSelectedElements()[2].y).toEqual(200); + expect(API.getSelectedElements()[3].y).toEqual(300); + + API.executeAction(actionAlignTop); + + expect(API.getSelectedElements()[0].y).toEqual(0); + expect(API.getSelectedElements()[1].y).toEqual(100); + expect(API.getSelectedElements()[2].y).toEqual(0); + expect(API.getSelectedElements()[3].y).toEqual(100); + }); + + it("aligns two groups correctly to the bottom", () => { + createAndSelectTwoGroups(); + + expect(API.getSelectedElements()[0].y).toEqual(0); + expect(API.getSelectedElements()[1].y).toEqual(100); + expect(API.getSelectedElements()[2].y).toEqual(200); + expect(API.getSelectedElements()[3].y).toEqual(300); + + API.executeAction(actionAlignBottom); + + expect(API.getSelectedElements()[0].y).toEqual(200); + expect(API.getSelectedElements()[1].y).toEqual(300); + expect(API.getSelectedElements()[2].y).toEqual(200); + expect(API.getSelectedElements()[3].y).toEqual(300); + }); + + it("aligns two groups correctly to the left", () => { + createAndSelectTwoGroups(); + + expect(API.getSelectedElements()[0].x).toEqual(0); + expect(API.getSelectedElements()[1].x).toEqual(100); + expect(API.getSelectedElements()[2].x).toEqual(200); + expect(API.getSelectedElements()[3].x).toEqual(300); + + API.executeAction(actionAlignLeft); + + expect(API.getSelectedElements()[0].x).toEqual(0); + expect(API.getSelectedElements()[1].x).toEqual(100); + expect(API.getSelectedElements()[2].x).toEqual(0); + expect(API.getSelectedElements()[3].x).toEqual(100); + }); + + it("aligns two groups correctly to the right", () => { + createAndSelectTwoGroups(); + + expect(API.getSelectedElements()[0].x).toEqual(0); + expect(API.getSelectedElements()[1].x).toEqual(100); + expect(API.getSelectedElements()[2].x).toEqual(200); + expect(API.getSelectedElements()[3].x).toEqual(300); + + API.executeAction(actionAlignRight); + + expect(API.getSelectedElements()[0].x).toEqual(200); + expect(API.getSelectedElements()[1].x).toEqual(300); + expect(API.getSelectedElements()[2].x).toEqual(200); + expect(API.getSelectedElements()[3].x).toEqual(300); + }); + + it("centers two groups correctly vertically", () => { + createAndSelectTwoGroups(); + + expect(API.getSelectedElements()[0].y).toEqual(0); + expect(API.getSelectedElements()[1].y).toEqual(100); + expect(API.getSelectedElements()[2].y).toEqual(200); + expect(API.getSelectedElements()[3].y).toEqual(300); + + API.executeAction(actionAlignVerticallyCentered); + + expect(API.getSelectedElements()[0].y).toEqual(100); + expect(API.getSelectedElements()[1].y).toEqual(200); + expect(API.getSelectedElements()[2].y).toEqual(100); + expect(API.getSelectedElements()[3].y).toEqual(200); + }); + + it("centers two groups correctly horizontally", () => { + createAndSelectTwoGroups(); + + expect(API.getSelectedElements()[0].x).toEqual(0); + expect(API.getSelectedElements()[1].x).toEqual(100); + expect(API.getSelectedElements()[2].x).toEqual(200); + expect(API.getSelectedElements()[3].x).toEqual(300); + + API.executeAction(actionAlignHorizontallyCentered); + + expect(API.getSelectedElements()[0].x).toEqual(100); + expect(API.getSelectedElements()[1].x).toEqual(200); + expect(API.getSelectedElements()[2].x).toEqual(100); + expect(API.getSelectedElements()[3].x).toEqual(200); + }); + + const createAndSelectNestedGroupAndRectangle = () => { + UI.clickTool("rectangle"); + mouse.down(); + mouse.up(100, 100); + + UI.clickTool("rectangle"); + mouse.down(0, 0); + mouse.up(100, 100); + + // Select the first element. + // The second rectangle is already reselected because it was the last element created + mouse.reset(); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.click(); + }); + + // Create first group of rectangles + API.executeAction(actionGroup); + + mouse.reset(); + UI.clickTool("rectangle"); + mouse.down(200, 200); + mouse.up(100, 100); + + // Add group to current selection + mouse.restorePosition(0, 0); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.click(); + }); + + // Create the nested group + API.executeAction(actionGroup); + + mouse.reset(); + UI.clickTool("rectangle"); + mouse.down(300, 300); + mouse.up(100, 100); + + // Select the nested group, the rectangle is already selected + mouse.reset(); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.click(); + }); + }; + + it("aligns nested group and other element correctly to the top", () => { + createAndSelectNestedGroupAndRectangle(); + + expect(API.getSelectedElements()[0].y).toEqual(0); + expect(API.getSelectedElements()[1].y).toEqual(100); + expect(API.getSelectedElements()[2].y).toEqual(200); + expect(API.getSelectedElements()[3].y).toEqual(300); + + API.executeAction(actionAlignTop); + + expect(API.getSelectedElements()[0].y).toEqual(0); + expect(API.getSelectedElements()[1].y).toEqual(100); + expect(API.getSelectedElements()[2].y).toEqual(200); + expect(API.getSelectedElements()[3].y).toEqual(0); + }); + + it("aligns nested group and other element correctly to the bottom", () => { + createAndSelectNestedGroupAndRectangle(); + + expect(API.getSelectedElements()[0].y).toEqual(0); + expect(API.getSelectedElements()[1].y).toEqual(100); + expect(API.getSelectedElements()[2].y).toEqual(200); + expect(API.getSelectedElements()[3].y).toEqual(300); + + API.executeAction(actionAlignBottom); + + expect(API.getSelectedElements()[0].y).toEqual(100); + expect(API.getSelectedElements()[1].y).toEqual(200); + expect(API.getSelectedElements()[2].y).toEqual(300); + expect(API.getSelectedElements()[3].y).toEqual(300); + }); + + it("aligns nested group and other element correctly to the left", () => { + createAndSelectNestedGroupAndRectangle(); + + expect(API.getSelectedElements()[0].x).toEqual(0); + expect(API.getSelectedElements()[1].x).toEqual(100); + expect(API.getSelectedElements()[2].x).toEqual(200); + expect(API.getSelectedElements()[3].x).toEqual(300); + + API.executeAction(actionAlignLeft); + + expect(API.getSelectedElements()[0].x).toEqual(0); + expect(API.getSelectedElements()[1].x).toEqual(100); + expect(API.getSelectedElements()[2].x).toEqual(200); + expect(API.getSelectedElements()[3].x).toEqual(0); + }); + + it("aligns nested group and other element correctly to the right", () => { + createAndSelectNestedGroupAndRectangle(); + + expect(API.getSelectedElements()[0].x).toEqual(0); + expect(API.getSelectedElements()[1].x).toEqual(100); + expect(API.getSelectedElements()[2].x).toEqual(200); + expect(API.getSelectedElements()[3].x).toEqual(300); + + API.executeAction(actionAlignRight); + + expect(API.getSelectedElements()[0].x).toEqual(100); + expect(API.getSelectedElements()[1].x).toEqual(200); + expect(API.getSelectedElements()[2].x).toEqual(300); + expect(API.getSelectedElements()[3].x).toEqual(300); + }); + + it("centers nested group and other element correctly vertically", () => { + createAndSelectNestedGroupAndRectangle(); + + expect(API.getSelectedElements()[0].y).toEqual(0); + expect(API.getSelectedElements()[1].y).toEqual(100); + expect(API.getSelectedElements()[2].y).toEqual(200); + expect(API.getSelectedElements()[3].y).toEqual(300); + + API.executeAction(actionAlignVerticallyCentered); + + expect(API.getSelectedElements()[0].y).toEqual(50); + expect(API.getSelectedElements()[1].y).toEqual(150); + expect(API.getSelectedElements()[2].y).toEqual(250); + expect(API.getSelectedElements()[3].y).toEqual(150); + }); + + it("centers nested group and other element correctly horizontally", () => { + createAndSelectNestedGroupAndRectangle(); + + expect(API.getSelectedElements()[0].x).toEqual(0); + expect(API.getSelectedElements()[1].x).toEqual(100); + expect(API.getSelectedElements()[2].x).toEqual(200); + expect(API.getSelectedElements()[3].x).toEqual(300); + + API.executeAction(actionAlignHorizontallyCentered); + + expect(API.getSelectedElements()[0].x).toEqual(50); + expect(API.getSelectedElements()[1].x).toEqual(150); + expect(API.getSelectedElements()[2].x).toEqual(250); + expect(API.getSelectedElements()[3].x).toEqual(150); + }); +}); diff --git a/packages/excalidraw/tests/appState.test.tsx b/packages/excalidraw/tests/appState.test.tsx new file mode 100644 index 0000000..4a937cd --- /dev/null +++ b/packages/excalidraw/tests/appState.test.tsx @@ -0,0 +1,81 @@ +import React from "react"; +import { fireEvent, queryByTestId, render, waitFor } from "./test-utils"; +import { Excalidraw } from "../index"; +import { API } from "./helpers/api"; +import { getDefaultAppState } from "../appState"; +import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants"; +import { Pointer, UI } from "./helpers/ui"; +import type { ExcalidrawTextElement } from "../element/types"; + +const { h } = window; + +describe("appState", () => { + it("drag&drop file doesn't reset non-persisted appState", async () => { + const defaultAppState = getDefaultAppState(); + const exportBackground = !defaultAppState.exportBackground; + + await render( + <Excalidraw + initialData={{ + appState: { + exportBackground, + viewBackgroundColor: "#F00", + }, + }} + />, + {}, + ); + + await waitFor(() => { + expect(h.state.exportBackground).toBe(exportBackground); + expect(h.state.viewBackgroundColor).toBe("#F00"); + }); + + await API.drop( + new Blob( + [ + JSON.stringify({ + type: EXPORT_DATA_TYPES.excalidraw, + appState: { + viewBackgroundColor: "#000", + }, + elements: [API.createElement({ type: "rectangle", id: "A" })], + }), + ], + { type: MIME_TYPES.json }, + ), + ); + + await waitFor(() => { + expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]); + // non-imported prop → retain + expect(h.state.exportBackground).toBe(exportBackground); + // imported prop → overwrite + expect(h.state.viewBackgroundColor).toBe("#000"); + }); + }); + + it("changing fontSize with text tool selected (no element created yet)", async () => { + const { container } = await render( + <Excalidraw + initialData={{ + appState: { + currentItemFontSize: 30, + }, + }} + />, + ); + + UI.clickTool("text"); + + expect(h.state.currentItemFontSize).toBe(30); + fireEvent.click(queryByTestId(container, "fontSize-small")!); + expect(h.state.currentItemFontSize).toBe(16); + + const mouse = new Pointer("mouse"); + + mouse.clickAt(100, 100); + + expect((h.elements[0] as ExcalidrawTextElement).fontSize).toBe(16); + }); +}); diff --git a/packages/excalidraw/tests/binding.test.tsx b/packages/excalidraw/tests/binding.test.tsx new file mode 100644 index 0000000..905a44e --- /dev/null +++ b/packages/excalidraw/tests/binding.test.tsx @@ -0,0 +1,481 @@ +import React from "react"; +import { fireEvent, render } from "./test-utils"; +import { Excalidraw, isLinearElement } from "../index"; +import { UI, Pointer, Keyboard } from "./helpers/ui"; +import { getTransformHandles } from "../element/transformHandles"; +import { API } from "./helpers/api"; +import { KEYS } from "../keys"; +import { actionWrapTextInContainer } from "../actions/actionBoundText"; +import { arrayToMap } from "../utils"; +import { pointFrom } from "@excalidraw/math"; + +const { h } = window; + +const mouse = new Pointer("mouse"); + +describe("element binding", () => { + beforeEach(async () => { + await render(<Excalidraw handleKeyboardGlobally={true} />); + }); + + it("should create valid binding if duplicate start/end points", async () => { + const rect = API.createElement({ + type: "rectangle", + x: 0, + y: 0, + width: 50, + height: 50, + }); + const arrow = API.createElement({ + type: "arrow", + x: 100, + y: 0, + width: 100, + height: 1, + points: [ + pointFrom(0, 0), + pointFrom(0, 0), + pointFrom(100, 0), + pointFrom(100, 0), + ], + }); + API.setElements([rect, arrow]); + expect(arrow.startBinding).toBe(null); + + // select arrow + mouse.clickAt(150, 0); + + // move arrow start to potential binding position + mouse.downAt(100, 0); + mouse.moveTo(55, 0); + mouse.up(0, 0); + + // Point selection is evaluated like the points are rendered, + // from right to left. So clicking on the first point should move the joint, + // not the start point. + expect(arrow.startBinding).toBe(null); + + // Now that the start point is free, move it into overlapping position + mouse.downAt(100, 0); + mouse.moveTo(55, 0); + mouse.up(0, 0); + + expect(API.getSelectedElements()).toEqual([arrow]); + + expect(arrow.startBinding).toEqual({ + elementId: rect.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }); + + // Move the end point to the overlapping binding position + mouse.downAt(200, 0); + mouse.moveTo(55, 0); + mouse.up(0, 0); + + // Both the start and the end points should be bound + expect(arrow.startBinding).toEqual({ + elementId: rect.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }); + expect(arrow.endBinding).toEqual({ + elementId: rect.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }); + }); + + //@TODO fix the test with rotation + it.skip("rotation of arrow should rebind both ends", () => { + const rectLeft = UI.createElement("rectangle", { + x: 0, + width: 200, + height: 500, + }); + const rectRight = UI.createElement("rectangle", { + x: 400, + width: 200, + height: 500, + }); + const arrow = UI.createElement("arrow", { + x: 210, + y: 250, + width: 180, + height: 1, + }); + expect(arrow.startBinding?.elementId).toBe(rectLeft.id); + expect(arrow.endBinding?.elementId).toBe(rectRight.id); + + const rotation = getTransformHandles( + arrow, + h.state.zoom, + arrayToMap(h.elements), + "mouse", + ).rotation!; + const rotationHandleX = rotation[0] + rotation[2] / 2; + const rotationHandleY = rotation[1] + rotation[3] / 2; + mouse.down(rotationHandleX, rotationHandleY); + mouse.move(300, 400); + mouse.up(); + expect(arrow.angle).toBeGreaterThan(0.7 * Math.PI); + expect(arrow.angle).toBeLessThan(1.3 * Math.PI); + expect(arrow.startBinding?.elementId).toBe(rectRight.id); + expect(arrow.endBinding?.elementId).toBe(rectLeft.id); + }); + + // TODO fix & reenable once we rewrite tests to work with concurrency + it.skip( + "editing arrow and moving its head to bind it to element A, finalizing the" + + "editing by clicking on element A should end up selecting A", + async () => { + UI.createElement("rectangle", { + y: 0, + size: 100, + }); + // Create arrow bound to rectangle + UI.clickTool("arrow"); + mouse.down(50, -100); + mouse.up(0, 80); + + // Edit arrow with multi-point + mouse.doubleClick(); + // move arrow head + mouse.down(); + mouse.up(0, 10); + expect(API.getSelectedElement().type).toBe("arrow"); + + // NOTE this mouse down/up + await needs to be done in order to repro + // the issue, due to https://github.com/excalidraw/excalidraw/blob/46bff3daceb602accf60c40a84610797260fca94/src/components/App.tsx#L740 + mouse.reset(); + expect(h.state.editingLinearElement).not.toBe(null); + mouse.down(0, 0); + await new Promise((r) => setTimeout(r, 100)); + expect(h.state.editingLinearElement).toBe(null); + expect(API.getSelectedElement().type).toBe("rectangle"); + mouse.up(); + expect(API.getSelectedElement().type).toBe("rectangle"); + }, + ); + + it("should unbind arrow when moving it with keyboard", () => { + const rectangle = UI.createElement("rectangle", { + x: 75, + y: 0, + size: 100, + }); + + // Creates arrow 1px away from bidding with rectangle + const arrow = UI.createElement("arrow", { + x: 0, + y: 0, + size: 50, + }); + + expect(arrow.endBinding).toBe(null); + + mouse.downAt(50, 50); + mouse.moveTo(51, 0); + mouse.up(0, 0); + + // Test sticky connection + expect(API.getSelectedElement().type).toBe("arrow"); + Keyboard.keyPress(KEYS.ARROW_RIGHT); + expect(arrow.endBinding?.elementId).toBe(rectangle.id); + Keyboard.keyPress(KEYS.ARROW_LEFT); + expect(arrow.endBinding?.elementId).toBe(rectangle.id); + + // Sever connection + expect(API.getSelectedElement().type).toBe("arrow"); + Keyboard.keyPress(KEYS.ARROW_LEFT); + expect(arrow.endBinding).toBe(null); + Keyboard.keyPress(KEYS.ARROW_RIGHT); + expect(arrow.endBinding).toBe(null); + }); + + it("should unbind on bound element deletion", () => { + const rectangle = UI.createElement("rectangle", { + x: 60, + y: 0, + size: 100, + }); + + const arrow = UI.createElement("arrow", { + x: 0, + y: 0, + size: 50, + }); + + expect(arrow.endBinding?.elementId).toBe(rectangle.id); + + mouse.select(rectangle); + expect(API.getSelectedElement().type).toBe("rectangle"); + Keyboard.keyDown(KEYS.DELETE); + expect(arrow.endBinding).toBe(null); + }); + + it("should unbind on text element deletion by submitting empty text", async () => { + const text = API.createElement({ + type: "text", + text: "ola", + x: 60, + y: 0, + width: 100, + height: 100, + }); + + API.setElements([text]); + + const arrow = UI.createElement("arrow", { + x: 0, + y: 0, + size: 50, + }); + + expect(arrow.endBinding?.elementId).toBe(text.id); + + // edit text element and submit + // ------------------------------------------------------------------------- + + 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); + + fireEvent.change(editor, { target: { value: "" } }); + fireEvent.keyDown(editor, { key: KEYS.ESCAPE }); + + expect( + document.querySelector(".excalidraw-textEditorContainer > textarea"), + ).toBe(null); + expect(arrow.endBinding).toBe(null); + }); + + it("should keep binding on text update", async () => { + const text = API.createElement({ + type: "text", + text: "ola", + x: 60, + y: 0, + width: 100, + height: 100, + }); + + API.setElements([text]); + + const arrow = UI.createElement("arrow", { + x: 0, + y: 0, + size: 50, + }); + + expect(arrow.endBinding?.elementId).toBe(text.id); + + // delete text element by submitting empty 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); + + fireEvent.change(editor, { target: { value: "asdasdasdasdas" } }); + fireEvent.keyDown(editor, { key: KEYS.ESCAPE }); + + expect( + document.querySelector(".excalidraw-textEditorContainer > textarea"), + ).toBe(null); + expect(arrow.endBinding?.elementId).toBe(text.id); + }); + + it("should update binding when text containerized", async () => { + const rectangle1 = API.createElement({ + type: "rectangle", + id: "rectangle1", + width: 100, + height: 100, + boundElements: [ + { id: "arrow1", type: "arrow" }, + { id: "arrow2", type: "arrow" }, + ], + }); + + const arrow1 = API.createElement({ + type: "arrow", + id: "arrow1", + points: [pointFrom(0, 0), pointFrom(0, -87.45777932247563)], + startBinding: { + elementId: "rectangle1", + focus: 0.2, + gap: 7, + fixedPoint: [0.5, 1], + }, + endBinding: { + elementId: "text1", + focus: 0.2, + gap: 7, + fixedPoint: [1, 0.5], + }, + }); + + const arrow2 = API.createElement({ + type: "arrow", + id: "arrow2", + points: [pointFrom(0, 0), pointFrom(0, -87.45777932247563)], + startBinding: { + elementId: "text1", + focus: 0.2, + gap: 7, + fixedPoint: [0.5, 1], + }, + endBinding: { + elementId: "rectangle1", + focus: 0.2, + gap: 7, + fixedPoint: [1, 0.5], + }, + }); + + const text1 = API.createElement({ + type: "text", + id: "text1", + text: "ola", + boundElements: [ + { id: "arrow1", type: "arrow" }, + { id: "arrow2", type: "arrow" }, + ], + }); + + API.setElements([rectangle1, arrow1, arrow2, text1]); + + API.setSelectedElements([text1]); + + expect(h.state.selectedElementIds[text1.id]).toBe(true); + + API.executeAction(actionWrapTextInContainer); + + // new text container will be placed before the text element + const container = h.elements.at(-2)!; + + expect(container.type).toBe("rectangle"); + expect(container.id).not.toBe(rectangle1.id); + + expect(container).toEqual( + expect.objectContaining({ + boundElements: expect.arrayContaining([ + { + type: "text", + id: text1.id, + }, + { + type: "arrow", + id: arrow1.id, + }, + { + type: "arrow", + id: arrow2.id, + }, + ]), + }), + ); + + expect(arrow1.startBinding?.elementId).toBe(rectangle1.id); + expect(arrow1.endBinding?.elementId).toBe(container.id); + expect(arrow2.startBinding?.elementId).toBe(container.id); + expect(arrow2.endBinding?.elementId).toBe(rectangle1.id); + }); + + // #6459 + it("should unbind arrow only from the latest element", () => { + const rectLeft = UI.createElement("rectangle", { + x: 0, + width: 200, + height: 500, + }); + const rectRight = UI.createElement("rectangle", { + x: 400, + width: 200, + height: 500, + }); + const arrow = UI.createElement("arrow", { + x: 210, + y: 250, + width: 180, + height: 1, + }); + expect(arrow.startBinding?.elementId).toBe(rectLeft.id); + expect(arrow.endBinding?.elementId).toBe(rectRight.id); + + // Drag arrow off of bound rectangle range + const handles = getTransformHandles( + arrow, + h.state.zoom, + arrayToMap(h.elements), + "mouse", + ).se!; + + Keyboard.keyDown(KEYS.CTRL_OR_CMD); + const elX = handles[0] + handles[2] / 2; + const elY = handles[1] + handles[3] / 2; + mouse.downAt(elX, elY); + mouse.moveTo(300, 400); + mouse.up(); + + expect(arrow.startBinding).not.toBe(null); + expect(arrow.endBinding).toBe(null); + }); + + it("should not unbind when duplicating via selection group", () => { + const rectLeft = UI.createElement("rectangle", { + x: 0, + width: 200, + height: 500, + }); + const rectRight = UI.createElement("rectangle", { + x: 400, + y: 200, + width: 200, + height: 500, + }); + const arrow = UI.createElement("arrow", { + x: 210, + y: 250, + width: 177, + height: 1, + }); + expect(arrow.startBinding?.elementId).toBe(rectLeft.id); + expect(arrow.endBinding?.elementId).toBe(rectRight.id); + + mouse.downAt(-100, -100); + mouse.moveTo(650, 750); + mouse.up(0, 0); + + expect(API.getSelectedElements().length).toBe(3); + + mouse.moveTo(5, 5); + Keyboard.withModifierKeys({ alt: true }, () => { + mouse.downAt(5, 5); + mouse.moveTo(1000, 1000); + mouse.up(0, 0); + + expect(window.h.elements.length).toBe(6); + window.h.elements.forEach((element) => { + if (isLinearElement(element)) { + expect(element.startBinding).not.toBe(null); + expect(element.endBinding).not.toBe(null); + } else { + expect(element.boundElements).not.toBe(null); + } + }); + }); + }); +}); diff --git a/packages/excalidraw/tests/charts.test.tsx b/packages/excalidraw/tests/charts.test.tsx new file mode 100644 index 0000000..a4bce15 --- /dev/null +++ b/packages/excalidraw/tests/charts.test.tsx @@ -0,0 +1,13 @@ +import { tryParseSpreadsheet } from "../charts"; + +describe("tryParseSpreadsheet", () => { + it("works for numbers with comma in them", () => { + const result = tryParseSpreadsheet( + `Week Index${"\t"}Users +Week 1${"\t"}814 +Week 2${"\t"}10,301 +Week 3${"\t"}4,264`, + ); + expect(result).toMatchSnapshot(); + }); +}); diff --git a/packages/excalidraw/tests/clients.test.ts b/packages/excalidraw/tests/clients.test.ts new file mode 100644 index 0000000..e78cf18 --- /dev/null +++ b/packages/excalidraw/tests/clients.test.ts @@ -0,0 +1,39 @@ +import { getNameInitial } from "../clients"; + +describe("getClientInitials", () => { + it("returns substring if one name provided", () => { + expect(getNameInitial("Alan")).toBe("A"); + }); + + it("returns initials", () => { + expect(getNameInitial("John Doe")).toBe("J"); + }); + + it("returns correct initials if many names provided", () => { + expect(getNameInitial("John Alan Doe")).toBe("J"); + }); + + it("returns single initial if 1 letter provided", () => { + expect(getNameInitial("z")).toBe("Z"); + }); + + it("trims trailing whitespace", () => { + expect(getNameInitial(" q ")).toBe("Q"); + }); + + it('returns "?" if falsey value provided', () => { + expect(getNameInitial("")).toBe("?"); + expect(getNameInitial(undefined)).toBe("?"); + expect(getNameInitial(null)).toBe("?"); + }); + + it('returns "?" when value is blank', () => { + expect(getNameInitial(" ")).toBe("?"); + }); + + it("works with multibyte strings", () => { + expect(getNameInitial("😀")).toBe("😀"); + // but doesn't work with emoji ZWJ sequences + expect(getNameInitial("👨👩👦")).toBe("👨"); + }); +}); diff --git a/packages/excalidraw/tests/clipboard.test.tsx b/packages/excalidraw/tests/clipboard.test.tsx new file mode 100644 index 0000000..c2c0a5e --- /dev/null +++ b/packages/excalidraw/tests/clipboard.test.tsx @@ -0,0 +1,521 @@ +import React from "react"; +import { vi } from "vitest"; +import { + render, + waitFor, + GlobalTestState, + unmountComponent, +} from "./test-utils"; +import { Pointer, Keyboard } from "./helpers/ui"; +import { Excalidraw } from "../index"; +import { KEYS } from "../keys"; +import { getLineHeightInPx } from "../element/textMeasurements"; +import { getElementBounds } from "../element"; +import type { NormalizedZoomValue } from "../types"; +import { API } from "./helpers/api"; +import { createPasteEvent, serializeAsClipboardJSON } from "../clipboard"; +import { arrayToMap } from "../utils"; +import { mockMermaidToExcalidraw } from "./helpers/mocks"; +import { getLineHeight } from "../fonts"; + +const { h } = window; + +const mouse = new Pointer("mouse"); + +vi.mock("../keys.ts", async (importOriginal) => { + const module: any = await importOriginal(); + return { + __esmodule: true, + ...module, + isDarwin: false, + KEYS: { + ...module.KEYS, + CTRL_OR_CMD: "ctrlKey", + }, + }; +}); + +const sendPasteEvent = (text: string) => { + const clipboardEvent = createPasteEvent({ + types: { + "text/plain": text, + }, + }); + document.dispatchEvent(clipboardEvent); +}; + +const pasteWithCtrlCmdShiftV = (text: string) => { + Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => { + //triggering keydown with an empty clipboard + Keyboard.keyPress(KEYS.V); + //triggering paste event with faked clipboard + sendPasteEvent(text); + }); +}; + +const pasteWithCtrlCmdV = (text: string) => { + Keyboard.withModifierKeys({ ctrl: true }, () => { + //triggering keydown with an empty clipboard + Keyboard.keyPress(KEYS.V); + //triggering paste event with faked clipboard + sendPasteEvent(text); + }); +}; + +const sleep = (ms: number) => { + return new Promise((resolve) => setTimeout(() => resolve(null), ms)); +}; + +beforeEach(async () => { + unmountComponent(); + + localStorage.clear(); + + mouse.reset(); + + await render( + <Excalidraw + autoFocus={true} + handleKeyboardGlobally={true} + initialData={{ appState: { zoom: { value: 1 as NormalizedZoomValue } } }} + />, + ); + Object.assign(document, { + elementFromPoint: () => GlobalTestState.canvas, + }); +}); + +describe("general paste behavior", () => { + it("should randomize seed on paste", async () => { + const rectangle = API.createElement({ type: "rectangle" }); + const clipboardJSON = await serializeAsClipboardJSON({ + elements: [rectangle], + files: null, + }); + pasteWithCtrlCmdV(clipboardJSON); + + await waitFor(() => { + expect(h.elements.length).toBe(1); + expect(h.elements[0].seed).not.toBe(rectangle.seed); + }); + }); + + it("should retain seed on shift-paste", async () => { + const rectangle = API.createElement({ type: "rectangle" }); + const clipboardJSON = await serializeAsClipboardJSON({ + elements: [rectangle], + files: null, + }); + + // assert we don't randomize seed on shift-paste + pasteWithCtrlCmdShiftV(clipboardJSON); + await waitFor(() => { + expect(h.elements.length).toBe(1); + expect(h.elements[0].seed).toBe(rectangle.seed); + }); + }); +}); + +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"; + pasteWithCtrlCmdV(text); + 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"; + pasteWithCtrlCmdV(text); + 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"; + pasteWithCtrlCmdV(text); + 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 elementsMap = arrayToMap(h.elements); + + const text = "hkhkjhki\njgkjhffjh\njgkjhffjh"; + const lineHeightPx = + getLineHeightInPx( + h.app.state.currentItemFontSize, + getLineHeight(h.state.currentItemFontFamily), + ) + + 10 / h.app.state.zoom.value; + mouse.moveTo(100, 100); + pasteWithCtrlCmdV(text); + await waitFor(async () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [fx, firstElY] = getElementBounds(h.elements[0], elementsMap); + 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], elementsMap); + expect(elY).toEqual(firstElY + lineHeightPx * i); + } + }); + }); + + it("should leave a space for blank new lines", async () => { + const elementsMap = arrayToMap(h.elements); + const text = "hkhkjhki\n\njgkjhffjh"; + const lineHeightPx = + getLineHeightInPx( + h.app.state.currentItemFontSize, + getLineHeight(h.state.currentItemFontFamily), + ) + + 10 / h.app.state.zoom.value; + mouse.moveTo(100, 100); + pasteWithCtrlCmdV(text); + + await waitFor(async () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [fx, firstElY] = getElementBounds(h.elements[0], elementsMap); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [lx, lastElY] = getElementBounds(h.elements[1], elementsMap); + 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"; + pasteWithCtrlCmdShiftV(text); + 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"; + pasteWithCtrlCmdShiftV(text); + 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], + }); + pasteWithCtrlCmdShiftV(data); + + 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, + ], + }); + pasteWithCtrlCmdShiftV(data); + + 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); + }); + }); +}); + +describe("pasting & frames", () => { + it("should add pasted elements to frame under cursor", async () => { + const frame = API.createElement({ + type: "frame", + width: 100, + height: 100, + x: 0, + y: 0, + }); + const rect = API.createElement({ type: "rectangle" }); + + API.setElements([frame]); + + const clipboardJSON = await serializeAsClipboardJSON({ + elements: [rect], + files: null, + }); + + mouse.moveTo(50, 50); + + pasteWithCtrlCmdV(clipboardJSON); + + await waitFor(() => { + expect(h.elements.length).toBe(2); + expect(h.elements[1].type).toBe(rect.type); + expect(h.elements[1].frameId).toBe(frame.id); + }); + }); + + it("should filter out elements not overlapping frame", async () => { + const frame = API.createElement({ + type: "frame", + width: 100, + height: 100, + x: 0, + y: 0, + }); + const rect = API.createElement({ + type: "rectangle", + width: 50, + height: 50, + }); + const rect2 = API.createElement({ + type: "rectangle", + width: 50, + height: 50, + x: 100, + y: 100, + }); + + API.setElements([frame]); + + const clipboardJSON = await serializeAsClipboardJSON({ + elements: [rect, rect2], + files: null, + }); + + mouse.moveTo(90, 90); + + pasteWithCtrlCmdV(clipboardJSON); + + await waitFor(() => { + expect(h.elements.length).toBe(3); + expect(h.elements[1].type).toBe(rect.type); + expect(h.elements[1].frameId).toBe(frame.id); + expect(h.elements[2].type).toBe(rect2.type); + expect(h.elements[2].frameId).toBe(null); + }); + }); + + it("should not filter out elements not overlapping frame if part of group", async () => { + const frame = API.createElement({ + type: "frame", + width: 100, + height: 100, + x: 0, + y: 0, + }); + const rect = API.createElement({ + type: "rectangle", + width: 50, + height: 50, + groupIds: ["g1"], + }); + const rect2 = API.createElement({ + type: "rectangle", + width: 50, + height: 50, + x: 100, + y: 100, + groupIds: ["g1"], + }); + + API.setElements([frame]); + + const clipboardJSON = await serializeAsClipboardJSON({ + elements: [rect, rect2], + files: null, + }); + + mouse.moveTo(90, 90); + + pasteWithCtrlCmdV(clipboardJSON); + + await waitFor(() => { + expect(h.elements.length).toBe(3); + expect(h.elements[1].type).toBe(rect.type); + expect(h.elements[1].frameId).toBe(frame.id); + expect(h.elements[2].type).toBe(rect2.type); + expect(h.elements[2].frameId).toBe(frame.id); + }); + }); + + it("should not filter out other frames and their children", async () => { + const frame = API.createElement({ + type: "frame", + width: 100, + height: 100, + x: 0, + y: 0, + }); + const rect = API.createElement({ + type: "rectangle", + width: 50, + height: 50, + groupIds: ["g1"], + }); + + const frame2 = API.createElement({ + type: "frame", + width: 75, + height: 75, + x: 0, + y: 0, + }); + const rect2 = API.createElement({ + type: "rectangle", + width: 50, + height: 50, + x: 55, + y: 55, + frameId: frame2.id, + }); + + API.setElements([frame]); + + const clipboardJSON = await serializeAsClipboardJSON({ + elements: [rect, rect2, frame2], + files: null, + }); + + mouse.moveTo(90, 90); + + pasteWithCtrlCmdV(clipboardJSON); + + await waitFor(() => { + expect(h.elements.length).toBe(4); + expect(h.elements[1].type).toBe(rect.type); + expect(h.elements[1].frameId).toBe(frame.id); + expect(h.elements[2].type).toBe(rect2.type); + expect(h.elements[2].frameId).toBe(h.elements[3].id); + expect(h.elements[3].type).toBe(frame2.type); + expect(h.elements[3].frameId).toBe(null); + }); + }); +}); + +describe("clipboard - pasting mermaid definition", () => { + beforeAll(() => { + mockMermaidToExcalidraw({ + parseMermaidToExcalidraw: async (definition) => { + const lines = definition.split("\n"); + return new Promise((resolve, reject) => { + if (lines.some((line) => line === "flowchart TD")) { + resolve({ + elements: [ + { + id: "rect1", + type: "rectangle", + groupIds: [], + x: 0, + y: 0, + width: 69.703125, + height: 44, + strokeWidth: 2, + label: { + groupIds: [], + text: "A", + fontSize: 20, + }, + link: null, + }, + ], + }); + } else { + reject(new Error("ERROR")); + } + }); + }, + }); + }); + + it("should detect and paste as mermaid", async () => { + const text = "flowchart TD\nA"; + + pasteWithCtrlCmdV(text); + await waitFor(() => { + expect(h.elements.length).toEqual(2); + expect(h.elements).toEqual( + expect.arrayContaining([ + expect.objectContaining({ type: "rectangle" }), + expect.objectContaining({ type: "text", text: "A" }), + ]), + ); + }); + }); + + it("should support directives", async () => { + const text = "%%{init: { **config** } }%%\nflowchart TD\nA"; + + pasteWithCtrlCmdV(text); + await waitFor(() => { + expect(h.elements.length).toEqual(2); + expect(h.elements).toEqual( + expect.arrayContaining([ + expect.objectContaining({ type: "rectangle" }), + expect.objectContaining({ type: "text", text: "A" }), + ]), + ); + }); + }); + + it("should paste as normal text if invalid mermaid", async () => { + const text = "flowchart TD xx\nA"; + pasteWithCtrlCmdV(text); + await waitFor(() => { + expect(h.elements.length).toEqual(2); + expect(h.elements).toEqual( + expect.arrayContaining([ + expect.objectContaining({ type: "text", text: "flowchart TD xx" }), + expect.objectContaining({ type: "text", text: "A" }), + ]), + ); + }); + }); +}); diff --git a/packages/excalidraw/tests/contextmenu.test.tsx b/packages/excalidraw/tests/contextmenu.test.tsx new file mode 100644 index 0000000..bb3f415 --- /dev/null +++ b/packages/excalidraw/tests/contextmenu.test.tsx @@ -0,0 +1,625 @@ +import React from "react"; +import { + render, + fireEvent, + mockBoundingClientRect, + restoreOriginalGetBoundingClientRect, + GlobalTestState, + screen, + queryByText, + queryAllByText, + waitFor, + togglePopover, + unmountComponent, +} from "./test-utils"; +import { Excalidraw } from "../index"; +import * as StaticScene from "../renderer/staticScene"; +import { reseed } from "../random"; +import { UI, Pointer, Keyboard } from "./helpers/ui"; +import { KEYS } from "../keys"; +import type { ShortcutName } from "../actions/shortcuts"; +import { copiedStyles } from "../actions/actionStyles"; +import { API } from "./helpers/api"; +import { setDateTimeForTests } from "../utils"; +import { vi } from "vitest"; +import type { ActionName } from "../actions/types"; + +const checkpoint = (name: string) => { + expect(renderStaticScene.mock.calls.length).toMatchSnapshot( + `[${name}] number of renders`, + ); + expect(h.state).toMatchSnapshot(`[${name}] appState`); + expect(h.history).toMatchSnapshot(`[${name}] history`); + expect(h.elements.length).toMatchSnapshot(`[${name}] number of elements`); + h.elements.forEach((element, i) => + expect(element).toMatchSnapshot(`[${name}] element ${i}`), + ); +}; + +const mouse = new Pointer("mouse"); + +unmountComponent(); + +const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene"); +beforeEach(() => { + localStorage.clear(); + renderStaticScene.mockClear(); + reseed(7); +}); + +const { h } = window; + +describe("contextMenu element", () => { + beforeEach(async () => { + localStorage.clear(); + renderStaticScene.mockClear(); + reseed(7); + setDateTimeForTests("201933152653"); + + await render(<Excalidraw handleKeyboardGlobally={true} />); + }); + + beforeAll(() => { + mockBoundingClientRect(); + }); + + afterAll(() => { + restoreOriginalGetBoundingClientRect(); + }); + + afterEach(() => { + checkpoint("end of test"); + + mouse.reset(); + mouse.down(0, 0); + }); + + it("shows context menu for canvas", () => { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 1, + clientY: 1, + }); + const contextMenu = UI.queryContextMenu(); + const contextMenuOptions = + contextMenu?.querySelectorAll(".context-menu li"); + const expectedShortcutNames: ShortcutName[] = [ + "paste", + "selectAll", + "gridMode", + "zenMode", + "viewMode", + "objectsSnapMode", + "stats", + ]; + + expect(contextMenu).not.toBeNull(); + expect(contextMenuOptions?.length).toBe(expectedShortcutNames.length); + expectedShortcutNames.forEach((shortcutName) => { + expect( + contextMenu?.querySelector(`li[data-testid="${shortcutName}"]`), + ).not.toBeNull(); + }); + }); + + it("shows context menu for element", () => { + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(20, 20); + + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 3, + clientY: 3, + }); + const contextMenu = UI.queryContextMenu(); + const contextMenuOptions = + contextMenu?.querySelectorAll(".context-menu li"); + const expectedContextMenuItems: ActionName[] = [ + "cut", + "copy", + "paste", + "wrapSelectionInFrame", + "copyStyles", + "pasteStyles", + "deleteSelectedElements", + "addToLibrary", + "flipHorizontal", + "flipVertical", + "sendBackward", + "bringForward", + "sendToBack", + "bringToFront", + "duplicateSelection", + "hyperlink", + "copyElementLink", + "toggleElementLock", + ]; + + expect(contextMenu).not.toBeNull(); + expect(contextMenuOptions?.length).toBe(expectedContextMenuItems.length); + expectedContextMenuItems.forEach((item) => { + expect( + contextMenu?.querySelector(`li[data-testid="${item}"]`), + ).not.toBeNull(); + }); + }); + + it("shows context menu for element", () => { + const rect1 = API.createElement({ + type: "rectangle", + x: 0, + y: 0, + height: 200, + width: 200, + backgroundColor: "red", + }); + const rect2 = API.createElement({ + type: "rectangle", + x: 0, + y: 0, + height: 200, + width: 200, + backgroundColor: "red", + }); + API.setElements([rect1, rect2]); + API.setSelectedElements([rect1]); + + // lower z-index + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 100, + clientY: 100, + }); + expect(UI.queryContextMenu()).not.toBeNull(); + expect(API.getSelectedElement().id).toBe(rect1.id); + + // higher z-index + API.setSelectedElements([rect2]); + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 100, + clientY: 100, + }); + expect(UI.queryContextMenu()).not.toBeNull(); + expect(API.getSelectedElement().id).toBe(rect2.id); + }); + + it("shows 'Group selection' in context menu for multiple selected elements", () => { + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(10, 10); + + UI.clickTool("rectangle"); + mouse.down(12, -10); + mouse.up(10, 10); + + mouse.reset(); + mouse.click(10, 10); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.click(22, 0); + }); + + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 3, + clientY: 3, + }); + + const contextMenu = UI.queryContextMenu(); + const contextMenuOptions = + contextMenu?.querySelectorAll(".context-menu li"); + const expectedShortcutNames: ShortcutName[] = [ + "cut", + "copy", + "paste", + "wrapSelectionInFrame", + "copyStyles", + "pasteStyles", + "deleteSelectedElements", + "group", + "addToLibrary", + "flipHorizontal", + "flipVertical", + "sendBackward", + "bringForward", + "sendToBack", + "bringToFront", + "duplicateSelection", + "toggleElementLock", + ]; + + expect(contextMenu).not.toBeNull(); + expect(contextMenuOptions?.length).toBe(expectedShortcutNames.length); + expectedShortcutNames.forEach((shortcutName) => { + expect( + contextMenu?.querySelector(`li[data-testid="${shortcutName}"]`), + ).not.toBeNull(); + }); + }); + + it("shows 'Ungroup selection' in context menu for group inside selected elements", () => { + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(10, 10); + + UI.clickTool("rectangle"); + mouse.down(12, -10); + mouse.up(10, 10); + + mouse.reset(); + mouse.click(10, 10); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.click(22, 0); + }); + + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.G); + }); + + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 3, + clientY: 3, + }); + + const contextMenu = UI.queryContextMenu(); + const contextMenuOptions = + contextMenu?.querySelectorAll(".context-menu li"); + const expectedContextMenuItems: ActionName[] = [ + "cut", + "copy", + "paste", + "wrapSelectionInFrame", + "copyStyles", + "pasteStyles", + "deleteSelectedElements", + "copyElementLink", + "ungroup", + "addToLibrary", + "flipHorizontal", + "flipVertical", + "sendBackward", + "bringForward", + "sendToBack", + "bringToFront", + "duplicateSelection", + "toggleElementLock", + ]; + + expect(contextMenu).not.toBeNull(); + expect(contextMenuOptions?.length).toBe(expectedContextMenuItems.length); + expectedContextMenuItems.forEach((item) => { + expect( + contextMenu?.querySelector(`li[data-testid="${item}"]`), + ).not.toBeNull(); + }); + }); + + it("selecting 'Copy styles' in context menu copies styles", () => { + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(20, 20); + + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 3, + clientY: 3, + }); + const contextMenu = UI.queryContextMenu(); + expect(copiedStyles).toBe("{}"); + fireEvent.click(queryByText(contextMenu!, "Copy styles")!); + expect(copiedStyles).not.toBe("{}"); + const element = JSON.parse(copiedStyles)[0]; + expect(element).toEqual(API.getSelectedElement()); + }); + + it("selecting 'Paste styles' in context menu pastes styles", () => { + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(20, 20); + + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(20, 20); + + // Change some styles of second rectangle + togglePopover("Stroke"); + UI.clickOnTestId("color-red"); + togglePopover("Background"); + UI.clickOnTestId("color-blue"); + // Fill style + fireEvent.click(screen.getByTitle("Cross-hatch")); + // Stroke width + fireEvent.click(screen.getByTitle("Bold")); + // Stroke style + fireEvent.click(screen.getByTitle("Dotted")); + // Roughness + fireEvent.click(screen.getByTitle("Cartoonist")); + // Opacity + fireEvent.change(screen.getByTestId("opacity"), { + target: { value: "60" }, + }); + + // closing the background popover as this blocks + // context menu from rendering after we started focussing + // the popover once rendered :/ + togglePopover("Background"); + + mouse.reset(); + + // Copy styles of second rectangle + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 40, + clientY: 40, + }); + + let contextMenu = UI.queryContextMenu(); + fireEvent.click(queryByText(contextMenu!, "Copy styles")!); + const secondRect = JSON.parse(copiedStyles)[0]; + expect(secondRect.id).toBe(h.elements[1].id); + + mouse.reset(); + // Paste styles to first rectangle + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 10, + clientY: 10, + }); + contextMenu = UI.queryContextMenu(); + fireEvent.click(queryByText(contextMenu!, "Paste styles")!); + + const firstRect = API.getSelectedElement(); + expect(firstRect.id).toBe(h.elements[0].id); + expect(firstRect.strokeColor).toBe("#e03131"); + expect(firstRect.backgroundColor).toBe("#a5d8ff"); + expect(firstRect.fillStyle).toBe("cross-hatch"); + expect(firstRect.strokeWidth).toBe(2); // Bold: 2 + expect(firstRect.strokeStyle).toBe("dotted"); + expect(firstRect.roughness).toBe(2); // Cartoonist: 2 + expect(firstRect.opacity).toBe(60); + }); + + it("selecting 'Delete' in context menu deletes element", () => { + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(20, 20); + + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 3, + clientY: 3, + }); + const contextMenu = UI.queryContextMenu(); + fireEvent.click(queryAllByText(contextMenu!, "Delete")[0]); + expect(API.getSelectedElements()).toHaveLength(0); + expect(h.elements[0].isDeleted).toBe(true); + }); + + it("selecting 'Add to library' in context menu adds element to library", async () => { + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(20, 20); + + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 3, + clientY: 3, + }); + const contextMenu = UI.queryContextMenu(); + fireEvent.click(queryByText(contextMenu!, "Add to library")!); + + await waitFor(async () => { + const libraryItems = await h.app.library.getLatestLibrary(); + expect(libraryItems[0].elements[0]).toEqual(h.elements[0]); + }); + }); + + it("selecting 'Duplicate' in context menu duplicates element", () => { + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(20, 20); + + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 3, + clientY: 3, + }); + const contextMenu = UI.queryContextMenu(); + fireEvent.click(queryByText(contextMenu!, "Duplicate")!); + expect(h.elements).toHaveLength(2); + const { + id: _id0, + seed: _seed0, + x: _x0, + y: _y0, + index: _fractionalIndex0, + version: _version0, + versionNonce: _versionNonce0, + ...rect1 + } = h.elements[0]; + const { + id: _id1, + seed: _seed1, + x: _x1, + y: _y1, + index: _fractionalIndex1, + version: _version1, + versionNonce: _versionNonce1, + ...rect2 + } = h.elements[1]; + expect(rect1).toEqual(rect2); + }); + + it("selecting 'Send backward' in context menu sends element backward", () => { + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(20, 20); + + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(20, 20); + + mouse.reset(); + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 40, + clientY: 40, + }); + const contextMenu = UI.queryContextMenu(); + const elementsBefore = h.elements; + fireEvent.click(queryByText(contextMenu!, "Send backward")!); + expect(elementsBefore[0].id).toEqual(h.elements[1].id); + expect(elementsBefore[1].id).toEqual(h.elements[0].id); + }); + + it("selecting 'Bring forward' in context menu brings element forward", () => { + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(20, 20); + + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(20, 20); + + mouse.reset(); + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 10, + clientY: 10, + }); + const contextMenu = UI.queryContextMenu(); + const elementsBefore = h.elements; + fireEvent.click(queryByText(contextMenu!, "Bring forward")!); + expect(elementsBefore[0].id).toEqual(h.elements[1].id); + expect(elementsBefore[1].id).toEqual(h.elements[0].id); + }); + + it("selecting 'Send to back' in context menu sends element to back", () => { + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(20, 20); + + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(20, 20); + + mouse.reset(); + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 40, + clientY: 40, + }); + const contextMenu = UI.queryContextMenu(); + const elementsBefore = h.elements; + fireEvent.click(queryByText(contextMenu!, "Send to back")!); + expect(elementsBefore[1].id).toEqual(h.elements[0].id); + }); + + it("selecting 'Bring to front' in context menu brings element to front", () => { + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(20, 20); + + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(20, 20); + + mouse.reset(); + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 10, + clientY: 10, + }); + const contextMenu = UI.queryContextMenu(); + const elementsBefore = h.elements; + fireEvent.click(queryByText(contextMenu!, "Bring to front")!); + expect(elementsBefore[0].id).toEqual(h.elements[1].id); + }); + + it("selecting 'Group selection' in context menu groups selected elements", () => { + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(20, 20); + + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(20, 20); + + mouse.reset(); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.click(10, 10); + }); + + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 3, + clientY: 3, + }); + const contextMenu = UI.queryContextMenu(); + fireEvent.click(queryByText(contextMenu!, "Group selection")!); + const selectedGroupIds = Object.keys(h.state.selectedGroupIds); + expect(h.elements[0].groupIds).toEqual(selectedGroupIds); + expect(h.elements[1].groupIds).toEqual(selectedGroupIds); + }); + + it("selecting 'Ungroup selection' in context menu ungroups selected group", () => { + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(20, 20); + + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(20, 20); + + mouse.reset(); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.click(10, 10); + }); + + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.G); + }); + + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 3, + clientY: 3, + }); + + const contextMenu = UI.queryContextMenu(); + expect(contextMenu).not.toBeNull(); + fireEvent.click(queryByText(contextMenu!, "Ungroup selection")!); + + const selectedGroupIds = Object.keys(h.state.selectedGroupIds); + expect(selectedGroupIds).toHaveLength(0); + expect(h.elements[0].groupIds).toHaveLength(0); + expect(h.elements[1].groupIds).toHaveLength(0); + }); + + it("right-clicking on a group should select whole group", () => { + const rectangle1 = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + groupIds: ["g1"], + }); + const rectangle2 = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + groupIds: ["g1"], + }); + API.setElements([rectangle1, rectangle2]); + + mouse.rightClickAt(50, 50); + expect(API.getSelectedElements().length).toBe(2); + expect(API.getSelectedElements()).toEqual([ + expect.objectContaining({ id: rectangle1.id }), + expect.objectContaining({ id: rectangle2.id }), + ]); + }); +}); diff --git a/packages/excalidraw/tests/cropElement.test.tsx b/packages/excalidraw/tests/cropElement.test.tsx new file mode 100644 index 0000000..a508f5d --- /dev/null +++ b/packages/excalidraw/tests/cropElement.test.tsx @@ -0,0 +1,342 @@ +import React from "react"; +import { vi } from "vitest"; +import { Keyboard, Pointer, UI } from "./helpers/ui"; +import type { ExcalidrawImageElement, ImageCrop } from "../element/types"; +import { act, GlobalTestState, render, unmountComponent } from "./test-utils"; +import { Excalidraw, exportToCanvas, exportToSvg } from ".."; +import { API } from "./helpers/api"; +import type { NormalizedZoomValue } from "../types"; +import { KEYS } from "../keys"; +import { duplicateElement } from "../element"; +import { cloneJSON } from "../utils"; +import { actionFlipHorizontal, actionFlipVertical } from "../actions"; + +const { h } = window; +const mouse = new Pointer("mouse"); + +beforeEach(async () => { + unmountComponent(); + + mouse.reset(); + localStorage.clear(); + sessionStorage.clear(); + vi.clearAllMocks(); + + Object.assign(document, { + elementFromPoint: () => GlobalTestState.canvas, + }); + await render(<Excalidraw autoFocus={true} handleKeyboardGlobally={true} />); + API.setAppState({ + zoom: { + value: 1 as NormalizedZoomValue, + }, + }); + + const image = API.createElement({ type: "image", width: 200, height: 100 }); + API.setElements([image]); + API.setAppState({ + selectedElementIds: { + [image.id]: true, + }, + }); +}); + +const generateRandomNaturalWidthAndHeight = (image: ExcalidrawImageElement) => { + const initialWidth = image.width; + const initialHeight = image.height; + + const scale = 1 + Math.random() * 5; + + return { + naturalWidth: initialWidth * scale, + naturalHeight: initialHeight * scale, + }; +}; + +const compareCrops = (cropA: ImageCrop, cropB: ImageCrop) => { + (Object.keys(cropA) as [keyof ImageCrop]).forEach((key) => { + const propA = cropA[key]; + const propB = cropB[key]; + + expect(propA as number).toBeCloseTo(propB as number); + }); +}; + +describe("Enter and leave the crop editor", () => { + it("enter the editor by double clicking", () => { + const image = h.elements[0]; + expect(h.state.croppingElementId).toBe(null); + mouse.doubleClickOn(image); + expect(h.state.croppingElementId).not.toBe(null); + expect(h.state.croppingElementId).toBe(image.id); + }); + + it("enter the editor by pressing enter", () => { + const image = h.elements[0]; + expect(h.state.croppingElementId).toBe(null); + Keyboard.keyDown(KEYS.ENTER); + expect(h.state.croppingElementId).not.toBe(null); + expect(h.state.croppingElementId).toBe(image.id); + }); + + it("leave the editor by clicking outside", () => { + const image = h.elements[0]; + Keyboard.keyDown(KEYS.ENTER); + expect(h.state.croppingElementId).not.toBe(null); + + mouse.click(image.x - 20, image.y - 20); + expect(h.state.croppingElementId).toBe(null); + }); + + it("leave the editor by pressing escape", () => { + const image = h.elements[0]; + mouse.doubleClickOn(image); + expect(h.state.croppingElementId).not.toBe(null); + + Keyboard.keyDown(KEYS.ESCAPE); + expect(h.state.croppingElementId).toBe(null); + }); +}); + +describe("Crop an image", () => { + it("Cropping changes the dimension", async () => { + const image = h.elements[0] as ExcalidrawImageElement; + + const initialWidth = image.width; + const initialHeight = image.height; + + const { naturalWidth, naturalHeight } = + generateRandomNaturalWidthAndHeight(image); + + UI.crop(image, "w", naturalWidth, naturalHeight, [initialWidth / 2, 0]); + + expect(image.width).toBeLessThan(initialWidth); + UI.crop(image, "n", naturalWidth, naturalHeight, [0, initialHeight / 2]); + expect(image.height).toBeLessThan(initialHeight); + }); + + it("Cropping has minimal sizes", async () => { + const image = h.elements[0] as ExcalidrawImageElement; + const initialWidth = image.width; + const initialHeight = image.height; + + const { naturalWidth, naturalHeight } = + generateRandomNaturalWidthAndHeight(image); + + UI.crop(image, "w", naturalWidth, naturalHeight, [initialWidth, 0]); + expect(image.width).toBeLessThan(initialWidth); + expect(image.width).toBeGreaterThan(0); + UI.crop(image, "w", naturalWidth, naturalHeight, [-initialWidth, 0]); + UI.crop(image, "n", naturalWidth, naturalHeight, [0, initialHeight]); + expect(image.height).toBeLessThan(initialHeight); + expect(image.height).toBeGreaterThan(0); + }); + + it("Preserve aspect ratio", async () => { + let image = h.elements[0] as ExcalidrawImageElement; + const initialWidth = image.width; + const initialHeight = image.height; + + const { naturalWidth, naturalHeight } = + generateRandomNaturalWidthAndHeight(image); + + UI.crop(image, "w", naturalWidth, naturalHeight, [initialWidth / 3, 0]); + + let resizedWidth = image.width; + let resizedHeight = image.height; + + // max height, cropping should not change anything + UI.crop( + image, + "w", + naturalWidth, + naturalHeight, + [-initialWidth / 3, 0], + true, + ); + expect(image.width).toBeCloseTo(resizedWidth, 10); + expect(image.height).toBeCloseTo(resizedHeight, 10); + + // re-crop to initial state + UI.crop(image, "w", naturalWidth, naturalHeight, [-initialWidth / 3, 0]); + // change crop height and width + UI.crop(image, "s", naturalWidth, naturalHeight, [0, -initialHeight / 2]); + UI.crop(image, "e", naturalWidth, naturalHeight, [-initialWidth / 3, 0]); + + resizedWidth = image.width; + resizedHeight = image.height; + + // test corner handle aspect ratio preserving + UI.crop(image, "se", naturalWidth, naturalHeight, [initialWidth, 0], true); + expect(image.width / image.height).toBe(resizedWidth / resizedHeight); + expect(image.width).toBeLessThanOrEqual(initialWidth + 0.0001); + expect(image.height).toBeLessThanOrEqual(initialHeight + 0.0001); + + // reset + image = API.createElement({ type: "image", width: 200, height: 100 }); + API.setElements([image]); + API.setAppState({ + selectedElementIds: { + [image.id]: true, + }, + }); + + // 50 x 50 square + UI.crop(image, "nw", naturalWidth, naturalHeight, [150, 50]); + UI.crop(image, "n", naturalWidth, naturalHeight, [0, -100], true); + expect(image.width).toBeCloseTo(image.height); + // image is at the corner, not space to its right to expand, should not be able to resize + expect(image.height).toBeCloseTo(50); + + UI.crop(image, "nw", naturalWidth, naturalHeight, [-150, -100], true); + expect(image.width).toBeCloseTo(image.height); + // max height should be reached + expect(image.height).toBeCloseTo(initialHeight); + expect(image.width).toBeCloseTo(initialHeight); + }); +}); + +describe("Cropping and other features", async () => { + it("Cropping works independently of duplication", async () => { + const image = h.elements[0] as ExcalidrawImageElement; + const initialWidth = image.width; + const initialHeight = image.height; + + const { naturalWidth, naturalHeight } = + generateRandomNaturalWidthAndHeight(image); + + UI.crop(image, "nw", naturalWidth, naturalHeight, [ + initialWidth / 2, + initialHeight / 2, + ]); + Keyboard.keyDown(KEYS.ESCAPE); + const duplicatedImage = duplicateElement(null, new Map(), image, {}); + act(() => { + h.app.scene.insertElement(duplicatedImage); + }); + + expect(duplicatedImage.width).toBe(image.width); + expect(duplicatedImage.height).toBe(image.height); + + UI.crop(duplicatedImage, "nw", naturalWidth, naturalHeight, [ + -initialWidth / 2, + -initialHeight / 2, + ]); + expect(duplicatedImage.width).toBe(initialWidth); + expect(duplicatedImage.height).toBe(initialHeight); + const resizedWidth = image.width; + const resizedHeight = image.height; + + expect(image.width).not.toBe(duplicatedImage.width); + expect(image.height).not.toBe(duplicatedImage.height); + UI.crop(duplicatedImage, "se", naturalWidth, naturalHeight, [ + -initialWidth / 1.5, + -initialHeight / 1.5, + ]); + expect(duplicatedImage.width).not.toBe(initialWidth); + expect(image.width).toBe(resizedWidth); + expect(duplicatedImage.height).not.toBe(initialHeight); + expect(image.height).toBe(resizedHeight); + }); + + it("Resizing should not affect crop", async () => { + const image = h.elements[0] as ExcalidrawImageElement; + const initialWidth = image.width; + const initialHeight = image.height; + + const { naturalWidth, naturalHeight } = + generateRandomNaturalWidthAndHeight(image); + + UI.crop(image, "nw", naturalWidth, naturalHeight, [ + initialWidth / 2, + initialHeight / 2, + ]); + const cropBeforeResizing = image.crop; + const cropBeforeResizingCloned = cloneJSON(image.crop) as ImageCrop; + expect(cropBeforeResizing).not.toBe(null); + + UI.crop(image, "e", naturalWidth, naturalHeight, [200, 0]); + expect(cropBeforeResizing).toBe(image.crop); + compareCrops(cropBeforeResizingCloned, image.crop!); + + UI.resize(image, "s", [0, -100]); + expect(cropBeforeResizing).toBe(image.crop); + compareCrops(cropBeforeResizingCloned, image.crop!); + + UI.resize(image, "ne", [-50, -50]); + expect(cropBeforeResizing).toBe(image.crop); + compareCrops(cropBeforeResizingCloned, image.crop!); + }); + + it("Flipping does not change crop", async () => { + const image = h.elements[0] as ExcalidrawImageElement; + const initialWidth = image.width; + const initialHeight = image.height; + + const { naturalWidth, naturalHeight } = + generateRandomNaturalWidthAndHeight(image); + + mouse.doubleClickOn(image); + expect(h.state.croppingElementId).not.toBe(null); + UI.crop(image, "nw", naturalWidth, naturalHeight, [ + initialWidth / 2, + initialHeight / 2, + ]); + Keyboard.keyDown(KEYS.ESCAPE); + const cropBeforeResizing = image.crop; + const cropBeforeResizingCloned = cloneJSON(image.crop) as ImageCrop; + + API.executeAction(actionFlipHorizontal); + expect(image.crop).toBe(cropBeforeResizing); + compareCrops(cropBeforeResizingCloned, image.crop!); + + API.executeAction(actionFlipVertical); + expect(image.crop).toBe(cropBeforeResizing); + compareCrops(cropBeforeResizingCloned, image.crop!); + }); + + it("Exports should preserve crops", async () => { + const image = h.elements[0] as ExcalidrawImageElement; + const initialWidth = image.width; + const initialHeight = image.height; + + const { naturalWidth, naturalHeight } = + generateRandomNaturalWidthAndHeight(image); + + mouse.doubleClickOn(image); + expect(h.state.croppingElementId).not.toBe(null); + UI.crop(image, "nw", naturalWidth, naturalHeight, [ + initialWidth / 2, + initialHeight / 4, + ]); + Keyboard.keyDown(KEYS.ESCAPE); + const widthToHeightRatio = image.width / image.height; + + const canvas = await exportToCanvas({ + elements: [image], + // @ts-ignore + appState: h.state, + files: h.app.files, + exportPadding: 0, + }); + const exportedCanvasRatio = canvas.width / canvas.height; + + expect(widthToHeightRatio).toBeCloseTo(exportedCanvasRatio); + + const svg = await exportToSvg({ + elements: [image], + // @ts-ignore + appState: h.state, + files: h.app.files, + exportPadding: 0, + }); + const svgWidth = svg.getAttribute("width"); + const svgHeight = svg.getAttribute("height"); + + expect(svgWidth).toBeDefined(); + expect(svgHeight).toBeDefined(); + + const exportedSvgRatio = Number(svgWidth) / Number(svgHeight); + expect(widthToHeightRatio).toBeCloseTo(exportedSvgRatio); + }); +}); diff --git a/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap b/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap new file mode 100644 index 0000000..7dd0c01 --- /dev/null +++ b/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap @@ -0,0 +1,397 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`restoreElements > should restore arrow element correctly 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [], + "customData": undefined, + "elbowed": false, + "endArrowhead": null, + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id-arrow01", + "index": "a0", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 100, + 100, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "seed": Any<Number>, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 2, + "versionNonce": Any<Number>, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`restoreElements > should restore correctly with rectangle, ellipse and diamond elements 1`] = ` +{ + "angle": 0, + "backgroundColor": "blue", + "boundElements": [], + "customData": undefined, + "fillStyle": "cross-hatch", + "frameId": null, + "groupIds": [ + "1", + "2", + "3", + ], + "height": 200, + "id": "1", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 10, + "roughness": 2, + "roundness": { + "type": 3, + }, + "seed": Any<Number>, + "strokeColor": "red", + "strokeStyle": "dashed", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "versionNonce": Any<Number>, + "width": 100, + "x": 10, + "y": 20, +} +`; + +exports[`restoreElements > should restore correctly with rectangle, ellipse and diamond elements 2`] = ` +{ + "angle": 0, + "backgroundColor": "blue", + "boundElements": [], + "customData": undefined, + "fillStyle": "cross-hatch", + "frameId": null, + "groupIds": [ + "1", + "2", + "3", + ], + "height": 200, + "id": "2", + "index": "a1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 10, + "roughness": 2, + "roundness": { + "type": 3, + }, + "seed": Any<Number>, + "strokeColor": "red", + "strokeStyle": "dashed", + "strokeWidth": 2, + "type": "ellipse", + "updated": 1, + "version": 2, + "versionNonce": Any<Number>, + "width": 100, + "x": 10, + "y": 20, +} +`; + +exports[`restoreElements > should restore correctly with rectangle, ellipse and diamond elements 3`] = ` +{ + "angle": 0, + "backgroundColor": "blue", + "boundElements": [], + "customData": undefined, + "fillStyle": "cross-hatch", + "frameId": null, + "groupIds": [ + "1", + "2", + "3", + ], + "height": 200, + "id": "3", + "index": "a2", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 10, + "roughness": 2, + "roundness": { + "type": 3, + }, + "seed": Any<Number>, + "strokeColor": "red", + "strokeStyle": "dashed", + "strokeWidth": 2, + "type": "diamond", + "updated": 1, + "version": 2, + "versionNonce": Any<Number>, + "width": 100, + "x": 10, + "y": 20, +} +`; + +exports[`restoreElements > should restore freedraw element correctly 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id-freedraw01", + "index": "a0", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 10, + 10, + ], + ], + "pressures": [], + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": Any<Number>, + "simulatePressure": true, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "freedraw", + "updated": 1, + "version": 2, + "versionNonce": Any<Number>, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`restoreElements > should restore line and draw elements correctly 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [], + "customData": undefined, + "endArrowhead": null, + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id-line01", + "index": "a0", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 100, + 100, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "seed": Any<Number>, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "line", + "updated": 1, + "version": 2, + "versionNonce": Any<Number>, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`restoreElements > should restore line and draw elements correctly 2`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [], + "customData": undefined, + "endArrowhead": null, + "endBinding": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id-draw01", + "index": "a1", + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 100, + 100, + ], + ], + "roughness": 1, + "roundness": { + "type": 2, + }, + "seed": Any<Number>, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "line", + "updated": 1, + "version": 2, + "versionNonce": Any<Number>, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`restoreElements > should restore text element correctly passing value for each attribute 1`] = ` +{ + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": [], + "containerId": null, + "customData": undefined, + "fillStyle": "solid", + "fontFamily": 1, + "fontSize": 14, + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id-text01", + "index": "a0", + "isDeleted": false, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "text", + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": Any<Number>, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "text", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any<Number>, + "verticalAlign": "middle", + "width": 100, + "x": -20, + "y": "-8.75000", +} +`; + +exports[`restoreElements > should restore text element correctly with unknown font family, null text and undefined alignment 1`] = ` +{ + "angle": 0, + "autoResize": true, + "backgroundColor": "transparent", + "boundElements": [], + "containerId": null, + "customData": undefined, + "fillStyle": "solid", + "font": "10 unknown", + "fontFamily": 5, + "fontSize": 10, + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id-text01", + "index": "a0", + "isDeleted": true, + "lineHeight": "1.25000", + "link": null, + "locked": false, + "opacity": 100, + "originalText": "", + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": Any<Number>, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "text": "", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 3, + "versionNonce": Any<Number>, + "verticalAlign": "top", + "width": 100, + "x": 0, + "y": 0, +} +`; diff --git a/packages/excalidraw/tests/data/reconcile.test.ts b/packages/excalidraw/tests/data/reconcile.test.ts new file mode 100644 index 0000000..f0e8105 --- /dev/null +++ b/packages/excalidraw/tests/data/reconcile.test.ts @@ -0,0 +1,382 @@ +import type { RemoteExcalidrawElement } from "../../data/reconcile"; +import { reconcileElements } from "../../data/reconcile"; +import type { + ExcalidrawElement, + OrderedExcalidrawElement, +} from "../../element/types"; +import { syncInvalidIndices } from "../../fractionalIndex"; +import { randomInteger } from "../../random"; +import type { AppState } from "../../types"; +import { cloneJSON } from "../../utils"; + +type Id = string; +type ElementLike = { + id: string; + version: number; + versionNonce: number; + index: string; +}; + +type Cache = Record<string, ExcalidrawElement | undefined>; + +const createElement = (opts: { uid: string } | ElementLike) => { + let uid: string; + let id: string; + let version: number | null; + let versionNonce: number | null = null; + if ("uid" in opts) { + const match = opts.uid.match(/^(\w+)(?::(\d+))?$/)!; + id = match[1]; + version = match[2] ? parseInt(match[2]) : null; + uid = version ? `${id}:${version}` : id; + } else { + ({ id, version, versionNonce } = opts); + uid = id; + } + return { + uid, + id, + version, + versionNonce: versionNonce || randomInteger(), + }; +}; + +const idsToElements = (ids: (Id | ElementLike)[], cache: Cache = {}) => { + return syncInvalidIndices( + ids.reduce((acc, _uid) => { + const { uid, id, version, versionNonce } = createElement( + typeof _uid === "string" ? { uid: _uid } : _uid, + ); + const cached = cache[uid]; + const elem = { + id, + version: version ?? 0, + versionNonce, + ...cached, + } as ExcalidrawElement; + // @ts-ignore + cache[uid] = elem; + acc.push(elem); + return acc; + }, [] as ExcalidrawElement[]), + ); +}; + +const test = <U extends `${string}:${"L" | "R"}`>( + local: (Id | ElementLike)[], + remote: (Id | ElementLike)[], + target: U[], +) => { + const cache: Cache = {}; + const _local = idsToElements(local, cache); + const _remote = idsToElements(remote, cache); + + const reconciled = reconcileElements( + cloneJSON(_local), + cloneJSON(_remote) as RemoteExcalidrawElement[], + {} as AppState, + ); + + const reconciledIds = reconciled.map((x) => x.id); + const reconciledIndices = reconciled.map((x) => x.index); + + expect(target.length).toEqual(reconciled.length); + expect(reconciledIndices.length).toEqual( + new Set([...reconciledIndices]).size, + ); // expect no duplicated indices + assert.deepEqual( + reconciledIds, + target.map((uid) => { + const [, id, source] = uid.match(/^(\w+):([LR])$/)!; + const element = (source === "L" ? _local : _remote).find( + (e) => e.id === id, + )!; + + return element.id; + }), + "remote reconciliation", + ); + + // convergent reconciliation on the remote client + try { + assert.deepEqual( + reconcileElements( + cloneJSON(_remote), + cloneJSON(_local as RemoteExcalidrawElement[]), + {} as AppState, + ).map((x) => x.id), + reconciledIds, + "convergent reconciliation", + ); + } catch (error: any) { + console.error("local original", _remote); + console.error("remote original", _local); + throw error; + } + + // bidirectional re-reconciliation on remote client + try { + assert.deepEqual( + reconcileElements( + cloneJSON(_remote), + cloneJSON(reconciled as unknown as RemoteExcalidrawElement[]), + {} as AppState, + ).map((x) => x.id), + reconciledIds, + "local re-reconciliation", + ); + } catch (error: any) { + console.error("local original", _remote); + console.error("remote reconciled", reconciled); + throw error; + } +}; + +describe("elements reconciliation", () => { + it("reconcileElements()", () => { + // ------------------------------------------------------------------------- + // + // in following tests, we pass: + // (1) an array of local elements and their version (:1, :2...) + // (2) an array of remote elements and their version (:1, :2...) + // (3) expected reconciled elements + // + // in the reconciled array: + // :L means local element was resolved + // :R means remote element was resolved + // + // if versions are missing, it defaults to version 0 + // ------------------------------------------------------------------------- + + test(["A:1", "B:1", "C:1"], ["B:2"], ["A:L", "B:R", "C:L"]); + test(["A:1", "B:1", "C"], ["B:2", "A:2"], ["B:R", "A:R", "C:L"]); + test(["A:2", "B:1", "C"], ["B:2", "A:1"], ["A:L", "B:R", "C:L"]); + test(["A:1", "C:1"], ["B:1"], ["A:L", "B:R", "C:L"]); + test(["A", "B"], ["A:1"], ["A:R", "B:L"]); + test(["A"], ["A", "B"], ["A:L", "B:R"]); + test(["A"], ["A:1", "B"], ["A:R", "B:R"]); + test(["A:2"], ["A:1", "B"], ["A:L", "B:R"]); + test(["A:2"], ["B", "A:1"], ["A:L", "B:R"]); + test(["A:1"], ["B", "A:2"], ["B:R", "A:R"]); + test(["A"], ["A:1"], ["A:R"]); + test(["A", "B:1", "D"], ["B", "C:2", "A"], ["C:R", "A:R", "B:L", "D:L"]); + + // some of the following tests are kinda arbitrary and they're less + // likely to happen in real-world cases + test(["A", "B"], ["B:1", "A:1"], ["B:R", "A:R"]); + test(["A:2", "B:2"], ["B:1", "A:1"], ["A:L", "B:L"]); + test(["A", "B", "C"], ["A", "B:2", "G", "C"], ["A:L", "B:R", "G:R", "C:L"]); + test(["A", "B", "C"], ["A", "B:2", "G"], ["A:R", "B:R", "C:L", "G:R"]); + test( + ["A:2", "B:2", "C"], + ["D", "B:1", "A:3"], + ["D:R", "B:L", "A:R", "C:L"], + ); + test( + ["A:2", "B:2", "C"], + ["D", "B:2", "A:3", "C"], + ["D:R", "B:L", "A:R", "C:L"], + ); + test( + ["A", "B", "C", "D", "E", "F"], + ["A", "B:2", "X", "E:2", "F", "Y"], + ["A:L", "B:R", "X:R", "C:L", "E:R", "D:L", "F:L", "Y:R"], + ); + + // fractional elements (previously annotated) + test( + ["A", "B", "C"], + ["A", "B", "X", "Y", "Z"], + ["A:R", "B:R", "C:L", "X:R", "Y:R", "Z:R"], + ); + + test(["A"], ["X", "Y"], ["A:L", "X:R", "Y:R"]); + test(["A"], ["X", "Y", "Z"], ["A:L", "X:R", "Y:R", "Z:R"]); + test(["A", "B"], ["C", "D", "F"], ["A:L", "C:R", "B:L", "D:R", "F:R"]); + + test( + ["A", "B", "C", "D"], + ["C:1", "B", "D:1"], + ["A:L", "C:R", "B:L", "D:R"], + ); + test( + ["A", "B", "C"], + ["X", "A", "Y", "B", "Z"], + ["X:R", "A:R", "Y:R", "B:L", "C:L", "Z:R"], + ); + test( + ["B", "A", "C"], + ["X", "A", "Y", "B", "Z"], + ["X:R", "A:R", "C:L", "Y:R", "B:R", "Z:R"], + ); + test(["A", "B"], ["A", "X", "Y"], ["A:R", "B:L", "X:R", "Y:R"]); + test( + ["A", "B", "C", "D", "E"], + ["A", "X", "C", "Y", "D", "Z"], + ["A:R", "B:L", "X:R", "C:R", "Y:R", "D:R", "E:L", "Z:R"], + ); + test( + ["X", "Y", "Z"], + ["A", "B", "C"], + ["A:R", "X:L", "B:R", "Y:L", "C:R", "Z:L"], + ); + test( + ["X", "Y", "Z"], + ["A", "B", "C", "X", "D", "Y", "Z"], + ["A:R", "B:R", "C:R", "X:L", "D:R", "Y:L", "Z:L"], + ); + test( + ["A", "B", "C", "D", "E"], + ["C", "X", "A", "Y", "D", "E:1"], + ["B:L", "C:L", "X:R", "A:R", "Y:R", "D:R", "E:R"], + ); + test( + ["C:1", "B", "D:1"], + ["A", "B", "C:1", "D:1"], + ["A:R", "B:R", "C:R", "D:R"], + ); + + test( + ["C:1", "B", "D:1"], + ["A", "B", "C:2", "D:1"], + ["A:R", "B:L", "C:R", "D:L"], + ); + + test( + ["A", "B", "C", "D"], + ["A", "C:1", "B", "D:1"], + ["A:L", "C:R", "B:L", "D:R"], + ); + + test( + ["A", "B", "C", "D"], + ["C", "X", "B", "Y", "A", "Z"], + ["C:R", "D:L", "X:R", "B:R", "Y:R", "A:R", "Z:R"], + ); + + test( + ["A", "B", "C", "D"], + ["A", "B:1", "C:1"], + ["A:R", "B:R", "C:R", "D:L"], + ); + + test( + ["A", "B", "C", "D"], + ["A", "C:1", "B:1"], + ["A:R", "C:R", "B:R", "D:L"], + ); + + test( + ["A", "B", "C", "D"], + ["A", "C:1", "B", "D:1"], + ["A:R", "C:R", "B:R", "D:R"], + ); + + test(["A:1", "B:1", "C"], ["B:2"], ["A:L", "B:R", "C:L"]); + test(["A:1", "B:1", "C"], ["B:2", "C:2"], ["A:L", "B:R", "C:R"]); + test(["A", "B"], ["A", "C", "B", "D"], ["A:R", "C:R", "B:R", "D:R"]); + test(["A", "B"], ["B", "C", "D"], ["A:L", "B:R", "C:R", "D:R"]); + test(["A", "B"], ["C", "D"], ["A:L", "C:R", "B:L", "D:R"]); + test(["A", "B"], ["A", "B:1"], ["A:L", "B:R"]); + test(["A:2", "B"], ["A", "B:1"], ["A:L", "B:R"]); + test(["A:2", "B:2"], ["B:1"], ["A:L", "B:L"]); + test(["A:2", "B:2"], ["B:1", "C"], ["A:L", "B:L", "C:R"]); + test(["A:2", "B:2"], ["A", "C", "B:1"], ["A:L", "B:L", "C:R"]); + + // concurrent convergency + test(["A", "B", "C"], ["A", "B", "D"], ["A:R", "B:R", "C:L", "D:R"]); + test(["A", "B", "E"], ["A", "B", "D"], ["A:R", "B:R", "D:R", "E:L"]); + test( + ["A", "B", "C"], + ["A", "B", "D", "E"], + ["A:R", "B:R", "C:L", "D:R", "E:R"], + ); + test( + ["A", "B", "E"], + ["A", "B", "D", "C"], + ["A:R", "B:R", "D:R", "E:L", "C:R"], + ); + test(["A", "B"], ["B", "D"], ["A:L", "B:R", "D:R"]); + test(["C", "A", "B"], ["C", "B", "D"], ["C:R", "A:L", "B:R", "D:R"]); + }); + + it("test identical elements reconciliation", () => { + const testIdentical = ( + local: ElementLike[], + remote: ElementLike[], + expected: Id[], + ) => { + const ret = reconcileElements( + local as unknown as OrderedExcalidrawElement[], + remote as unknown as RemoteExcalidrawElement[], + {} as AppState, + ); + + if (new Set(ret.map((x) => x.id)).size !== ret.length) { + throw new Error("reconcileElements: duplicate elements found"); + } + + assert.deepEqual( + ret.map((x) => x.id), + expected, + ); + }; + + // identical id/version/versionNonce/index + // ------------------------------------------------------------------------- + + testIdentical( + [{ id: "A", version: 1, versionNonce: 1, index: "a0" }], + [{ id: "A", version: 1, versionNonce: 1, index: "a0" }], + ["A"], + ); + testIdentical( + [ + { id: "A", version: 1, versionNonce: 1, index: "a0" }, + { id: "B", version: 1, versionNonce: 1, index: "a0" }, + ], + [ + { id: "B", version: 1, versionNonce: 1, index: "a0" }, + { id: "A", version: 1, versionNonce: 1, index: "a0" }, + ], + ["A", "B"], + ); + + // actually identical (arrays and element objects) + // ------------------------------------------------------------------------- + + const elements1 = [ + { + id: "A", + version: 1, + versionNonce: 1, + index: "a0", + }, + { + id: "B", + version: 1, + versionNonce: 1, + index: "a0", + }, + ]; + + testIdentical(elements1, elements1, ["A", "B"]); + testIdentical(elements1, elements1.slice(), ["A", "B"]); + testIdentical(elements1.slice(), elements1, ["A", "B"]); + testIdentical(elements1.slice(), elements1.slice(), ["A", "B"]); + + const el1 = { + id: "A", + version: 1, + versionNonce: 1, + index: "a0", + }; + const el2 = { + id: "B", + version: 1, + versionNonce: 1, + index: "a0", + }; + testIdentical([el1, el2], [el2, el1], ["A", "B"]); + }); +}); diff --git a/packages/excalidraw/tests/data/restore.test.ts b/packages/excalidraw/tests/data/restore.test.ts new file mode 100644 index 0000000..37a27ac --- /dev/null +++ b/packages/excalidraw/tests/data/restore.test.ts @@ -0,0 +1,815 @@ +import * as restore from "../../data/restore"; +import type { + ExcalidrawElement, + ExcalidrawFreeDrawElement, + ExcalidrawLinearElement, + ExcalidrawTextElement, +} from "../../element/types"; +import * as sizeHelpers from "../../element/sizeHelpers"; +import { API } from "../helpers/api"; +import { getDefaultAppState } from "../../appState"; +import type { ImportedDataState } from "../../data/types"; +import type { NormalizedZoomValue } from "../../types"; +import { DEFAULT_SIDEBAR, FONT_FAMILY, ROUNDNESS } from "../../constants"; +import { newElementWith } from "../../element/mutateElement"; +import { vi } from "vitest"; +import { pointFrom } from "@excalidraw/math"; + +describe("restoreElements", () => { + const mockSizeHelper = vi.spyOn(sizeHelpers, "isInvisiblySmallElement"); + + beforeEach(() => { + mockSizeHelper.mockReset(); + }); + + afterAll(() => { + mockSizeHelper.mockRestore(); + }); + + it("should return empty array when element is null", () => { + expect(restore.restoreElements(null, null)).toStrictEqual([]); + }); + + it("should not call isInvisiblySmallElement when element is a selection element", () => { + const selectionEl = { type: "selection" } as ExcalidrawElement; + const restoreElements = restore.restoreElements([selectionEl], null); + expect(restoreElements.length).toBe(0); + expect(sizeHelpers.isInvisiblySmallElement).toBeCalledTimes(0); + }); + + it("should return empty array when input type is not supported", () => { + const dummyNotSupportedElement: any = API.createElement({ + type: "text", + }); + + dummyNotSupportedElement.type = "not supported"; + expect( + restore.restoreElements([dummyNotSupportedElement], null).length, + ).toBe(0); + }); + + it("should return empty array when isInvisiblySmallElement is true", () => { + const rectElement = API.createElement({ type: "rectangle" }); + mockSizeHelper.mockImplementation(() => true); + + expect(restore.restoreElements([rectElement], null).length).toBe(0); + }); + + it("should restore text element correctly passing value for each attribute", () => { + const textElement = API.createElement({ + type: "text", + fontSize: 14, + fontFamily: FONT_FAMILY.Virgil, + text: "text", + textAlign: "center", + verticalAlign: "middle", + id: "id-text01", + }); + + const restoredText = restore.restoreElements( + [textElement], + null, + )[0] as ExcalidrawTextElement; + + expect(restoredText).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + }); + }); + + it("should restore text element correctly with unknown font family, null text and undefined alignment", () => { + const textElement: any = API.createElement({ + type: "text", + textAlign: undefined, + verticalAlign: undefined, + id: "id-text01", + }); + + textElement.text = null; + textElement.font = "10 unknown"; + + expect(textElement.isDeleted).toBe(false); + const restoredText = restore.restoreElements( + [textElement], + null, + )[0] as ExcalidrawTextElement; + expect(restoredText.isDeleted).toBe(true); + expect(restoredText).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + }); + }); + + it("should restore freedraw element correctly", () => { + const freedrawElement = API.createElement({ + type: "freedraw", + id: "id-freedraw01", + points: [pointFrom(0, 0), pointFrom(10, 10)], + }); + + const restoredFreedraw = restore.restoreElements( + [freedrawElement], + null, + )[0] as ExcalidrawFreeDrawElement; + + expect(restoredFreedraw).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + }); + }); + + it("should restore line and draw elements correctly", () => { + const lineElement = API.createElement({ type: "line", id: "id-line01" }); + + const drawElement: any = API.createElement({ + type: "line", + id: "id-draw01", + }); + drawElement.type = "draw"; + + const restoredElements = restore.restoreElements( + [lineElement, drawElement], + null, + ); + + const restoredLine = restoredElements[0] as ExcalidrawLinearElement; + const restoredDraw = restoredElements[1] as ExcalidrawLinearElement; + + expect(restoredLine).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + }); + expect(restoredDraw).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + }); + }); + + it("should restore arrow element correctly", () => { + const arrowElement = API.createElement({ type: "arrow", id: "id-arrow01" }); + + const restoredElements = restore.restoreElements([arrowElement], null); + + const restoredArrow = restoredElements[0] as ExcalidrawLinearElement; + + expect(restoredArrow).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + }); + }); + + it('should set arrow element endArrowHead as "arrow" when arrow element endArrowHead is null', () => { + const arrowElement = API.createElement({ type: "arrow" }); + const restoredElements = restore.restoreElements([arrowElement], null); + + const restoredArrow = restoredElements[0] as ExcalidrawLinearElement; + + expect(arrowElement.endArrowhead).toBe(restoredArrow.endArrowhead); + }); + + it('should set arrow element endArrowHead as "arrow" when arrow element endArrowHead is undefined', () => { + const arrowElement = API.createElement({ type: "arrow" }); + Object.defineProperty(arrowElement, "endArrowhead", { + get: vi.fn(() => undefined), + }); + + const restoredElements = restore.restoreElements([arrowElement], null); + + const restoredArrow = restoredElements[0] as ExcalidrawLinearElement; + + expect(restoredArrow.endArrowhead).toBe("arrow"); + }); + + it("when element.points of a line element is not an array", () => { + const lineElement: any = API.createElement({ + type: "line", + width: 100, + height: 200, + }); + + lineElement.points = "not an array"; + + const expectedLinePoints = [ + [0, 0], + [lineElement.width, lineElement.height], + ]; + + const restoredLine = restore.restoreElements( + [lineElement], + null, + )[0] as ExcalidrawLinearElement; + + expect(restoredLine.points).toMatchObject(expectedLinePoints); + }); + + it("when the number of points of a line is greater or equal 2", () => { + const lineElement_0 = API.createElement({ + type: "line", + width: 100, + height: 200, + x: 10, + y: 20, + }); + const lineElement_1 = API.createElement({ + type: "line", + width: 200, + height: 400, + x: 30, + y: 40, + }); + + const pointsEl_0 = [ + [0, 0], + [1, 1], + ]; + Object.defineProperty(lineElement_0, "points", { + get: vi.fn(() => pointsEl_0), + }); + + const pointsEl_1 = [ + [3, 4], + [5, 6], + ]; + Object.defineProperty(lineElement_1, "points", { + get: vi.fn(() => pointsEl_1), + }); + + const restoredElements = restore.restoreElements( + [lineElement_0, lineElement_1], + null, + ); + + const restoredLine_0 = restoredElements[0] as ExcalidrawLinearElement; + const restoredLine_1 = restoredElements[1] as ExcalidrawLinearElement; + + expect(restoredLine_0.points).toMatchObject(pointsEl_0); + + const offsetX = pointsEl_1[0][0]; + const offsetY = pointsEl_1[0][1]; + const restoredPointsEl1 = [ + [pointsEl_1[0][0] - offsetX, pointsEl_1[0][1] - offsetY], + [pointsEl_1[1][0] - offsetX, pointsEl_1[1][1] - offsetY], + ]; + expect(restoredLine_1.points).toMatchObject(restoredPointsEl1); + expect(restoredLine_1.x).toBe(lineElement_1.x + offsetX); + expect(restoredLine_1.y).toBe(lineElement_1.y + offsetY); + }); + + it("should restore correctly with rectangle, ellipse and diamond elements", () => { + const types = ["rectangle", "ellipse", "diamond"]; + + const elements: ExcalidrawElement[] = []; + let idCount = 0; + types.forEach((elType) => { + idCount += 1; + const element = API.createElement({ + type: elType as "rectangle" | "ellipse" | "diamond" | "embeddable", + id: idCount.toString(), + fillStyle: "cross-hatch", + strokeWidth: 2, + strokeStyle: "dashed", + roughness: 2, + opacity: 10, + x: 10, + y: 20, + strokeColor: "red", + backgroundColor: "blue", + width: 100, + height: 200, + groupIds: ["1", "2", "3"], + roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS }, + }); + + elements.push(element); + }); + + const restoredElements = restore.restoreElements(elements, null); + + expect(restoredElements[0]).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + }); + expect(restoredElements[1]).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + }); + expect(restoredElements[2]).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + }); + }); + + it("bump versions of local duplicate elements when supplied", () => { + const rectangle = API.createElement({ type: "rectangle" }); + const ellipse = API.createElement({ type: "ellipse" }); + const rectangle_modified = newElementWith(rectangle, { isDeleted: true }); + + const restoredElements = restore.restoreElements( + [rectangle, ellipse], + [rectangle_modified], + ); + + expect(restoredElements[0].id).toBe(rectangle.id); + expect(restoredElements[0].versionNonce).not.toBe(rectangle.versionNonce); + expect(restoredElements).toEqual([ + expect.objectContaining({ + id: rectangle.id, + version: rectangle_modified.version + 2, + }), + expect.objectContaining({ + id: ellipse.id, + version: ellipse.version + 1, + }), + ]); + }); +}); + +describe("restoreAppState", () => { + it("should restore with imported data", () => { + const stubImportedAppState = getDefaultAppState(); + stubImportedAppState.activeTool.type = "selection"; + stubImportedAppState.cursorButton = "down"; + stubImportedAppState.name = "imported app state"; + + const stubLocalAppState = getDefaultAppState(); + stubLocalAppState.activeTool.type = "rectangle"; + stubLocalAppState.cursorButton = "up"; + stubLocalAppState.name = "local app state"; + + const restoredAppState = restore.restoreAppState( + stubImportedAppState, + stubLocalAppState, + ); + expect(restoredAppState.activeTool).toEqual( + stubImportedAppState.activeTool, + ); + expect(restoredAppState.cursorButton).toBe("up"); + expect(restoredAppState.name).toBe(stubImportedAppState.name); + }); + + it("should restore with current app state when imported data state is undefined", () => { + const stubImportedAppState = { + ...getDefaultAppState(), + cursorButton: undefined, + name: undefined, + }; + + const stubLocalAppState = getDefaultAppState(); + stubLocalAppState.cursorButton = "down"; + stubLocalAppState.name = "local app state"; + + const restoredAppState = restore.restoreAppState( + stubImportedAppState, + stubLocalAppState, + ); + expect(restoredAppState.cursorButton).toBe(stubLocalAppState.cursorButton); + expect(restoredAppState.name).toBe(stubLocalAppState.name); + }); + + it("should return imported data when local app state is null", () => { + const stubImportedAppState = getDefaultAppState(); + stubImportedAppState.cursorButton = "down"; + stubImportedAppState.name = "imported app state"; + + const restoredAppState = restore.restoreAppState( + stubImportedAppState, + null, + ); + expect(restoredAppState.cursorButton).toBe("up"); + expect(restoredAppState.name).toBe(stubImportedAppState.name); + }); + + it("should return local app state when imported data state is null", () => { + const stubLocalAppState = getDefaultAppState(); + stubLocalAppState.cursorButton = "down"; + stubLocalAppState.name = "local app state"; + + const restoredAppState = restore.restoreAppState(null, stubLocalAppState); + expect(restoredAppState.cursorButton).toBe(stubLocalAppState.cursorButton); + expect(restoredAppState.name).toBe(stubLocalAppState.name); + }); + + it("should return default app state when imported data state and local app state are undefined", () => { + const stubImportedAppState = { + ...getDefaultAppState(), + cursorButton: undefined, + }; + + const stubLocalAppState = { + ...getDefaultAppState(), + cursorButton: undefined, + }; + + const restoredAppState = restore.restoreAppState( + stubImportedAppState, + stubLocalAppState, + ); + expect(restoredAppState.cursorButton).toBe( + getDefaultAppState().cursorButton, + ); + }); + + it("should return default app state when imported data state and local app state are null", () => { + const restoredAppState = restore.restoreAppState(null, null); + expect(restoredAppState.cursorButton).toBe( + getDefaultAppState().cursorButton, + ); + }); + + it("when imported data state has a not allowed Excalidraw Element Types", () => { + const stubImportedAppState: any = getDefaultAppState(); + + stubImportedAppState.activeTool = "not allowed Excalidraw Element Types"; + const stubLocalAppState = getDefaultAppState(); + + const restoredAppState = restore.restoreAppState( + stubImportedAppState, + stubLocalAppState, + ); + expect(restoredAppState.activeTool.type).toBe("selection"); + }); + + describe("with zoom in imported data state", () => { + it("when imported data state has zoom as a number", () => { + const stubImportedAppState: any = getDefaultAppState(); + + stubImportedAppState.zoom = 10; + + const stubLocalAppState = getDefaultAppState(); + + const restoredAppState = restore.restoreAppState( + stubImportedAppState, + stubLocalAppState, + ); + + expect(restoredAppState.zoom.value).toBe(10); + }); + + it("when the zoom of imported data state is not a number", () => { + const stubImportedAppState = getDefaultAppState(); + stubImportedAppState.zoom = { + value: 10 as NormalizedZoomValue, + }; + + const stubLocalAppState = getDefaultAppState(); + + const restoredAppState = restore.restoreAppState( + stubImportedAppState, + stubLocalAppState, + ); + + expect(restoredAppState.zoom.value).toBe(10); + expect(restoredAppState.zoom).toMatchObject(stubImportedAppState.zoom); + }); + + it("when the zoom of imported data state zoom is null", () => { + const stubImportedAppState = getDefaultAppState(); + + Object.defineProperty(stubImportedAppState, "zoom", { + get: vi.fn(() => null), + }); + + const stubLocalAppState = getDefaultAppState(); + + const restoredAppState = restore.restoreAppState( + stubImportedAppState, + stubLocalAppState, + ); + + expect(restoredAppState.zoom).toMatchObject(getDefaultAppState().zoom); + }); + }); + + it("should handle appState.openSidebar legacy values", () => { + expect(restore.restoreAppState({}, null).openSidebar).toBe(null); + expect( + restore.restoreAppState({ openSidebar: "library" } as any, null) + .openSidebar, + ).toEqual({ name: DEFAULT_SIDEBAR.name }); + expect( + restore.restoreAppState({ openSidebar: "xxx" } as any, null).openSidebar, + ).toEqual({ name: DEFAULT_SIDEBAR.name }); + // while "library" was our legacy sidebar name, we can't assume it's legacy + // value as it may be some host app's custom sidebar name ¯\_(ツ)_/¯ + expect( + restore.restoreAppState({ openSidebar: { name: "library" } } as any, null) + .openSidebar, + ).toEqual({ name: "library" }); + expect( + restore.restoreAppState( + { openSidebar: { name: DEFAULT_SIDEBAR.name, tab: "ola" } } as any, + null, + ).openSidebar, + ).toEqual({ name: DEFAULT_SIDEBAR.name, tab: "ola" }); + }); +}); + +describe("restore", () => { + it("when imported data state is null it should return an empty array of elements", () => { + const stubLocalAppState = getDefaultAppState(); + + const restoredData = restore.restore(null, stubLocalAppState, null); + expect(restoredData.elements.length).toBe(0); + }); + + it("when imported data state is null it should return the local app state property", () => { + const stubLocalAppState = getDefaultAppState(); + stubLocalAppState.cursorButton = "down"; + stubLocalAppState.name = "local app state"; + + const restoredData = restore.restore(null, stubLocalAppState, null); + expect(restoredData.appState.cursorButton).toBe( + stubLocalAppState.cursorButton, + ); + expect(restoredData.appState.name).toBe(stubLocalAppState.name); + }); + + it("when imported data state has elements", () => { + const stubLocalAppState = getDefaultAppState(); + + const textElement = API.createElement({ type: "text" }); + const rectElement = API.createElement({ type: "rectangle" }); + const elements = [textElement, rectElement]; + + const importedDataState = {} as ImportedDataState; + importedDataState.elements = elements; + + const restoredData = restore.restore( + importedDataState, + stubLocalAppState, + null, + ); + expect(restoredData.elements.length).toBe(elements.length); + }); + + it("when local app state is null but imported app state is supplied", () => { + const stubImportedAppState = getDefaultAppState(); + stubImportedAppState.cursorButton = "down"; + stubImportedAppState.name = "imported app state"; + + const importedDataState = {} as ImportedDataState; + importedDataState.appState = stubImportedAppState; + + const restoredData = restore.restore(importedDataState, null, null); + expect(restoredData.appState.cursorButton).toBe("up"); + expect(restoredData.appState.name).toBe(stubImportedAppState.name); + }); + + it("bump versions of local duplicate elements when supplied", () => { + const rectangle = API.createElement({ type: "rectangle" }); + const ellipse = API.createElement({ type: "ellipse" }); + + const rectangle_modified = newElementWith(rectangle, { isDeleted: true }); + + const restoredData = restore.restore( + { elements: [rectangle, ellipse] }, + null, + [rectangle_modified], + ); + + expect(restoredData.elements[0].id).toBe(rectangle.id); + expect(restoredData.elements[0].versionNonce).not.toBe( + rectangle.versionNonce, + ); + expect(restoredData.elements).toEqual([ + expect.objectContaining({ version: rectangle_modified.version + 2 }), + expect.objectContaining({ + id: ellipse.id, + version: ellipse.version + 1, + }), + ]); + }); +}); + +describe("repairing bindings", () => { + it("should repair container boundElements when repair is true", () => { + const container = API.createElement({ + type: "rectangle", + boundElements: [], + }); + const boundElement = API.createElement({ + type: "text", + containerId: container.id, + }); + + expect(container.boundElements).toEqual([]); + + let restoredElements = restore.restoreElements( + [container, boundElement], + null, + ); + + expect(restoredElements).toEqual([ + expect.objectContaining({ + id: container.id, + boundElements: [], + }), + expect.objectContaining({ + id: boundElement.id, + containerId: container.id, + }), + ]); + + restoredElements = restore.restoreElements( + [container, boundElement], + null, + { repairBindings: true }, + ); + + expect(restoredElements).toEqual([ + expect.objectContaining({ + id: container.id, + boundElements: [{ type: boundElement.type, id: boundElement.id }], + }), + expect.objectContaining({ + id: boundElement.id, + containerId: container.id, + }), + ]); + }); + + it("should repair containerId of boundElements when repair is true", () => { + const boundElement = API.createElement({ + type: "text", + containerId: null, + }); + const container = API.createElement({ + type: "rectangle", + boundElements: [{ type: boundElement.type, id: boundElement.id }], + }); + + let restoredElements = restore.restoreElements( + [container, boundElement], + null, + ); + + expect(restoredElements).toEqual([ + expect.objectContaining({ + id: container.id, + boundElements: [{ type: boundElement.type, id: boundElement.id }], + }), + expect.objectContaining({ + id: boundElement.id, + containerId: null, + }), + ]); + + restoredElements = restore.restoreElements( + [container, boundElement], + null, + { repairBindings: true }, + ); + + expect(restoredElements).toEqual([ + expect.objectContaining({ + id: container.id, + boundElements: [{ type: boundElement.type, id: boundElement.id }], + }), + expect.objectContaining({ + id: boundElement.id, + containerId: container.id, + }), + ]); + }); + + it("should ignore bound element if deleted", () => { + const container = API.createElement({ + type: "rectangle", + boundElements: [], + }); + const boundElement = API.createElement({ + type: "text", + containerId: container.id, + isDeleted: true, + }); + + expect(container.boundElements).toEqual([]); + + const restoredElements = restore.restoreElements( + [container, boundElement], + null, + ); + + expect(restoredElements).toEqual([ + expect.objectContaining({ + id: container.id, + boundElements: [], + }), + expect.objectContaining({ + id: boundElement.id, + containerId: container.id, + }), + ]); + }); + + it("should remove bindings of deleted elements from boundElements when repair is true", () => { + const container = API.createElement({ + type: "rectangle", + boundElements: [], + }); + const boundElement = API.createElement({ + type: "text", + containerId: container.id, + isDeleted: true, + }); + const invisibleBoundElement = API.createElement({ + type: "text", + containerId: container.id, + width: 0, + height: 0, + }); + + const obsoleteBinding = { type: boundElement.type, id: boundElement.id }; + const invisibleBinding = { + type: invisibleBoundElement.type, + id: invisibleBoundElement.id, + }; + expect(container.boundElements).toEqual([]); + + const nonExistentBinding = { type: "text", id: "non-existent" }; + // @ts-ignore + container.boundElements = [ + obsoleteBinding, + invisibleBinding, + nonExistentBinding, + ]; + + let restoredElements = restore.restoreElements( + [container, invisibleBoundElement, boundElement], + null, + ); + + expect(restoredElements).toEqual([ + expect.objectContaining({ + id: container.id, + boundElements: [obsoleteBinding, invisibleBinding, nonExistentBinding], + }), + expect.objectContaining({ + id: boundElement.id, + containerId: container.id, + }), + ]); + + restoredElements = restore.restoreElements( + [container, invisibleBoundElement, boundElement], + null, + { repairBindings: true }, + ); + + expect(restoredElements).toEqual([ + expect.objectContaining({ + id: container.id, + boundElements: [], + }), + expect.objectContaining({ + id: boundElement.id, + containerId: container.id, + }), + ]); + }); + + it("should remove containerId if container not exists when repair is true", () => { + const boundElement = API.createElement({ + type: "text", + containerId: "non-existent", + }); + const boundElementDeleted = API.createElement({ + type: "text", + containerId: "non-existent", + isDeleted: true, + }); + + let restoredElements = restore.restoreElements( + [boundElement, boundElementDeleted], + null, + ); + + expect(restoredElements).toEqual([ + expect.objectContaining({ + id: boundElement.id, + containerId: "non-existent", + }), + expect.objectContaining({ + id: boundElementDeleted.id, + containerId: "non-existent", + }), + ]); + + restoredElements = restore.restoreElements( + [boundElement, boundElementDeleted], + null, + { repairBindings: true }, + ); + + expect(restoredElements).toEqual([ + expect.objectContaining({ + id: boundElement.id, + containerId: null, + }), + expect.objectContaining({ + id: boundElementDeleted.id, + containerId: null, + }), + ]); + }); +}); diff --git a/packages/excalidraw/tests/dragCreate.test.tsx b/packages/excalidraw/tests/dragCreate.test.tsx new file mode 100644 index 0000000..743ba79 --- /dev/null +++ b/packages/excalidraw/tests/dragCreate.test.tsx @@ -0,0 +1,347 @@ +import React from "react"; +import { Excalidraw } from "../index"; +import * as StaticScene from "../renderer/staticScene"; +import * as InteractiveScene from "../renderer/interactiveScene"; +import { KEYS } from "../keys"; +import { + render, + fireEvent, + mockBoundingClientRect, + restoreOriginalGetBoundingClientRect, + unmountComponent, +} from "./test-utils"; +import type { ExcalidrawLinearElement } from "../element/types"; +import { reseed } from "../random"; +import { vi } from "vitest"; + +unmountComponent(); + +const renderInteractiveScene = vi.spyOn( + InteractiveScene, + "renderInteractiveScene", +); +const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene"); + +beforeEach(() => { + localStorage.clear(); + renderInteractiveScene.mockClear(); + renderStaticScene.mockClear(); + reseed(7); +}); + +const { h } = window; + +describe("Test dragCreate", () => { + describe("add element to the scene when pointer dragging long enough", () => { + it("rectangle", async () => { + const { getByToolName, container } = await render(<Excalidraw />); + // select tool + const tool = getByToolName("rectangle"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas.interactive")!; + + // start from (30, 20) + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + + // move to (60,70) + fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); + + // finish (position does not matter) + fireEvent.pointerUp(canvas); + + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( + `5`, + ); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`); + expect(h.state.selectionElement).toBeNull(); + + expect(h.elements.length).toEqual(1); + expect(h.elements[0].type).toEqual("rectangle"); + expect(h.elements[0].x).toEqual(30); + expect(h.elements[0].y).toEqual(20); + expect(h.elements[0].width).toEqual(30); // 60 - 30 + expect(h.elements[0].height).toEqual(50); // 70 - 20 + + expect(h.elements.length).toMatchSnapshot(); + h.elements.forEach((element) => expect(element).toMatchSnapshot()); + }); + + it("ellipse", async () => { + const { getByToolName, container } = await render(<Excalidraw />); + // select tool + const tool = getByToolName("ellipse"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas.interactive")!; + + // start from (30, 20) + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + + // move to (60,70) + fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); + + // finish (position does not matter) + fireEvent.pointerUp(canvas); + + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( + `5`, + ); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`); + + expect(h.state.selectionElement).toBeNull(); + + expect(h.elements.length).toEqual(1); + expect(h.elements[0].type).toEqual("ellipse"); + expect(h.elements[0].x).toEqual(30); + expect(h.elements[0].y).toEqual(20); + expect(h.elements[0].width).toEqual(30); // 60 - 30 + expect(h.elements[0].height).toEqual(50); // 70 - 20 + + expect(h.elements.length).toMatchSnapshot(); + h.elements.forEach((element) => expect(element).toMatchSnapshot()); + }); + + it("diamond", async () => { + const { getByToolName, container } = await render(<Excalidraw />); + // select tool + const tool = getByToolName("diamond"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas.interactive")!; + + // start from (30, 20) + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + + // move to (60,70) + fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); + + // finish (position does not matter) + fireEvent.pointerUp(canvas); + + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( + `5`, + ); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`); + expect(h.state.selectionElement).toBeNull(); + + expect(h.elements.length).toEqual(1); + expect(h.elements[0].type).toEqual("diamond"); + expect(h.elements[0].x).toEqual(30); + expect(h.elements[0].y).toEqual(20); + expect(h.elements[0].width).toEqual(30); // 60 - 30 + expect(h.elements[0].height).toEqual(50); // 70 - 20 + + expect(h.elements.length).toMatchSnapshot(); + h.elements.forEach((element) => expect(element).toMatchSnapshot()); + }); + + it("arrow", async () => { + const { getByToolName, container } = await render(<Excalidraw />); + // select tool + const tool = getByToolName("arrow"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas.interactive")!; + + // start from (30, 20) + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + + // move to (60,70) + fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); + + // finish (position does not matter) + fireEvent.pointerUp(canvas); + + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( + `5`, + ); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`); + expect(h.state.selectionElement).toBeNull(); + + expect(h.elements.length).toEqual(1); + + const element = h.elements[0] as ExcalidrawLinearElement; + + expect(element.type).toEqual("arrow"); + expect(element.x).toEqual(30); + expect(element.y).toEqual(20); + expect(element.points.length).toEqual(2); + expect(element.points[0]).toEqual([0, 0]); + expect(element.points[1]).toEqual([30, 50]); // (60 - 30, 70 - 20) + + expect(h.elements.length).toMatchSnapshot(); + h.elements.forEach((element) => expect(element).toMatchSnapshot()); + }); + + it("line", async () => { + const { getByToolName, container } = await render(<Excalidraw />); + // select tool + const tool = getByToolName("line"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas.interactive")!; + + // start from (30, 20) + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + + // move to (60,70) + fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); + + // finish (position does not matter) + fireEvent.pointerUp(canvas); + + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( + `5`, + ); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`); + expect(h.state.selectionElement).toBeNull(); + + expect(h.elements.length).toEqual(1); + + const element = h.elements[0] as ExcalidrawLinearElement; + + expect(element.type).toEqual("line"); + expect(element.x).toEqual(30); + expect(element.y).toEqual(20); + expect(element.points.length).toEqual(2); + expect(element.points[0]).toEqual([0, 0]); + expect(element.points[1]).toEqual([30, 50]); // (60 - 30, 70 - 20) + + h.elements.forEach((element) => expect(element).toMatchSnapshot()); + }); + }); + + describe("do not add element to the scene if size is too small", () => { + beforeAll(() => { + mockBoundingClientRect(); + }); + afterAll(() => { + restoreOriginalGetBoundingClientRect(); + }); + + it("rectangle", async () => { + const { getByToolName, container } = await render(<Excalidraw />); + // select tool + const tool = getByToolName("rectangle"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas.interactive")!; + + // start from (30, 20) + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + + // finish (position does not matter) + fireEvent.pointerUp(canvas); + + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( + `5`, + ); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`); + expect(h.state.selectionElement).toBeNull(); + expect(h.elements.length).toEqual(0); + }); + + it("ellipse", async () => { + const { getByToolName, container } = await render(<Excalidraw />); + // select tool + const tool = getByToolName("ellipse"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas.interactive")!; + + // start from (30, 20) + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + + // finish (position does not matter) + fireEvent.pointerUp(canvas); + + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( + `5`, + ); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`); + expect(h.state.selectionElement).toBeNull(); + expect(h.elements.length).toEqual(0); + }); + + it("diamond", async () => { + const { getByToolName, container } = await render(<Excalidraw />); + // select tool + const tool = getByToolName("diamond"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas.interactive")!; + + // start from (30, 20) + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + + // finish (position does not matter) + fireEvent.pointerUp(canvas); + + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( + `5`, + ); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`); + expect(h.state.selectionElement).toBeNull(); + expect(h.elements.length).toEqual(0); + }); + + it("arrow", async () => { + const { getByToolName, container } = await render( + <Excalidraw handleKeyboardGlobally={true} />, + ); + // select tool + const tool = getByToolName("arrow"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas.interactive")!; + + // start from (30, 20) + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + + // finish (position does not matter) + fireEvent.pointerUp(canvas); + + // we need to finalize it because arrows and lines enter multi-mode + fireEvent.keyDown(document, { + key: KEYS.ENTER, + }); + + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( + `6`, + ); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`); + expect(h.state.selectionElement).toBeNull(); + expect(h.elements.length).toEqual(0); + }); + + it("line", async () => { + const { getByToolName, container } = await render( + <Excalidraw handleKeyboardGlobally={true} />, + ); + // select tool + const tool = getByToolName("line"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas.interactive")!; + + // start from (30, 20) + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + + // finish (position does not matter) + fireEvent.pointerUp(canvas); + + // we need to finalize it because arrows and lines enter multi-mode + fireEvent.keyDown(document, { + key: KEYS.ENTER, + }); + + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( + `6`, + ); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`); + expect(h.state.selectionElement).toBeNull(); + expect(h.elements.length).toEqual(0); + }); + }); +}); diff --git a/packages/excalidraw/tests/elementLocking.test.tsx b/packages/excalidraw/tests/elementLocking.test.tsx new file mode 100644 index 0000000..281c268 --- /dev/null +++ b/packages/excalidraw/tests/elementLocking.test.tsx @@ -0,0 +1,388 @@ +import React from "react"; +import { Excalidraw } from "../index"; +import { render, unmountComponent } from "../tests/test-utils"; +import { Keyboard, Pointer, UI } from "../tests/helpers/ui"; +import { KEYS } from "../keys"; +import { API } from "../tests/helpers/api"; +import { actionSelectAll } from "../actions"; +import { t } from "../i18n"; +import { mutateElement } from "../element/mutateElement"; + +unmountComponent(); + +const mouse = new Pointer("mouse"); +const h = window.h; + +describe("element locking", () => { + beforeEach(async () => { + await render(<Excalidraw handleKeyboardGlobally={true} />); + API.setElements([]); + }); + + it("click-selecting a locked element is disabled", () => { + const lockedRectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + locked: true, + }); + + API.setElements([lockedRectangle]); + + mouse.clickAt(50, 50); + expect(API.getSelectedElements().length).toBe(0); + }); + + it("box-selecting a locked element is disabled", () => { + const lockedRectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + locked: true, + x: 100, + y: 100, + }); + + API.setElements([lockedRectangle]); + + mouse.downAt(50, 50); + mouse.moveTo(250, 250); + mouse.upAt(250, 250); + expect(API.getSelectedElements().length).toBe(0); + }); + + it("dragging a locked element is disabled", () => { + const lockedRectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + locked: true, + }); + + API.setElements([lockedRectangle]); + + mouse.downAt(50, 50); + mouse.moveTo(100, 100); + mouse.upAt(100, 100); + expect(lockedRectangle).toEqual(expect.objectContaining({ x: 0, y: 0 })); + }); + + it("you can drag element that's below a locked element", () => { + const rectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + }); + const lockedRectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + locked: true, + }); + + API.setElements([rectangle, lockedRectangle]); + + mouse.downAt(50, 50); + mouse.moveTo(100, 100); + mouse.upAt(100, 100); + expect(lockedRectangle).toEqual(expect.objectContaining({ x: 0, y: 0 })); + expect(rectangle).toEqual(expect.objectContaining({ x: 50, y: 50 })); + expect(API.getSelectedElements().length).toBe(1); + expect(API.getSelectedElement().id).toBe(rectangle.id); + }); + + it("selectAll shouldn't select locked elements", () => { + API.setElements([ + API.createElement({ type: "rectangle" }), + API.createElement({ type: "rectangle", locked: true }), + ]); + API.executeAction(actionSelectAll); + expect(API.getSelectedElements().length).toBe(1); + }); + + it("clicking on a locked element should select the unlocked element beneath it", () => { + const rectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + }); + const lockedRectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + locked: true, + }); + + API.setElements([rectangle, lockedRectangle]); + expect(API.getSelectedElements().length).toBe(0); + mouse.clickAt(50, 50); + expect(API.getSelectedElements().length).toBe(1); + expect(API.getSelectedElement().id).toBe(rectangle.id); + }); + + it("right-clicking on a locked element should select it & open its contextMenu", () => { + const rectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + }); + const lockedRectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + locked: true, + }); + + API.setElements([rectangle, lockedRectangle]); + expect(API.getSelectedElements().length).toBe(0); + mouse.rightClickAt(50, 50); + expect(API.getSelectedElements().length).toBe(1); + expect(API.getSelectedElement().id).toBe(lockedRectangle.id); + + const contextMenu = UI.queryContextMenu(); + expect(contextMenu).not.toBeNull(); + expect( + contextMenu?.querySelector( + `li[data-testid="toggleElementLock"] .context-menu-item__label`, + ), + ).toHaveTextContent(t("labels.elementLock.unlock")); + }); + + it("right-clicking on element covered by locked element should ignore the locked element", () => { + const rectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + }); + const lockedRectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + locked: true, + }); + + API.setElements([rectangle, lockedRectangle]); + API.setSelectedElements([rectangle]); + expect(API.getSelectedElements().length).toBe(1); + expect(API.getSelectedElement().id).toBe(rectangle.id); + mouse.rightClickAt(50, 50); + expect(API.getSelectedElements().length).toBe(1); + expect(API.getSelectedElement().id).toBe(rectangle.id); + + const contextMenu = UI.queryContextMenu(); + expect(contextMenu).not.toBeNull(); + }); + + it("selecting a group selects all elements including locked ones", () => { + const rectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + groupIds: ["g1"], + }); + const lockedRectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + locked: true, + groupIds: ["g1"], + x: 200, + y: 200, + }); + + API.setElements([rectangle, lockedRectangle]); + + mouse.clickAt(250, 250); + expect(API.getSelectedElements().length).toBe(0); + + mouse.clickAt(50, 50); + expect(API.getSelectedElements().length).toBe(2); + }); + + it("should ignore locked text element in center of container on ENTER", () => { + 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, + locked: true, + }); + API.setElements([container, text]); + API.setSelectedElements([container]); + Keyboard.keyPress(KEYS.ENTER); + expect(h.state.editingTextElement?.id).not.toBe(text.id); + expect(h.state.editingTextElement?.id).toBe(h.elements[1].id); + }); + + it("should ignore locked text under cursor when clicked with text tool", () => { + const text = API.createElement({ + type: "text", + text: "ola", + x: 60, + y: 0, + width: 100, + height: 100, + locked: true, + }); + API.setElements([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.editingTextElement?.id).not.toBe(text.id); + expect(h.elements.length).toBe(2); + expect(h.state.editingTextElement?.id).toBe(h.elements[1].id); + }); + + it("should ignore 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, + locked: true, + }); + API.setElements([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.editingTextElement?.id).not.toBe(text.id); + expect(h.elements.length).toBe(2); + expect(h.state.editingTextElement?.id).toBe(h.elements[1].id); + }); + + it("locking should include bound text", () => { + 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: [{ id: text.id, type: "text" }], + }); + + API.setElements([container, text]); + + UI.clickTool("selection"); + mouse.clickAt(container.x + 10, container.y + 10); + Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => { + Keyboard.keyPress(KEYS.L); + }); + + expect(h.elements).toEqual([ + expect.objectContaining({ + id: container.id, + locked: true, + }), + expect.objectContaining({ + id: text.id, + locked: true, + }), + ]); + }); + + it("bound text shouldn't be editable via double-click", () => { + const container = API.createElement({ + type: "rectangle", + width: 100, + locked: true, + }); + 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, + locked: true, + }); + mutateElement(container, { + boundElements: [{ id: text.id, type: "text" }], + }); + API.setElements([container, text]); + + UI.clickTool("selection"); + mouse.doubleClickAt(container.width / 2, container.height / 2); + + const editor = document.querySelector( + ".excalidraw-textEditorContainer > textarea", + ) as HTMLTextAreaElement; + expect(editor).not.toBe(null); + expect(h.state.editingTextElement?.id).not.toBe(text.id); + expect(h.elements.length).toBe(3); + expect(h.state.editingTextElement?.id).toBe(h.elements[2].id); + }); + + it("bound text shouldn't be editable via text tool", () => { + const container = API.createElement({ + type: "rectangle", + width: 100, + locked: true, + }); + 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, + locked: true, + }); + mutateElement(container, { + boundElements: [{ id: text.id, type: "text" }], + }); + API.setElements([container, text]); + + UI.clickTool("text"); + mouse.clickAt(container.width / 2, container.height / 2); + + const editor = document.querySelector( + ".excalidraw-textEditorContainer > textarea", + ) as HTMLTextAreaElement; + expect(editor).not.toBe(null); + expect(h.state.editingTextElement?.id).not.toBe(text.id); + expect(h.elements.length).toBe(3); + expect(h.state.editingTextElement?.id).toBe(h.elements[2].id); + }); +}); diff --git a/packages/excalidraw/tests/excalidraw.test.tsx b/packages/excalidraw/tests/excalidraw.test.tsx new file mode 100644 index 0000000..6fbcf2a --- /dev/null +++ b/packages/excalidraw/tests/excalidraw.test.tsx @@ -0,0 +1,409 @@ +import React from "react"; +import { fireEvent, GlobalTestState, toggleMenu, render } from "./test-utils"; +import { Excalidraw, Footer, MainMenu } from "../index"; +import { queryByText, queryByTestId } from "@testing-library/react"; +import { THEME } from "../constants"; +import { t } from "../i18n"; +import { useMemo } from "react"; + +const { h } = window; + +describe("<Excalidraw/>", () => { + afterEach(() => { + const menu = document.querySelector(".dropdown-menu"); + if (menu) { + toggleMenu(document.querySelector(".excalidraw")!); + } + }); + + describe("Test zenModeEnabled prop", () => { + it('should show exit zen mode button when zen mode is set and zen mode option in context menu when zenModeEnabled is "undefined"', async () => { + const { container } = await render(<Excalidraw />); + expect( + container.getElementsByClassName("disable-zen-mode--visible").length, + ).toBe(0); + expect(h.state.zenModeEnabled).toBe(false); + + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 1, + clientY: 1, + }); + const contextMenu = document.querySelector(".context-menu"); + fireEvent.click(queryByText(contextMenu as HTMLElement, "Zen mode")!); + expect(h.state.zenModeEnabled).toBe(true); + expect( + container.getElementsByClassName("disable-zen-mode--visible").length, + ).toBe(1); + }); + + it("should not show exit zen mode button and zen mode option in context menu when zenModeEnabled is set", async () => { + const { container } = await render(<Excalidraw zenModeEnabled={true} />); + expect( + container.getElementsByClassName("disable-zen-mode--visible").length, + ).toBe(0); + expect(h.state.zenModeEnabled).toBe(true); + + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 1, + clientY: 1, + }); + const contextMenu = document.querySelector(".context-menu"); + expect(queryByText(contextMenu as HTMLElement, "Zen mode")).toBe(null); + expect(h.state.zenModeEnabled).toBe(true); + expect( + container.getElementsByClassName("disable-zen-mode--visible").length, + ).toBe(0); + }); + }); + + it("should render the footer only when Footer is passed as children", async () => { + //Footer not passed hence it will not render the footer + let { container } = await render( + <Excalidraw> + <div>This is a custom footer</div> + </Excalidraw>, + ); + expect(container.querySelector(".footer-center")).toBe(null); + + // Footer passed hence it will render the footer + ({ container } = await render( + <Excalidraw> + <Footer> + <div>This is a custom footer</div> + </Footer> + </Excalidraw>, + )); + expect(container.querySelector(".footer-center")).toMatchInlineSnapshot( + ` + <div + class="footer-center zen-mode-transition" + > + <div> + This is a custom footer + </div> + </div> + `, + ); + }); + + describe("Test gridModeEnabled prop", () => { + it('should show grid mode in context menu when gridModeEnabled is "undefined"', async () => { + const { container } = await render(<Excalidraw />); + expect(h.state.gridModeEnabled).toBe(false); + + expect( + container.getElementsByClassName("disable-zen-mode--visible").length, + ).toBe(0); + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 1, + clientY: 1, + }); + const contextMenu = document.querySelector(".context-menu"); + fireEvent.click(queryByText(contextMenu as HTMLElement, "Toggle grid")!); + expect(h.state.gridModeEnabled).toBe(true); + }); + + it('should not show grid mode in context menu when gridModeEnabled is not "undefined"', async () => { + const { container } = await render( + <Excalidraw gridModeEnabled={false} />, + ); + expect(h.state.gridModeEnabled).toBe(false); + + expect( + container.getElementsByClassName("disable-zen-mode--visible").length, + ).toBe(0); + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 1, + clientY: 1, + }); + const contextMenu = document.querySelector(".context-menu"); + expect(queryByText(contextMenu as HTMLElement, "Show grid")).toBe(null); + expect(h.state.gridModeEnabled).toBe(false); + }); + }); + + describe("Test UIOptions prop", () => { + describe("Test canvasActions", () => { + it('should render menu with default items when "UIOPtions" is "undefined"', async () => { + const { container } = await render( + <Excalidraw UIOptions={undefined} />, + ); + //open menu + toggleMenu(container); + expect(queryByTestId(container, "dropdown-menu")).toMatchSnapshot(); + }); + + it("should hide clear canvas button when clearCanvas is false", async () => { + const { container } = await render( + <Excalidraw UIOptions={{ canvasActions: { clearCanvas: false } }} />, + ); + //open menu + toggleMenu(container); + expect(queryByTestId(container, "clear-canvas-button")).toBeNull(); + }); + + it("should hide export button when export is false", async () => { + const { container } = await render( + <Excalidraw UIOptions={{ canvasActions: { export: false } }} />, + ); + //open menu + toggleMenu(container); + expect(queryByTestId(container, "json-export-button")).toBeNull(); + }); + + it("should hide 'Save as image' button when 'saveAsImage' is false", async () => { + const { container } = await render( + <Excalidraw UIOptions={{ canvasActions: { saveAsImage: false } }} />, + ); + //open menu + toggleMenu(container); + expect(queryByTestId(container, "image-export-button")).toBeNull(); + }); + + it("should hide load button when loadScene is false", async () => { + const { container } = await render( + <Excalidraw UIOptions={{ canvasActions: { loadScene: false } }} />, + ); + + expect(queryByTestId(container, "load-button")).toBeNull(); + }); + + it("should hide save as button when saveFileToDisk is false", async () => { + const { container } = await render( + <Excalidraw + UIOptions={{ canvasActions: { export: { saveFileToDisk: false } } }} + />, + ); + //open menu + toggleMenu(container); + expect(queryByTestId(container, "save-as-button")).toBeNull(); + }); + + it("should hide save button when saveToActiveFile is false", async () => { + const { container } = await render( + <Excalidraw + UIOptions={{ canvasActions: { saveToActiveFile: false } }} + />, + ); + //open menu + toggleMenu(container); + expect(queryByTestId(container, "save-button")).toBeNull(); + }); + + it("should hide the canvas background picker when changeViewBackgroundColor is false", async () => { + const { container } = await render( + <Excalidraw + UIOptions={{ canvasActions: { changeViewBackgroundColor: false } }} + />, + ); + //open menu + toggleMenu(container); + expect(queryByTestId(container, "canvas-background-label")).toBeNull(); + expect(queryByTestId(container, "canvas-background-picker")).toBeNull(); + }); + + it("should hide the canvas background picker even if passed if the `canvasActions.changeViewBackgroundColor` is set to false", async () => { + const { container } = await render( + <Excalidraw + UIOptions={{ canvasActions: { changeViewBackgroundColor: false } }} + > + <MainMenu> + <MainMenu.DefaultItems.ChangeCanvasBackground /> + </MainMenu> + </Excalidraw>, + ); + //open menu + toggleMenu(container); + expect(queryByTestId(container, "canvas-background-label")).toBeNull(); + expect(queryByTestId(container, "canvas-background-picker")).toBeNull(); + }); + + it("should hide the theme toggle when theme is false", async () => { + const { container } = await render( + <Excalidraw UIOptions={{ canvasActions: { toggleTheme: false } }} />, + ); + //open menu + toggleMenu(container); + expect(queryByTestId(container, "toggle-dark-mode")).toBeNull(); + }); + + it("should not render default items in custom menu even if passed if the prop in `canvasActions` is set to false", async () => { + const { container } = await render( + <Excalidraw UIOptions={{ canvasActions: { loadScene: false } }}> + <MainMenu> + <MainMenu.ItemCustom> + <button + style={{ height: "2rem" }} + onClick={() => window.alert("custom menu item")} + > + custom item + </button> + </MainMenu.ItemCustom> + <MainMenu.DefaultItems.LoadScene /> + </MainMenu> + </Excalidraw>, + ); + //open menu + toggleMenu(container); + // load button shouldn't be rendered since `UIActions.canvasActions.loadScene` is `false` + expect(queryByTestId(container, "load-button")).toBeNull(); + }); + }); + }); + + describe("Test theme prop", () => { + it("should show the theme toggle by default", async () => { + const { container } = await render(<Excalidraw />); + expect(h.state.theme).toBe(THEME.LIGHT); + //open menu + toggleMenu(container); + const darkModeToggle = queryByTestId(container, "toggle-dark-mode"); + expect(darkModeToggle).toBeTruthy(); + }); + + it("should not show theme toggle when the theme prop is defined", async () => { + const { container } = await render(<Excalidraw theme={THEME.DARK} />); + + expect(h.state.theme).toBe(THEME.DARK); + //open menu + toggleMenu(container); + expect(queryByTestId(container, "toggle-dark-mode")).toBe(null); + }); + + it("should show theme mode toggle when `UIOptions.canvasActions.toggleTheme` is true", async () => { + const { container } = await render( + <Excalidraw + theme={THEME.DARK} + UIOptions={{ canvasActions: { toggleTheme: true } }} + />, + ); + expect(h.state.theme).toBe(THEME.DARK); + //open menu + toggleMenu(container); + const darkModeToggle = queryByTestId(container, "toggle-dark-mode"); + expect(darkModeToggle).toBeTruthy(); + }); + + it("should not show theme toggle when `UIOptions.canvasActions.toggleTheme` is false", async () => { + const { container } = await render( + <Excalidraw + UIOptions={{ canvasActions: { toggleTheme: false } }} + theme={THEME.DARK} + />, + ); + expect(h.state.theme).toBe(THEME.DARK); + //open menu + toggleMenu(container); + const darkModeToggle = queryByTestId(container, "toggle-dark-mode"); + expect(darkModeToggle).toBe(null); + }); + }); + + describe("Test name prop", () => { + it("should allow editing name", async () => { + const { container } = await render(<Excalidraw />); + //open menu + toggleMenu(container); + fireEvent.click(queryByTestId(container, "image-export-button")!); + const textInput: HTMLInputElement | null = document.querySelector( + ".ImageExportModal .ImageExportModal__preview__filename .TextInput", + ); + expect(textInput?.value).toContain(`${t("labels.untitled")}`); + expect(textInput?.nodeName).toBe("INPUT"); + }); + + it('should set the name when the name prop is present"', async () => { + const name = "test"; + const { container } = await render(<Excalidraw name={name} />); + //open menu + toggleMenu(container); + await fireEvent.click(queryByTestId(container, "image-export-button")!); + const textInput = document.querySelector( + ".ImageExportModal .ImageExportModal__preview__filename .TextInput", + ) as HTMLInputElement; + expect(textInput?.value).toEqual(name); + expect(textInput?.nodeName).toBe("INPUT"); + }); + }); + + describe("Test autoFocus prop", () => { + it("should not focus when autoFocus is false", async () => { + const { container } = await render(<Excalidraw />); + + expect( + container.querySelector(".excalidraw") === document.activeElement, + ).toBe(false); + }); + + it("should focus when autoFocus is true", async () => { + const { container } = await render(<Excalidraw autoFocus={true} />); + + expect( + container.querySelector(".excalidraw") === document.activeElement, + ).toBe(true); + }); + }); + + describe("<MainMenu/>", () => { + it("should render main menu with host menu items if passed from host", async () => { + const { container } = await render( + <Excalidraw> + <MainMenu> + <MainMenu.Item onSelect={() => window.alert("Clicked")}> + Click me + </MainMenu.Item> + <MainMenu.ItemLink href="blog.excalidaw.com"> + Excalidraw blog + </MainMenu.ItemLink> + <MainMenu.ItemCustom> + <button + style={{ height: "2rem" }} + onClick={() => window.alert("custom menu item")} + > + custom menu item + </button> + </MainMenu.ItemCustom> + <MainMenu.DefaultItems.Help /> + </MainMenu> + </Excalidraw>, + ); + //open menu + toggleMenu(container); + expect(queryByTestId(container, "dropdown-menu")).toMatchSnapshot(); + }); + + it("should update themeToggle text even if MainMenu memoized", async () => { + const CustomExcalidraw = () => { + const customMenu = useMemo(() => { + return ( + <MainMenu> + <MainMenu.DefaultItems.ToggleTheme /> + </MainMenu> + ); + }, []); + + return <Excalidraw>{customMenu}</Excalidraw>; + }; + + const { container } = await render(<CustomExcalidraw />); + //open menu + toggleMenu(container); + + expect(h.state.theme).toBe(THEME.LIGHT); + + expect( + queryByTestId(container, "toggle-dark-mode")?.textContent, + ).toContain(t("buttons.darkMode")); + + fireEvent.click(queryByTestId(container, "toggle-dark-mode")!); + + expect( + queryByTestId(container, "toggle-dark-mode")?.textContent, + ).toContain(t("buttons.lightMode")); + }); + }); +}); diff --git a/packages/excalidraw/tests/export.test.tsx b/packages/excalidraw/tests/export.test.tsx new file mode 100644 index 0000000..3547b29 --- /dev/null +++ b/packages/excalidraw/tests/export.test.tsx @@ -0,0 +1,197 @@ +import React from "react"; +import { render, waitFor } from "./test-utils"; +import { Excalidraw } from "../index"; +import { API } from "./helpers/api"; +import { encodePngMetadata } from "../data/image"; +import { serializeAsJSON } from "../data/json"; +import { + decodeSvgBase64Payload, + encodeSvgBase64Payload, + exportToSvg, +} from "../scene/export"; +import type { FileId } from "../element/types"; +import { getDataURL } from "../data/blob"; +import { getDefaultAppState } from "../appState"; +import { SVG_NS } from "../constants"; + +const { h } = window; + +const testElements = [ + { + ...API.createElement({ + type: "text", + id: "A", + text: "😀", + }), + // can't get jsdom text measurement to work so this is a temp hack + // to ensure the element isn't stripped as invisible + width: 16, + height: 16, + }, +]; + +// tiny polyfill for TextDecoder.decode on which we depend +Object.defineProperty(window, "TextDecoder", { + value: class TextDecoder { + decode(ab: ArrayBuffer) { + return new Uint8Array(ab).reduce( + (acc, c) => acc + String.fromCharCode(c), + "", + ); + } + }, +}); + +describe("export", () => { + beforeEach(async () => { + await render(<Excalidraw />); + }); + + it("export embedded png and reimport", async () => { + const pngBlob = await API.loadFile("./fixtures/smiley.png"); + const pngBlobEmbedded = await encodePngMetadata({ + blob: pngBlob, + metadata: serializeAsJSON(testElements, h.state, {}, "local"), + }); + await API.drop(pngBlobEmbedded); + + await waitFor(() => { + expect(h.elements).toEqual([ + expect.objectContaining({ type: "text", text: "😀" }), + ]); + }); + }); + + it("test encoding/decoding scene for SVG export", async () => { + const metadataElement = document.createElementNS(SVG_NS, "metadata"); + + encodeSvgBase64Payload({ + metadataElement, + payload: serializeAsJSON(testElements, h.state, {}, "local"), + }); + + const decoded = JSON.parse( + decodeSvgBase64Payload({ svg: metadataElement.innerHTML }), + ); + expect(decoded.elements).toEqual([ + expect.objectContaining({ type: "text", text: "😀" }), + ]); + }); + + it("export svg-embedded scene", async () => { + const svg = await exportToSvg( + testElements, + { ...getDefaultAppState(), exportEmbedScene: true }, + {}, + ); + const svgText = svg.outerHTML; + + expect(svgText).toMatchSnapshot(`svg-embdedded scene export output`); + }); + + it("import embedded png (legacy v1)", async () => { + await API.drop(await API.loadFile("./fixtures/test_embedded_v1.png")); + await waitFor(() => { + expect(h.elements).toEqual([ + expect.objectContaining({ type: "text", text: "test" }), + ]); + }); + }); + + it("import embedded png (v2)", async () => { + await API.drop(await API.loadFile("./fixtures/smiley_embedded_v2.png")); + await waitFor(() => { + expect(h.elements).toEqual([ + expect.objectContaining({ type: "text", text: "😀" }), + ]); + }); + }); + + it("import embedded svg (legacy v1)", async () => { + await API.drop(await API.loadFile("./fixtures/test_embedded_v1.svg")); + await waitFor(() => { + expect(h.elements).toEqual([ + expect.objectContaining({ type: "text", text: "test" }), + ]); + }); + }); + + it("import embedded svg (v2)", async () => { + await API.drop(await API.loadFile("./fixtures/smiley_embedded_v2.svg")); + await waitFor(() => { + expect(h.elements).toEqual([ + expect.objectContaining({ type: "text", text: "😀" }), + ]); + }); + }); + + it("exporting svg containing transformed images", async () => { + const normalizeAngle = (angle: number) => (angle / 180) * Math.PI; + + const elements = [ + API.createElement({ + type: "image", + fileId: "file_A", + x: 0, + y: 0, + scale: [1, 1], + width: 100, + height: 100, + angle: normalizeAngle(315), + }), + API.createElement({ + type: "image", + fileId: "file_A", + x: 100, + y: 0, + scale: [-1, 1], + width: 50, + height: 50, + angle: normalizeAngle(45), + }), + API.createElement({ + type: "image", + fileId: "file_A", + x: 0, + y: 100, + scale: [1, -1], + width: 100, + height: 100, + angle: normalizeAngle(45), + }), + API.createElement({ + type: "image", + fileId: "file_A", + x: 100, + y: 100, + scale: [-1, -1], + width: 50, + height: 50, + angle: normalizeAngle(315), + }), + ]; + const appState = { ...getDefaultAppState(), exportBackground: false }; + const files = { + file_A: { + id: "file_A" as FileId, + dataURL: await getDataURL(await API.loadFile("./fixtures/deer.png")), + mimeType: "image/png", + created: Date.now(), + lastRetrieved: Date.now(), + }, + } as const; + + const svg = await exportToSvg(elements, appState, files); + + const svgText = svg.outerHTML; + + // expect 1 <image> element (deduped) + expect(svgText.match(/<image/g)?.length).toBe(1); + // expect 4 <use> elements (one for each excalidraw image element) + expect(svgText.match(/<use/g)?.length).toBe(4); + + // in case of regressions, save the SVG to a file and visually compare to: + // src/tests/fixtures/svg-image-exporting-reference.svg + expect(svgText).toMatchSnapshot(`svg export output`); + }); +}); diff --git a/packages/excalidraw/tests/fitToContent.test.tsx b/packages/excalidraw/tests/fitToContent.test.tsx new file mode 100644 index 0000000..a4f0391 --- /dev/null +++ b/packages/excalidraw/tests/fitToContent.test.tsx @@ -0,0 +1,191 @@ +import React from "react"; +import { act, render } from "./test-utils"; +import { API } from "./helpers/api"; + +import { Excalidraw } from "../index"; +import { vi } from "vitest"; + +const { h } = window; + +const waitForNextAnimationFrame = () => { + return act( + () => + new Promise((resolve) => { + requestAnimationFrame(() => { + requestAnimationFrame(resolve); + }); + }), + ); +}; + +describe("fitToContent", () => { + it("should zoom to fit the selected element", async () => { + await render(<Excalidraw />); + + h.state.width = 10; + h.state.height = 10; + + const rectElement = API.createElement({ + width: 50, + height: 100, + x: 50, + y: 100, + }); + + expect(h.state.zoom.value).toBe(1); + + act(() => { + h.app.scrollToContent(rectElement, { fitToContent: true }); + }); + + // element is 10x taller than the viewport size, + // zoom should be at least 1/10 + expect(h.state.zoom.value).toBeLessThanOrEqual(0.1); + }); + + it("should zoom to fit multiple elements", async () => { + await render(<Excalidraw />); + + const topLeft = API.createElement({ + width: 20, + height: 20, + x: 0, + y: 0, + }); + + const bottomRight = API.createElement({ + width: 20, + height: 20, + x: 80, + y: 80, + }); + + h.state.width = 10; + h.state.height = 10; + + expect(h.state.zoom.value).toBe(1); + + act(() => { + h.app.scrollToContent([topLeft, bottomRight], { + fitToContent: true, + }); + }); + + // elements take 100x100, which is 10x bigger than the viewport size, + // zoom should be at least 1/10 + expect(h.state.zoom.value).toBeLessThanOrEqual(0.1); + }); + + it("should scroll the viewport to the selected element", async () => { + await render(<Excalidraw />); + + h.state.width = 10; + h.state.height = 10; + + const rectElement = API.createElement({ + width: 100, + height: 100, + x: 100, + y: 100, + }); + + expect(h.state.zoom.value).toBe(1); + expect(h.state.scrollX).toBe(0); + expect(h.state.scrollY).toBe(0); + + act(() => { + h.app.scrollToContent(rectElement); + }); + + // zoom level should stay the same + expect(h.state.zoom.value).toBe(1); + + // state should reflect some scrolling + expect(h.state.scrollX).not.toBe(0); + expect(h.state.scrollY).not.toBe(0); + }); +}); + +describe("fitToContent animated", () => { + beforeEach(() => { + vi.spyOn(window, "requestAnimationFrame"); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("should ease scroll the viewport to the selected element", async () => { + await render(<Excalidraw />); + + h.state.width = 10; + h.state.height = 10; + + const rectElement = API.createElement({ + width: 100, + height: 100, + x: -100, + y: -100, + }); + + act(() => { + h.app.scrollToContent(rectElement, { animate: true }); + }); + + expect(window.requestAnimationFrame).toHaveBeenCalled(); + + // Since this is an animation, we expect values to change through time. + // We'll verify that the scroll values change at 50ms and 100ms + expect(h.state.scrollX).toBe(0); + expect(h.state.scrollY).toBe(0); + + await waitForNextAnimationFrame(); + + const prevScrollX = h.state.scrollX; + const prevScrollY = h.state.scrollY; + + expect(h.state.scrollX).not.toBe(0); + expect(h.state.scrollY).not.toBe(0); + + await waitForNextAnimationFrame(); + + expect(h.state.scrollX).not.toBe(prevScrollX); + expect(h.state.scrollY).not.toBe(prevScrollY); + }); + + it("should animate the scroll but not the zoom", async () => { + await render(<Excalidraw />); + + h.state.width = 50; + h.state.height = 50; + + const rectElement = API.createElement({ + width: 100, + height: 100, + x: 100, + y: 100, + }); + + expect(h.state.scrollX).toBe(0); + expect(h.state.scrollY).toBe(0); + + act(() => { + h.app.scrollToContent(rectElement, { animate: true, fitToContent: true }); + }); + + expect(window.requestAnimationFrame).toHaveBeenCalled(); + + await waitForNextAnimationFrame(); + + const prevScrollX = h.state.scrollX; + const prevScrollY = h.state.scrollY; + + expect(h.state.scrollX).not.toBe(0); + expect(h.state.scrollY).not.toBe(0); + + await waitForNextAnimationFrame(); + + expect(h.state.scrollX).not.toBe(prevScrollX); + expect(h.state.scrollY).not.toBe(prevScrollY); + }); +}); diff --git a/packages/excalidraw/tests/fixtures/deer.png b/packages/excalidraw/tests/fixtures/deer.png Binary files differnew file mode 100644 index 0000000..69a4512 --- /dev/null +++ b/packages/excalidraw/tests/fixtures/deer.png diff --git a/packages/excalidraw/tests/fixtures/diagramFixture.ts b/packages/excalidraw/tests/fixtures/diagramFixture.ts new file mode 100644 index 0000000..72b909a --- /dev/null +++ b/packages/excalidraw/tests/fixtures/diagramFixture.ts @@ -0,0 +1,33 @@ +import { VERSIONS } from "../../constants"; +import { + diamondFixture, + ellipseFixture, + rectangleFixture, +} from "./elementFixture"; + +export const diagramFixture = { + type: "excalidraw", + version: VERSIONS.excalidraw, + source: "https://excalidraw.com", + elements: [diamondFixture, ellipseFixture, rectangleFixture], + appState: { + viewBackgroundColor: "#ffffff", + gridModeEnabled: false, + }, + files: {}, +}; + +export const diagramFactory = ({ + overrides = {}, + elementOverrides = {}, +} = {}) => ({ + ...diagramFixture, + elements: [ + { ...diamondFixture, ...elementOverrides }, + { ...ellipseFixture, ...elementOverrides }, + { ...rectangleFixture, ...elementOverrides }, + ], + ...overrides, +}); + +export default diagramFixture; diff --git a/packages/excalidraw/tests/fixtures/elementFixture.ts b/packages/excalidraw/tests/fixtures/elementFixture.ts new file mode 100644 index 0000000..f0470ec --- /dev/null +++ b/packages/excalidraw/tests/fixtures/elementFixture.ts @@ -0,0 +1,67 @@ +import type { Radians } from "@excalidraw/math"; +import { DEFAULT_FONT_FAMILY } from "../../constants"; +import type { ExcalidrawElement } from "../../element/types"; + +const elementBase: Omit<ExcalidrawElement, "type"> = { + id: "vWrqOAfkind2qcm7LDAGZ", + x: 414, + y: 237, + width: 214, + height: 214, + angle: 0 as Radians, + strokeColor: "#000000", + backgroundColor: "#15aabf", + fillStyle: "hachure", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + groupIds: [], + frameId: null, + roundness: null, + index: null, + seed: 1041657908, + version: 120, + versionNonce: 1188004276, + isDeleted: false, + boundElements: null, + updated: 1, + link: null, + locked: false, +}; + +export const rectangleFixture: ExcalidrawElement = { + ...elementBase, + type: "rectangle", +}; +export const embeddableFixture: ExcalidrawElement = { + ...elementBase, + type: "embeddable", +}; +export const ellipseFixture: ExcalidrawElement = { + ...elementBase, + type: "ellipse", +}; +export const diamondFixture: ExcalidrawElement = { + ...elementBase, + type: "diamond", +}; +export const rectangleWithLinkFixture: ExcalidrawElement = { + ...elementBase, + type: "rectangle", + link: "excalidraw.com", +}; + +export const textFixture: ExcalidrawElement = { + ...elementBase, + type: "text", + fontSize: 20, + fontFamily: DEFAULT_FONT_FAMILY, + text: "original text", + originalText: "original text", + textAlign: "left", + verticalAlign: "top", + containerId: null, + lineHeight: 1.25 as any, + autoResize: false, +}; diff --git a/packages/excalidraw/tests/fixtures/fixture_library.excalidrawlib b/packages/excalidraw/tests/fixtures/fixture_library.excalidrawlib new file mode 100644 index 0000000..1077988 --- /dev/null +++ b/packages/excalidraw/tests/fixtures/fixture_library.excalidrawlib @@ -0,0 +1,31 @@ +{ + "type": "excalidrawlib", + "version": 1, + "library": [ + [ + { + "type": "rectangle", + "version": 38, + "versionNonce": 1046419680, + "isDeleted": false, + "id": "A", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 21801, + "y": 719.5, + "strokeColor": "#c92a2a", + "backgroundColor": "#e64980", + "width": 50, + "height": 30, + "seed": 117297479, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElementIds": [] + } + ] + ] +} diff --git a/packages/excalidraw/tests/fixtures/smiley.png b/packages/excalidraw/tests/fixtures/smiley.png Binary files differnew file mode 100644 index 0000000..7a40543 --- /dev/null +++ b/packages/excalidraw/tests/fixtures/smiley.png diff --git a/packages/excalidraw/tests/fixtures/smiley_embedded_v2.png b/packages/excalidraw/tests/fixtures/smiley_embedded_v2.png Binary files differnew file mode 100644 index 0000000..b74075b --- /dev/null +++ b/packages/excalidraw/tests/fixtures/smiley_embedded_v2.png diff --git a/packages/excalidraw/tests/fixtures/smiley_embedded_v2.svg b/packages/excalidraw/tests/fixtures/smiley_embedded_v2.svg new file mode 100644 index 0000000..203e044 --- /dev/null +++ b/packages/excalidraw/tests/fixtures/smiley_embedded_v2.svg @@ -0,0 +1,16 @@ +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 56 77"> + <!-- svg-source:excalidraw --> + <!-- payload-type:application/vnd.excalidraw+json --><!-- payload-version:2 --><!-- payload-start -->eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nGVSy07jMFx1MDAxNN3zXHUwMDE1kdmOIE1IXztcdTAwMWVcdTAwMDNiw6ZcdTAwMTKjXHUwMDExXHUwMDFhjUxym1hcdTAwMTjbsm9pU4TEZ8xufpFPwNcpcVqyiHTPfVx1MDAxZJ9zX4+ShGFrgM1cdTAwMTNcdTAwMDabkktRWb5mP1xif1x1MDAwMeuEVj6VhdjplS1DZYNo5qenUvuGRjuc52madk0g4Vx1MDAxOVx1MDAxNDpf9uDjJHlccn+fXHUwMDExXHUwMDE1tS4up2Urr2/M9lwivV1v26vf8Pc+tIaiLy5cYlx1MDAxYozoxkPT6biPW1x1MDAxZufF5KTokbWosCE0XHUwMDE2NSDqXHUwMDA2PVZMeoyrWtL8tEdcdTAwMWNa/Vx1MDAwNJdaakt7j8tZxjNcdTAwMWVXP/LyqbZ6paq+XHUwMDA2LVfOcOufXHUwMDE565ZCylx1MDAwNbay04eXzcpcdTAwMDI72PJrR3J0gPd9Tnv9Y5dfWzdcbpzb69GGl1x1MDAwMkmCUVx1MDAxYd9BXHUwMDFjzW1cdTAwMTV0/3M4v+HW7OYwR8GAXHUwMDE5XHUwMDAw+ZJl6WSSj2dxzcD9/Fx1MDAxMLzTKlx1MDAxY0IxK/JiXHUwMDFj08Jdef8xTFxccukgykhcbv7sbqNjqVZSRtvJbk/u4/+/94GmWuFCbGHfV0Kv+bOQ7Z4sNOJcXIqaXHUwMDE4M1x0y4E3njVcbn+qfVx1MDAxYbVcdTAwMTk67EBcbkVbzkZcdTAwMDF88/+gIePGLJAj5bo7Zi9cdTAwMDLWXHUwMDE332/ieFx1MDAxOb7dVO+GqHbM6Z1HNPPtXHUwMDEz+I3nwSJ9<!-- payload-end --> + <defs> + <style> + @font-face { + font-family: "Virgil"; + src: url("https://excalidraw.com/Virgil.woff2"); + } + @font-face { + font-family: "Cascadia"; + src: url("https://excalidraw.com/Cascadia.woff2"); + } + </style> + </defs> + <rect x="0" y="0" width="56" height="77" fill="#ffffff"></rect><g transform="translate(10 10) rotate(0 18 28.5)"><text x="0" y="41" font-family="Virgil, Segoe UI Emoji" font-size="36px" fill="#c92a2a" text-anchor="start" style="white-space: pre;" direction="ltr">😀</text></g></svg> diff --git a/packages/excalidraw/tests/fixtures/svg-image-exporting-reference.svg b/packages/excalidraw/tests/fixtures/svg-image-exporting-reference.svg new file mode 100644 index 0000000..513c72c --- /dev/null +++ b/packages/excalidraw/tests/fixtures/svg-image-exporting-reference.svg @@ -0,0 +1,16 @@ +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 201.06601717798213 261.4213562373095" width="201.06601717798213" height="261.4213562373095"><symbol id="image-file_A"><image width="100%" height="100%" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAT4AAAFPCAYAAADURqJUAAAAAXNSR0IArs4c6QAAIABJREFUeF7tnXeYFUXWxl9yzpIkOOSgJF1UEFFEUQHzmhUVEyZEV7/dNQAiuooBMWBARcREEjAgriJZFBMgeWDIMOSBIcfvOcO96wxzQ3dV9b1dt996Hh74o86pU79T96W6q7qqAFhIgARIIGAECgSsv+wuCZAACYDCx0FAAiQQOAIUvsClnB0mARKg8HEMkAAJBI4AhS9wKWeHSYAEKHwcAyRAAoEjQOELXMrZYRIgAQofxwAJkEDgCFD4ApdydpgESIDCxzFAAiQQOAIUvsClnB0mARKg8HEMkAAJBI4AhS9wKWeHSYAEKHwcAyRAAoEjQOELXMrZYRIgAQofxwAJkEDgCFD4ApdydpgESIDCxzFAAiQQOAIUvsClnB0mARKg8HEMkAAJBI4AhS9wKWeHSYAEKHwcAyRAAoEjQOELXMrZYRIgAQofxwAJkEDgCFD4ApdydpgESIDCxzFAAiQQOAIUvsClnB0mARKg8HEMkAAJBI4AhS9wKWeHSYAEKHwcAyRAAoEjQOELXMrZYRIgAQofxwAJkEDgCFD4ApdydpgESIDCxzFAAiQQOAIUvsClnB0mARKg8HEMkAAJBI4AhS9wKWeHSYAEKHwcAyRAAoEjQOELXMrZYRIgAQofxwAJkEDgCFD4ApdydpgESIDCxzFAAiQQOAIUvsClnB0mARKg8HEMkAAJBI4AhS9wKWeHSYAEKHzJHQNNAbQF0A5AIwCVAVQCUB7AFgCbAawHMAvAD6G/9zkMuQuAZgBOBlAXQNWQ/7Ihn+J7OYCpAKYAmOfQb3UA54X8iu9aIb81AewFkAVgeyh2iXtiqA2H7lmNBLwnQOHznnGkFv4OoHdImNxEsBvAOwBeDInX8bYimvcBuDckdG58pwN4BcDgKEatADwE4GY3TkN19wAYAaAPgDUK9jQhAaMEKHxGccZ1djaAQQBERHSLCODjoZmh+LoHwAAApTUdy0zzWQADQ35KAXgVQHdNv2FzEdd+oVmhIZd0QwLuCFD43PFSrV0CwPMAHlB1EMVuB4CnAXQEcLFh3wtDgvcYgNqGfWcCuNDF47Xh5uku6AQofN6PAHn3Je/nGnjflFUtZAO4PMTGqsAZrP0EKHze5lBEbzqANG+bsdr7FQDGWd0DBm8dAQqfdymj6Dljux9AGwB/OKvOWiSgT4DCp88wkgfh+puhRQxvIvSX1w0AWoS2wPgrMkaTkgQofN6ktVeuVVFvWkg9r2MBXJl63WKP/EiAwmc+KxUBrDKwrcR8ZP73eA6Aaf4PkxHaToDCZz6DskftSfNuA+FRttCcAuBoIHrLTiaNAIXPLHrZPCxfJsgnZyxqBLoBGK5mSisScEaAwueMk9NatwF432ll1otI4BsAncmGBLwkQOEzS3ckgKvNugyctwMAKgCQ73tZSMATAhQ+c1gLAdgKoJw5l4H1dBWAzwPbe3bccwIUPmeIOwG4LnSaisxG5I+s3sr+swwAawHsBHCnM3eRa13eFnjwciCtKrByI/DUR8AUp4dFxWm4z03Auc2P+Z6TAdz2EpC1SyfaY7blSwMD7z7mO2v3sXglbk3fzwH4t3509EACkQlQ+KKPDDkb758A5GW7/NvT0vdmQMTp+HLFU8C4H/WaHtsHEFHNXUSY6tyiJ1AieiuGHRO/3GXOcqCVHIylXj5SPP5KvUVaBooAhS9/uuUYpv8D8A8A8m/PiwjH9jGRmxGBqiAPfopFZmKTX4hsPOw74FY52U+xfPAIcMsFkY0fegt4RbYkqxU5GLWDmimtSCA+AQpfXkZyvNOniZjh5W42ljhJPZk9ySxKpUSbSYov3ZnZH4OBlvU8EdUlABqr9Jc2JOCEAIXvL0qPAvgPAFmkSGiJJ3wdHlV/1xdL+KSTBeRUPMVy9NvohpqzSTmw4FTFsGhGAnEJUPiOIfowme+UUlH4ZIGjr/o25K8BdI07elmBBBQJUPiOnWD8hCI/I2Y2Cl+8mDWF713dFXIjiaGTlCUQdOGTZYPRyc5uPBHx46PuKz2AB+UI0ShFJ+bQnRxyMRELCXhCIMjCJ1tUVgIo6QlZF05tEz5Zhf7jDSCtWvROykq0xl6+HgDedoGQVUnAFYEgC588Tt3uipZHlW0TvngLJlPnAefKUpF6uRTAl+rmtCSB2ASCKnxykfd8WdT0wwCxSfhk+8rkAfk3LefmqLmHT1y1BvCrH3LDGFKTgC9++ElAO8GD6xiVu2GL8Dl5xN2xG0jrpvWYK5em694NrJwLGgaDQBCFr0zoMuuE79eLNqRsED6Z6Y3tHfu9nvRPczVXXMgJN9cG4+fHXiaLQBCFT769HZYs4JHa9bvwycEJ8l7v+G9yj++LgdmeuLwGwCg/5YexpB6BIAqfHHcUYyNG4pPsR+GTU1wuawP0uiL+LC9MzMCBCnsBVAIgf7OQgGcEgih8qwHUUiUqsx6ZAYlYyfFO8mmW6ne04Rh0hO+c5sdikSKPo+WPO1ZBBCzWtpMpc/OTOFcuenRZBo0Fer3l0ih/dflPSeNIBu326SAgBIImfNJfOeG3sEp+RUTkw/zjH/nkbLsP/qvi8ZiNW+GTE1HkmKnjj5pSj0DP0sD2lXAAN4QOidALiNYkEIdA0ITvBJ1Lq2UbR6TZkGzUlRNU5PBQleJU+ETw+t7k/NFTJRa3NiJ6lz+ltYobbnI9gLoA9ruNgfVJwC2BoAlfMwDKZxrHOo1EZ9YXT/jE9y3nRxZdtwk3Wd/Q4204JN6uZjI59BWTQNCErxWA31XHRCzh09nGEU/4VOP1yk5Wb+V9ns7j/XGxyX26J3sVL/2SwPEEgiZ8cmzmMtVhEEv4dGY/tgifCJ6cqix/NL7DjYT/XABTVfNCOxJwSyBowicHE2xyCylcP+tzoFyUw+h1TjP2u/DJezy59+OD74wLnqCVr2i6qOaEdiSgQiBowlcMwD4VUGIz5QVAto9EK3W6qS1wmBK+uRlmhCl8s5ss1ojgGZ7d5cZ3FEBDnVm4ai5pF2wCQRM+ybbcfStXQ7ou8c6gU33Ppyp88ugp79lkJqa7l9A1DDMGjwN41owreiEB5wSCKHzfA5BLhVyXeAKlemVjPL+RApWN07LA4OFszDUflwbj/PYFjcv4Wd1iAkEUvoEAeqnmLNZ7PvEpMzDZfuKmuBU+na0zbuLysK4cCXY6P03zkDBdxyQQROHrDuA91XER6y7ZsE+359G5ET4D38Oqdt2U3Q4Asp9yjSmH9EMCbgkEUfjqAMhwCypcXz5XW/lh9NXdcD25YUze+TkpToXPraA6aTvBdY4A6ARgUoLbZXMkkIdAEIVPACwG0Eh1LMQ7ej3sV1ZERazifcrmRPhkxbblPaoR+8JODhiVAwhi3MbrizgZRAAIBFX4BgBQvhXC6awvPH5kw2+sU1ycCJ/mrWXJHsqrAMjV5UuSHQjbJwEhEFThOxvANJ0hIEdAyb6+aBuaI/mWFdhI205ESMVftLJq47Hj3C0t0wHI5UFZlsbPsFOQQFCFT/otn67JaSDK5dZOwNB/KJs7NtT5HM5xI95UHAygJ4DD3rinVxJQIxBU4RNaDwB4VQ3bX1aJED8LFzXmhLYM8ftb3QFGe08IBFn45CLxTABy+ZBWkQNBZZuLm8deNw1a9H5PVsufDB0mKp+jsZCALwkEWfgkIS8AeMREZuQd3bg+wElVTXjL68MC4dsCoD8AebQ9aJ4APZKAWQJBF76nAPQ2iVQefeWbXpOzPx8Lnxy4L/dkfAIg2yRH+iIBLwkEWfjuB/CaF3BllfbWCwARwRZayyfHovOR8Im4yTFS4wF8CWCXF/zokwS8JhBU4fNM9I5PmFxQJPd0tKz715YVeSx2MyNMoPDJe7kNAFaE/sg7u/C/5e+1AOTrCxYSsJpAEIXvYQAujxFwl+MyJYuhZYMT0ah2FdStUQllSxZD8WJFULxoYZQI/V20SGHs3X8w50/Z4utwUfMfojbisfDJpuJzACheleSOTYrX/gzAtRp9lCPT5KDczbn+ls3f8krhDw2/ND2OQNCETxYyZEHDeClWpDDOb90AHU6rjxb1T3Tlv0LJ9Tittjw5Ri7PjGyAF0euRVa2Z/dsy8yuLcXPVdoiVR4B4BptL5EdyH3Q8opB/sg2oUMetRMIt0ESvn8CeM50VmUWd03HFriifTOULikHPLsv8YTvt9WXYPueEzH592UYN3U+Fsb7+Nd9CGLBmZ8at9xWujM+pxHIu9ahAPqFDtZ1asd6IQJBET5PZnod/9YAd112JiqWlS2B6sWp8IVb+HzKPLw5dpZ6g9EtOfPTo+rljC9SZHLEl5xgLRvxla9U0OuyndZBED7jole4UEH84/pzcH5ruS5Cv7gVPmlxdeZ2PP3B91i5YZt+AHk9UPzUiSZa+MKRymPwY6FtRdw47iB/qS58xkWvfOni6HfnRWgiy7WGiorwhZse8sVPGDlprqFI/ueG4qdGNFnCF472q9Diyh618INjlcrCZ1z0qlcqixfu74qqFbW/csszwnSETxx9OWMBXh01w/Sopfi5J5ps4ZOI54UOe+UqfYz8parwGRe9BjVPwPP3dYVsVYlVdu3Zj3nLNyBzWzaa16uO+jVPiPvzKVEkG9XLRT+qbsOORth7MLbYfjF9AV4b7Yn4tQewLm4nWEEI+EH4JA7JV+eQCDIzEQikovAZFz0Rrxfv74pSJWKL3tz09ej73rfYtffA/1Cf1SwNfe+QMzi9Lx7N/GQf2VkUP0f584vwSbDyuHslT7yOnLdUE76kiZ7M9G7u90ke0Qsjv/mi09Dt4r85+uXoVqL46RLUsveT8ElH5BzEcwEYfxTQouQD41QSvqSJnuTx25+X4MVPpkRMabWKZTC8zw0JSzfFL2Goj2/Ib8In8cmyf2udC7aSRtPDhlNF+IyLXlr1injlwUvjPt6Gc/PhN79i+MTfoqbqu0F3e5jG/K4pfgnFHW7Mj8InsckLZHnk4KESoUylgvB5InovPXAJypYq7vjX4zfhk8Apfo7TZ6qiX4VP+vcNgC4AuM8vBS4b8oXoyajyo/BR/EzpmWM/fhY+6cQzAJ5w3JsUrmjzjM83oudn4aP4JfTX63fhk9neaTzpxd7rJY2LXr0alTDgvq6uHm9z/6T8OuMLxzh+2ny8PmamaRWQrS5ypJX8zaKxj+/aji1zDrvI2rUXW7J246cFq3MOpfDgRB45Bkiu+wx0sXHG5zvR8/uMz2Pxk82yss+P4qchfNed3xK3X3JGPjGaOW8F3h73EzZs3WlSqE4N+qzPNuEzLnp1TqyIF+93t5ARaQT6fcYXjnns1D8x+PMfTf6IxBfF7xhR5UfdaMIXTtSISXMw/JvfsP+gkWP45GDTxOyqNz3SDPmzSfgeD93kZajrgGxZcbt6G61xW4TPw3d+cix9u4DP/JTP47v+glbo3vX0mGNbTuJ5/O1vsGm7kV0pMks3/j+gsR+nx45sEb4+APqaZFFXZnoPXBr321unbcY6I69UiaIY99xtTl0lpJ5HZ/oFfebn2YwvPCh2792PJ96ZiPkZciW0VnkPwB1aHiw2tkH4jN19G86TyZle2Gfm1uycT9YilU6nN8SjN3bw3TCh+BlPibLwOZnxhaPN3rMf974wJucgDI0i93tUDuq+Pr8LnxWiFx58kYSkasXSeOvRvysfS68xsB2ZUvwcYXJaKSHCJ8Gs3ZSFe14Yg30HtN75yXe8cn9H4Iqfhc8q0QuPHDmhZeafK7FxWzZa1K+OTqc38q3oxRJsA7+EID72Jkz4JD+jfpiLd8b/pJMqObL+QR0Httr6VfiMi16tKuXxSq/LlPfp2Zpgp3Fz5ueUVMx6CRW+/QcO4brewyOeCOSwN3KHsrsrAR069ns1vwqf0e8J06pVwIsPXIJypUv4PR9JjW/M5Hl4a5zxS4xk5ieHmcqJzqlelIUv3naWaODe/2o2Pv1O68rdmkE8azHlhU9mei8/eCnKU/QciY5HMz95kX4FgOmOgrC3krLwuVncyI0nY91W3D1gtA4xuU/Z+P92OgElwjalhY+ipzaEPBI/eQv/EIDX1aKywirhwidUbnrqY2zcpry377rQxmsrAJsKMmWFj6KnN0Q8Ej8JSj4Y7gXgV70IfWmdFOF7bvgPmPRruiqQ/wMg79QDVVJS+Ch6Zsawh+InAY4FIBvT/zQTrS+8ePrlRrQeDv16Nj75r/J7vtcA9PQFvQQGkXLCR9EzO3o8Fj8JVn6xwwB8DGCL2egT7i0pM74JsxZh4GfTVDv7BYDLVI1ttUs54Wt/RkvUqBr/SsdICbuoVXWcWLGkrbn0LG4D+8WcxrYjJH5yJ+xBp0Y+qtc09DWE65BUV3WloZ/mr8KTQya6bjNk8AuA2B8Jq3r2sZ1fhU+OOKqtwq1hw4aoWLGiiimevr6Fkl0QjBIw8wsCxqh9VF3VFYe/LFqDx96aoMpPLopJzBWAqhF6YOdX4ZPt6PkPJ3MAoGbNmpA/bku18sVx38WN3JoFqj7Fz7t0U/i8YxvJs1+Fb5zqe4dq1aohLS3NNcUOp1TFec2qubYLmkECH3sDhVbnUXf2wtU5x1UpFs74FMF5YfYmgB4qjkuWLInmzZu7MuVszxUucObnjpeT2pzxOaFkro5fZ3xa5+81bdoUZcuWdUSpWJGC6H5ePS5qOKL1VyXO/FwCi1NdZ8b388LVeIIzPlcJ8avwdZVrYV31JFflypUro169enHN06qUwg3t0lCiWOG4dVkhPwHZNDvgo8k4ctTop9WBRM0ZX2LT7lfhKwpATlmUv5VK2zPPwBHk756IXbXyJVC9QgmcWldt9VcpoBQ1+n3JWvQe8q2puyBSlFL8blH44jMyWcOvwid9HK9zDV7Xs5riwWvONsmKvqIQWJ25Hb2HTMS6LUZvAgsUbwpfYtPtZ+GT+wCG6OB46Lr26NymiY4L2joksHf/QQwaOV3nm1GHLaVmNb7jS2xe/Sx88vnFZh0chQoWxMAHL0WTtKo6bmjrgsCvi9fkfD5l6CYwFy3bXZUzvsTmz8/CJyR+1v2cpkSxIuh5dTuc37phYskGuLUDBw9hxKS5GD9tPnbs3hdgEs67rjPj4z4+55zDNf0ufDcC+Mh9t/JbtG9ZFw9f1x6lShQz4Y4+HBL44bd0fDljoYnrEB22aGc1HeHT3M4i5yReEzotx054ClH7XfikS/MANFPoWz6TE8qVwtUdW+DC0xtSAE0AdeFj647dmDlvJabPzcCc9PUuLINRNYnCJ4APA7g6SOJng/B1AvCtyeFftEghdDi1fs4NaI1qV0axotzHZ5JvPF9yL+ys+StzhHDZ2i18HwggycIXFr+bAMiZgilfbBA+ScL3ADp6lY2TqlVAvRqVUKNyORTwiEj32+9Elcpqx2VF6veujSuwY8Myr5Ak1O++AwexemMWtu7Ygx279mJ79l4cOiyTkOCUU+pWR6uGNZQ6rPmom7vNIwA6m55oKHXKYyOPfubGo5ZHXXnktbY83e8pPNhLrpwwU/Zsz8Sf414y44xerCageSzV8X2XyzvkZKSFVkOJE7wtwifdeBbAv21NRo0aNbBgwQIULFjQWBcWfPUadm1ebcwfHdlJwLDwCYQ1AORwyu12EokftU3CJ4oh7/rOj98tf9b47LPP0LmzPEmYKVuW/Ybl0wPxSsYMsBT14oHwCSm5FKqDpSdhx820TcInnSkXeuRVOp05Lg2PK3Ts2BFjx8odO2bKkcOH8PunfXH44H4zDunFSgKrMrej1yvjsGvvAdPxvwjgUdNO/eDPNuETZvK+T05otvJyjDlz5qBu3brGcr9q9pfIXKB80YyxOOgouQS279yDFz6ZknMMvcEix+6cCWC2QZ++cGWj8Ak42XM00hcEXQZxzz334Pnnn3dpFb36vp1bMHeMOX/GAqOjpBD47+wleGP0TOzZb+yupiUAGielMx42aqvwCRJ51zcGgLMTRz2E6MZ1mTJlkJ6eDjkp2lRZ/O072LFe+UJpU2HQj08IrNu8A4++/iU2Z+02FdFzNi8sRoJgs/BJf+oDkMsG5G9ryqBBg3DbbbcZi3fbynlInzzcmD86sp+AfCnz6OtfYc2mLBOdkc/a5GTflNlCYLvwSVJlxiczP2tWexs1aoRffpHrTM2Uo0cO44+Rz+DgXjm7lYUEjhHYuXsf7n9pLDZsNXJO4lAA3VOFbSoIn+RC+iFTKNnrZ8UZVBMnTkTbtm2NjaO1v3+LdXPlAxcWEviLwKbt2bjvpbHIyt6ri0VmfXK4ZUp8LpQqwhdOaikATwCQTyR8fQzLVVddhaFD5T9RM+XAnp34Y0R/ALz/wgzR1PGyYsM29Hx5LPYdEO3SKvI+pZuWB58Yp5rwhbHKxbqy1CnH7fiyFCpUCIsXL0bVquYmqEu+H4qsNSn9pZEvc2lDUN/+vAQvfjLFRKh1AKw04SiZPlJV+MJM/yYHX4S2v/hu0/Njjz2Gf/3rX8byn7V2MZZ8954xf3SUWgQefnU8/lyeqdupfgDk+lerS6oLX+7kNAVwWejP6aH3gklNXpUqVbB06VJj3+8ePXoUc0Y9iwO7jazkJZUNGzdPIHNrNm7/zwgcOKh18o0cpljT9ncqQRK+40eSJK/ycX/Kuxxuwq+3S5s81YcNG4YrrrhCx0Ue2/XzfsCa32SHDwsJ5CcwevI8vD1uli6ai2w/uirIwqeb/LC9nBJwraqzdu3aYcKECarm+ewO7tuN3z/rBxyVo9VYSCAvgcOHj6Db05/qHv46ys/vz53knMLnhFLsOu0BTNVxM3v2bDRubO6roGVTPsLWFXN1QqJtChP4+seFeGXEdJ0eyg1SZQBoLxPrBKFjS+HTofeX7XwAJ6u6uv322zFw4EBV83x2Ozcsx6KJbxnzR0epRcDQrO88AJNtJUPhM5O5ewAMVnVVokQJrFixwuj3u3NGP4f92VtVQ6JdihMYMWkO3v1Cbm9VLs+E9swqO0imIYXPDH05cUAuP1c+eWDAgAHo0aOHmWgAZC6cgVU/jzfmj45Si4Bc+H5j3491OiVHw7XRcZBMWwqfOfpvALhX1V2dOnUwd66593KHDuzLOaRUvuNlIYFIBDT39cnAkq+jrBxgFD5zvwl5xyfv+pTL+PHj0aGDnPZtpmTMGIHN6b+acUYvKUfgq5kLMWik1iKHfLu72EYwFD6zWZNvgs5RddmlSxd8+umnqub57HZmLseib7jIYQxoijmSU1u69dMab1cB+NxGLBQ+s1mTb4NHqLosUKBAzk1sNWvK3mr9Io+5v3z0BI4etnbXgT4EeohJ4PJ/DcVu9bs6ngQgJ2NYVyh8ZlNWOHQ1XzVVt4888gh699b6GCRP07KtRba3sJBAJAL/HPwVfl+yThWOTBdvUDVOph2Fzzz9p3Q+Y6tQoQKWL1+OwoVFQ/VLxoxR2JyecnfF6IOhhxwCsqVFtrYoFnlBKBv4rSsUPvMpk9me/BeqfHP4kCFDcO21yl/B5emR3Lsr9++ykEAkApoLHKKYrWwkS+HzJmtyFP6Vqq5bt26NSZMmqZrnseNFREYwpqyTyb8vw7PDlMeavEOx6r6bcCIpfN4MafmcR3k0SUjTpk1Dy5YttaI7euQIfvukNy8c16KY2sazF67G428rn+Yjm/ar2EiIwudd1mR/UyNV9zfffDPeeEP2RKsXfrOrzi4olvMzMvHQIOUvfOQiD+WvlZLJmMLnHf2eAAapui9evDiWLFkCWexQLRkzRmJzurnb3FTjoJ1/CSxdsxn3vai8FU++2jCzCpdgRBQ+74DLtZcbdP5H7N+/P3r2FP10X/bu2Ix5n79g+0G57jtOC1cEKHyucLGyQwJvA7jLYd181dLS0jBv3jwl80XfvImdmRlKtjQKDgEKX3Bynciean+/O3r0aHTq1MlVzKt+/gKZC7W+wXTVHivbS4DCZ2/u/B75DABnqQZ5wQUXYMwY2R3jrKz941usm8OLxZ3RYi0KH8eAVwTkkx6tg8/kuCo5tipWObg3G6tmf4GtGcq78L3qP/36mACFz8fJsTw0WfWSy0wrqfajZ88H0L+/HHibv+zZnpmzcpu5YJqqe9oFmACFL8DJT0DXnwXwb9V2TqhYHuNe+yeKFCmUx8XBPdnYu2OTqlvakQAofBwEXhKoDWCFzve7Pa9uh0vaKd9n5GXf6NtiAhQ+i5NnSeiyPf5S1VirVyqLD3tfr2pOOxKISIDCx4HhNYELAUzUaeS5e7rgtMZmDinViYO2qUOAwpc6ufRrT+QrmaU6p1k0q1cNL/e8zK/9Y1wWEqDwWZg0C0N+GMBLOnFz1qdDj7bHE6DwcUwkgoB8vyvLsHItn1JpVLsyXv+H8lF/Sm3SKHUJUPhSN7d+69l7ALrrBNWneye0axF7Q7OOf9oGhwCFLzi5TnZPTwWgdRZ8pXIlMfTx61CiWJFk9yVq+xVqNUXD82/zbXwqgR05fAhLvn8fO9enq5j70obC58u0pGxQPwM4Xad3Xdo2Qa9r/XvPSyoKn+TryJHDWPLdeykjfhQ+nV8hbd0SkA15n7g1Or7+Sw9ciub1q+u68cQ+VYUvLH7pkz5A1lo5ZNvuoil80nkrz/S0Mmi7h1pO9HID2wIAjXX6UrFsSQx+5EpUKldKx40ntqksfAJMLmtfmgLipyl8PIHZk19Pajv9O4BRul1sUOsEDH7kKl03xu1TXfhSRfwofMaHPh06ICBnSLVwUC9mlfYt6+LJ2y7QdWPUPgjCFxa/9MnDsX21TODtKxQ++3KWChF3BfCliY6c26oe/t2tIwoW9Mfbi6AI3zHxO4L0yR9aKX4UPhO/PvpQIaC9whtu9IyTa6N3904oWjjv8VUqQenaBEn4wuK3bNon2LZiri66hNpT+BKKm43lItARgLGz4k+uUxXP9uiMksWLJhVy0IQvR/yOHsWyqR9bJX4UvqT+TALf+DAA3Uz3H/FWAAAQYklEQVRRkCOs+tzeCfVqKB/6rB1KEIXPRvGj8GkPdTrQIFA89DVHUw0feUwLFyqIW7u0xjXntUCBAol/7xdU4QuLX8b0EdiyXOsDHVNDIaYfCl9CMLORGATqA/gdQBmTlGpVKY8bLzwV555aD4UKyvbBxJQgC59N4kfhS8zvga3EJiCH7Y3zAlK1SmVwY6dTcU6regn5xjfowhcWvxU/jsbmpbO9SKkRnxQ+IxjpxACBQQB6GvAT0YU8AjerVx1tTjkJbU5JgwiiF4XC9xfVjJmjfCt+FD4vRj99qhKYCaCtqrFTu/JlSmBUf2NrKnmaLV29EU6+6A6noaR8vSU/fIysVf6781hT+CRviX+BbGC0WBm0gX773YUcWCpbXFp7GaiXwjdr/ir0HqJ1xYiXXffEd7c2tdGyVvmovk+sWweVqlfzpG1Vp5rCx291VcHTLiqB0gC+BuDZ2VMUPrOjL57wSWt+Ez9N4ZMuye1X68yS9N4bZ3zeM9ZpQY6ol0/aPPkQl8Knk5r8tk6Ez2/iZ0D4VgE4B4D8bU2h8Pk/VXLM8lgAXUyHSuEzS9Sp8EmrNerXQ8WqVcwGoODNgPBJqzLjO8sm8aPwKQyWJJgUBjASwBUm26bwmaQJuBE+v4ifIeGzTvwofGbHvpfeZPdxLwD9AZQw0RCFzwTFv3y4FT4/iJ9B4QuLn7yTzjBL1rw3Cp95pl57lC88PgJwhm5DFD5dgnntVYQv2eJnWPikOxtDW7F8LX4UPrNjP1HejMz+ypcujlHP3OJJzLP+XIne737riW+/OlUVvmSK36rM7bjjP/IWxWgR8ZMFjyVGvRp0RuEzCDMJrrRmf5zxmc2YjvBJJLUa1kf5ypXNBuXA2/hp8/H6GNkzb7RsAdDOr+JH4TOa66Q4OxnAfJWWKXwq1KLb6ApfCorfGwDuN0vZjDcKnxmOyfRC4Usm/VxtmxC+ZIrfhB8XYeCIaSZpUvhM0qSvPAQofD4ZEKaET7pTu1FDlDsh8QfJGhY/Cp9PxmYqhkHh80lWTQpfMsXvu9lLMeDjySaoUvhMUKSPiAQofD4ZGKaFLwXEj8Lnk7GZimFQ+HySVS+Ez3Lxo/D5ZGymYhhNACxU6ZiP9/FtAnCtSp8M2HwoO0tU/Nx8Zm20qh39WCoVn2Jz8NAR7Cl/Itq1qKPqQtnuueE/YNKv6ar2FD5VcrSLSyAVZ3xr5P1+3J57U0E23TZUce3VjG/msi0YO2cD+nbvhDbN0lRCU7Z5+bOp+GbWYlV7Cp8qOdrFJUDhi4vIVQXfCV//rxdj2+4DKFiwQMLFj8LnauywcgIJUPjMwvaV8C3JzMbb01b8r4eJFj8Kn9nBRW/mCFD4zLEUT74Svnenr8DCDdl5eiji1/+ui9G6idKrSFe0KHyucLFyAglQ+MzC9o3wbd21H89MiPydv9yW1+/OizwXPwqf2cFFb+YIUPjMsfTVjG/cH+sxLV2+9Y9cEiF+FD6zg4vezBFQFz4Pj6Wa+sdy9P9ALopTKoFf1T1w6Aj6fLEQ+w8diQuw/90X44ym3iyCU/ji4meFJBFQFz6P7tXN3JqNngPHYnv2XlUkgRe+Gelb8Pkf6x3xk5nfsz06o1XDGo7qu6lE4XNDi3UTScBXwiei99Cg8diyY7cOg0AL39GjR3Pe7ckWFqelSOFCeObui42LH4XPaQZYL9EEfCN8m7N2oefL43RFT/gFWvgWb8jGO9P/2sLidEB5IX4UPqf0WS/RBHwhfCJ6MtPbuG2Xif4HWviGTFuBRZl5t7A4hWpa/Ch8TsmzXqIJNAWwQKVRUycwb9u5J+edniHRS/aMbwOAaio8TXyyFmsLi9OYRPwG3NcFp9St7tQkaj0KnzZCOvCIQFJnfFm79uLBgeOwfstOk91L1oxPLsiZotoRE8I39vd1mL5sq2oI/7MrVqQwnru3s7b4Ufi0U0EHHhFQP51Fc1VXRO/hQV9gzaYs0117G0AP004d+JNrO290UC9iFV3h23fwcM4WloOHj6qGkMfOhPhR+Iykgk48IJC0GZ/M9BaulJsEjRY5FupWAGZ+/c5DKwdgM4Aizk3y1rzj7DQ0rV5W1RzTlm7BuDnOtrA4baR40cJ477FrUaVCaacmeepR+JSw0SgBBJImfNf1Ho6tO/aY7GKyRE/68BCAl1U7U7JoITx1aVMUKqh2f5fKFhansb7xyJVoWEvt2koKn1PKrJdoAqkifEMA3J2EmV44X0sBNFBN3nmNKqNrC/XFhIXrd+LdGStVm49pR+HLj0ftvydP0kOnigRSQfhE9O5S7L8Js/YApuo4erJLY1QoVVTZxdtTM7Bko5GtQPlioPBR+JQHpo8NbRe+1wE8kGS+WosaDauWRo9z6ip3wcQWlliNU/gofMqD08eGNgufH0RPFjVkhaaYao5vaVMbLWqp37Ux5rd1mLlcfwtLtPgpfBQ+1bHtZ7ukbWfRXNzwg+hJXh8E8IpqgnUXNUxvYYnUDwofhU91fPvZztYZXxcAE3wAVmtRo2PjyujSXH1RY+rSzRg/Rz4W8a5Q+Ch83o2u5Hmm8KmzPxvANHVzQGdRQ7aw9PtqMXbsPagTQlxbCh+FL+4gsbAChU89acMB3KRq3qhaadzdXn1RY8H6nXjPoy0suftE4aPwqY5xP9tR+NSyo72ocVvbk9CsprhRK29NzcBSj7awUPhi54T7+NTGrJ+sKHxq2dBa1ChTvDD6XNIEBQuo/YQ27tyH5yfK60XvC2d8nPF5P8oS3wKFT4251qLGBU2q4OJmSqdX5UQ7+re1+HH5NrXIXVpR+Ch8LoeMFdUpfO7T1A7AdPdmf1n0vaQJypZQO88gEVtY+KjLR12d8W2DLYXPfZbkMISb3Zsds2hSvQzuPLuOqjmmLNmML+Z6u4WFwkfhUx6glhhS+NwlSntRo/tZJ+GUGmqLGkeOHsXTCdjCQuGj8Ln7WdhXm8LnLmdJXdSYv24H3p+5yl3EmrX5jo/v+DSHkC/NKXzu0qK1qHHhyVUhf1TLm1MykL7Jm1NYosVE4aPwqY5XP9tR+Jxn5ywAM5xXz1tTNq7IFhbVRY1EbmHhoy4fdVXHuS12FD7nmRoGoJvz6nlrnnxiWdzeLk3VHKN+XYtZGYnZwkLho/ApD1RLDCl8zhKlvahxR7s0ND1R7U4N2cLy5PiFOHwk0VeJAHzU5aOus5+IXbUofM7y1RPAIGdV89eSLzVk714BxS81Ji/ehC/nZao2r2VH4aPwaQ0gnxpT+JwlRmtR46JTqqJTU7VFjWRsYeGjLh91nf0s7K1l60GknQF8kyDsbQHMVG1Ld1Hjz7U7MPTHxG5hofBR+FTHuy12nPHFz9TQ0F298WtGqHFKjbLofpb6osbgycuxbPNupbZDRnMAVABwkoqTJD7q+uWU7XzY1I6WUKFPG68IUPhik5VFjSwd+HedXQeNq5dRcmFoC4usRPcHUFsliCQK3xsA7leJ2WsbCp/XhL33T+GLzVhucHtVNQ3lSxbJOWVZdVFj5C9r8dMKrS0sYiwvF5dT+FSzmN+OwmeOZbI8Ufhik9da1OjcrBrOb1JFKbd79h9Cny8X6W5hkZnekwDkJSFnfEqZoPAZwuYrNxS+6OloA+BH1WwVLAA8dWlTlCpWWMnFpEWb8PWfWltYDgGoGbr+ksKnlIXIRpzxGYSZJFcUvujgtRY1mtcsh1vbKq0nwNAWls8AXB/qHoXP4A+MwmcQZpJcUfgig9f+UuPu9nXQqJraosbcNVkYNmu17pCQbTizKHy6GPmoa55g8j1S+CLnQFYTX1NNj+6ixus/LEfGFu0tLK1yxc8Zn2oyI9hxxmcQZpJcUfgig58HoJlqTro0r4aOjdUWNQxuYZHrL8OFwqeaTAqfQXL+cUXhy5+LM3M9IrrOlO6ixmez12D2yu2u281lEN7CIosbFD4dklFsOePzAGqCXVL48gN/H8BtqnloWascurVRW9QwtIXlaQC9j4ufMz7VhHLGZ5Ccf1xR+PLmQntR455z6qJB1dJKGf5+0SZMMLeFJXcMFD6ljEQ24ozPIMwkuaLw5QV/HwD5RlSpnFC6KB7r3FjJVraw9P1iEXbtz/2E6trVpwBuiGBF4XONMroBhc8gzCS5ovDlBa+1qHFJi+ro0KiyUirnrMnCh/pbWGTT9U8UPqUUODai8DlG5duKysInPWpR/0Tljs1dtl7ZFkAXABN0HESwPSOKaDhupmaFEiheuKDj+rkrbsrej537tGZ7cgpL7i0sRh51G9Q6ASWLFVXq05pNWdi2c4+SLQAeUqBKjnZxCWgJX1zv3lXwQvjeA9Ddu5A99yyXnH8UpRXlR13Po47eAIUvifBTvWkK37EMlwKwFUAxSxMeaQuLkRlfEnlQ+JIIP9WbpvAdy/C9oUcrW/PdT26vjBE8Z3wGM8t3fAZhJskVhe8YeK1FjSTlLtxs7lNYooVC4TOYJAqfQZhJckXhA04H8HOS+Jto9hMAN8ZxROEzQTrkg8JnEGaSXFH4gHcB3J4k/iaalU/s4gk3hc8EaQqfQYrJdRV04bN9USPWFpbcI4vCZ/B3xhmfQZhJchV04bsHwOAksTfR7E0APnbgiMLnAJLTKhQ+p6T8Wy/owmfzosbG0NHyTnY9U/gM/gYpfAZhJslVkIWvNYDZSeJuotmnAPR16IjC5xCUk2oUPieU/F0nyMI3BMAd/k5P1OicbGHhOz6Pkkvh8whsAt0GVfhkUUMeFeVvG4u815P3e04LZ3xOSTmoR+FzAMnnVYIqfD0AvOnz3MQKTw4jkBVdp4XC55SUg3oUPgeQfF4lqMJn86KG7NmTvXtuCoXPDa04dSl8BmEmyZUcHieHb9pW5FEvXTFoufPxH4q2fjD7DsBMl4H0AlDepU2yq8vCk+mjx4z0icJnBCOdkAAJ2ESAwmdTthgrCZCAEQIUPiMY6YQESMAmAhQ+m7LFWEmABIwQoPAZwUgnJEACNhGg8NmULcZKAiRghACFzwhGOiEBErCJAIXPpmwxVhIgASMEKHxGMNIJCZCATQQofDZli7GSAAkYIUDhM4KRTkiABGwiQOGzKVuMlQRIwAgBCp8RjHRCAiRgEwEKn03ZYqwkQAJGCFD4jGCkExIgAZsIUPhsyhZjJQESMEKAwmcEI52QAAnYRIDCZ1O2GCsJkIARAhQ+IxjphARIwCYCFD6bssVYSYAEjBCg8BnBSCckQAI2EaDw2ZQtxkoCJGCEAIXPCEY6IQESsIkAhc+mbDFWEiABIwQofEYw0gkJkIBNBCh8NmWLsZIACRghQOEzgpFOSIAEbCJA4bMpW4yVBEjACAEKnxGMdEICJGATAQqfTdlirCRAAkYIUPiMYKQTEiABmwhQ+GzKFmMlARIwQoDCZwQjnZAACdhEgMJnU7YYKwmQgBECFD4jGOmEBEjAJgIUPpuyxVhJgASMEKDwGcFIJyRAAjYRoPDZlC3GSgIkYIQAhc8IRjohARKwiQCFz6ZsMVYSIAEjBCh8RjDSCQmQgE0EKHw2ZYuxkgAJGCFA4TOCkU5IgARsIkDhsylbjJUESMAIAQqfEYx0QgIkYBMBCp9N2WKsJEACRgj8P/9qkjFpAoX4AAAAAElFTkSuQmCC"></image></symbol> + <!-- svg-source:excalidraw --> + + <defs> + <style> + @font-face { + font-family: "Virgil"; + src: url("https://excalidraw.com/Virgil.woff2"); + } + @font-face { + font-family: "Cascadia"; + src: url("https://excalidraw.com/Cascadia.woff2"); + } + </style> + </defs> + <g transform="translate(30.710678118654755 30.710678118654755) rotate(315 50 50)"><use href="#image-file_A" width="100" height="100"></use></g><g transform="translate(130.71067811865476 30.710678118654755) rotate(45 25 25)"><use href="#image-file_A" width="50" height="50" transform="scale(-1, 1) translate(-50 0)"></use></g><g transform="translate(30.710678118654755 130.71067811865476) rotate(45 50 50)"><use href="#image-file_A" width="100" height="100" transform="scale(1, -1) translate(0 -100)"></use></g><g transform="translate(130.71067811865476 130.71067811865476) rotate(315 25 25)"><use href="#image-file_A" width="50" height="50" transform="scale(-1, -1) translate(-50 -50)"></use></g></svg>
\ No newline at end of file diff --git a/packages/excalidraw/tests/fixtures/test_embedded_v1.png b/packages/excalidraw/tests/fixtures/test_embedded_v1.png Binary files differnew file mode 100644 index 0000000..96c97cf --- /dev/null +++ b/packages/excalidraw/tests/fixtures/test_embedded_v1.png diff --git a/packages/excalidraw/tests/fixtures/test_embedded_v1.svg b/packages/excalidraw/tests/fixtures/test_embedded_v1.svg new file mode 100644 index 0000000..3f26177 --- /dev/null +++ b/packages/excalidraw/tests/fixtures/test_embedded_v1.svg @@ -0,0 +1,16 @@ +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 97 77"> + <!-- svg-source:excalidraw --> + <!-- payload-type:application/vnd.excalidraw+json --><!-- payload-start -->ewogICJ0eXBlIjogImV4Y2FsaWRyYXciLAogICJ2ZXJzaW9uIjogMiwKICAic291cmNlIjogImh0dHBzOi8vZXhjYWxpZHJhdy5jb20iLAogICJlbGVtZW50cyI6IFsKICAgIHsKICAgICAgImlkIjogInRabVFwa0cyQlZ2SzNxT01icHVXeiIsCiAgICAgICJ0eXBlIjogInRleHQiLAogICAgICAieCI6IDg2MS4xMTExMTExMTExMTExLAogICAgICAieSI6IDM1Ni4zMzMzMzMzMzMzMzMzLAogICAgICAid2lkdGgiOiA3NywKICAgICAgImhlaWdodCI6IDU3LAogICAgICAiYW5nbGUiOiAwLAogICAgICAic3Ryb2tlQ29sb3IiOiAiIzAwMDAwMCIsCiAgICAgICJiYWNrZ3JvdW5kQ29sb3IiOiAiIzg2OGU5NiIsCiAgICAgICJmaWxsU3R5bGUiOiAiY3Jvc3MtaGF0Y2giLAogICAgICAic3Ryb2tlV2lkdGgiOiAyLAogICAgICAic3Ryb2tlU3R5bGUiOiAic29saWQiLAogICAgICAicm91Z2huZXNzIjogMSwKICAgICAgIm9wYWNpdHkiOiAxMDAsCiAgICAgICJncm91cElkcyI6IFtdLAogICAgICAic3Ryb2tlU2hhcnBuZXNzIjogInJvdW5kIiwKICAgICAgInNlZWQiOiA0NzYzNjM3OTMsCiAgICAgICJ2ZXJzaW9uIjogMjMsCiAgICAgICJ2ZXJzaW9uTm9uY2UiOiA1OTc0MzUxMzUsCiAgICAgICJpc0RlbGV0ZWQiOiBmYWxzZSwKICAgICAgImJvdW5kRWxlbWVudElkcyI6IG51bGwsCiAgICAgICJ0ZXh0IjogInRlc3QiLAogICAgICAiZm9udFNpemUiOiAzNiwKICAgICAgImZvbnRGYW1pbHkiOiAxLAogICAgICAidGV4dEFsaWduIjogImxlZnQiLAogICAgICAidmVydGljYWxBbGlnbiI6ICJ0b3AiLAogICAgICAiYmFzZWxpbmUiOiA0MQogICAgfQogIF0sCiAgImFwcFN0YXRlIjogewogICAgInZpZXdCYWNrZ3JvdW5kQ29sb3IiOiAiI2ZmZmZmZiIsCiAgICAiZ3JpZFNpemUiOiBudWxsCiAgfQp9<!-- payload-end --> + <defs> + <style> + @font-face { + font-family: "Virgil"; + src: url("https://excalidraw.com/Virgil.woff2"); + } + @font-face { + font-family: "Cascadia"; + src: url("https://excalidraw.com/Cascadia.woff2"); + } + </style> + </defs> + <rect x="0" y="0" width="97" height="77" fill="#ffffff"></rect><g transform="translate(10 10) rotate(0 38.5 28.5)"><text x="0" y="41" font-family="Virgil, Segoe UI Emoji" font-size="36px" fill="#000000" text-anchor="start" style="white-space: pre;" direction="ltr">test</text></g></svg> diff --git a/packages/excalidraw/tests/flip.test.tsx b/packages/excalidraw/tests/flip.test.tsx new file mode 100644 index 0000000..e3f44b3 --- /dev/null +++ b/packages/excalidraw/tests/flip.test.tsx @@ -0,0 +1,895 @@ +import React from "react"; +import { + fireEvent, + GlobalTestState, + render, + screen, + unmountComponent, + waitFor, +} from "./test-utils"; +import { UI, Pointer, Keyboard } from "./helpers/ui"; +import { API } from "./helpers/api"; +import { actionFlipHorizontal, actionFlipVertical } from "../actions"; +import { getElementAbsoluteCoords } from "../element"; +import type { + ExcalidrawElement, + ExcalidrawImageElement, + ExcalidrawLinearElement, + ExcalidrawTextElementWithContainer, + FileId, +} from "../element/types"; +import { newLinearElement } from "../element"; +import { Excalidraw } from "../index"; +import type { NormalizedZoomValue } from "../types"; +import { ROUNDNESS } from "../constants"; +import { vi } from "vitest"; +import { KEYS } from "../keys"; +import { getBoundTextElementPosition } from "../element/textElement"; +import { createPasteEvent } from "../clipboard"; +import { arrayToMap, cloneJSON } from "../utils"; +import type { LocalPoint } from "@excalidraw/math"; +import { pointFrom, type Radians } from "@excalidraw/math"; + +const { h } = window; +const mouse = new Pointer("mouse"); + +vi.mock("../data/blob", async (actual) => { + const orig: Object = await actual(); + return { + ...orig, + resizeImageFile: (imageFile: File) => imageFile, + generateIdFromFile: () => "fileId" as FileId, + }; +}); + +beforeEach(async () => { + unmountComponent(); + + mouse.reset(); + localStorage.clear(); + sessionStorage.clear(); + vi.clearAllMocks(); + + Object.assign(document, { + elementFromPoint: () => GlobalTestState.canvas, + }); + await render(<Excalidraw autoFocus={true} handleKeyboardGlobally={true} />); + API.setAppState({ + zoom: { + value: 1 as NormalizedZoomValue, + }, + }); +}); + +const createAndSelectOneRectangle = (angle: number = 0) => { + UI.createElement("rectangle", { + x: 0, + y: 0, + width: 100, + height: 50, + angle, + }); +}; + +const createAndSelectOneDiamond = (angle: number = 0) => { + UI.createElement("diamond", { + x: 0, + y: 0, + width: 100, + height: 50, + angle, + }); +}; + +const createAndSelectOneEllipse = (angle: number = 0) => { + UI.createElement("ellipse", { + x: 0, + y: 0, + width: 100, + height: 50, + angle, + }); +}; + +const createAndSelectOneArrow = (angle: number = 0) => { + UI.createElement("arrow", { + x: 0, + y: 0, + width: 100, + height: 50, + angle, + }); +}; + +const createAndSelectOneLine = (angle: number = 0) => { + UI.createElement("line", { + x: 0, + y: 0, + width: 100, + height: 50, + angle, + }); +}; + +const createAndReturnOneDraw = (angle: number = 0) => { + return UI.createElement("freedraw", { + x: 0, + y: 0, + width: 50, + height: 100, + angle, + }); +}; + +const createLinearElementWithCurveInsideMinMaxPoints = ( + type: "line" | "arrow", + extraProps: any = {}, +) => { + return newLinearElement({ + type, + x: 2256.910668124894, + y: -2412.5069664197654, + width: 1750.4888916015625, + height: 410.51605224609375, + angle: 0 as Radians, + strokeColor: "#000000", + backgroundColor: "#fa5252", + fillStyle: "hachure", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + groupIds: [], + roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS }, + boundElements: null, + link: null, + locked: false, + points: [ + pointFrom<LocalPoint>(0, 0), + pointFrom<LocalPoint>(-922.4761962890625, 300.3277587890625), + pointFrom<LocalPoint>(828.0126953125, 410.51605224609375), + ], + }); +}; + +const createLinearElementsWithCurveOutsideMinMaxPoints = ( + type: "line" | "arrow", + extraProps: any = {}, +) => { + return newLinearElement({ + type, + x: -1388.6555370382996, + y: 1037.698247710191, + width: 591.2804897585779, + height: 69.32871961377737, + angle: 0, + strokeColor: "#000000", + backgroundColor: "transparent", + fillStyle: "hachure", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + groupIds: [], + roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS }, + boundElements: null, + link: null, + locked: false, + points: [ + [0, 0], + [-584.1485186423079, -15.365636022723947], + [-591.2804897585779, 36.09360810181511], + [-148.56510566829502, 53.96308359105342], + ], + ...extraProps, + }); +}; + +const checkElementsBoundingBox = async ( + element1: ExcalidrawElement, + element2: ExcalidrawElement, + toleranceInPx: number = 0, +) => { + const elementsMap = arrayToMap([element1, element2]); + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element1, elementsMap); + + const [x12, y12, x22, y22] = getElementAbsoluteCoords(element2, elementsMap); + + await waitFor(() => { + // Check if width and height did not change + expect(x2 - x1).toBeCloseTo(x22 - x12, -1); + expect(y2 - y1).toBeCloseTo(y22 - y12, -1); + }); +}; + +const checkHorizontalFlip = async (toleranceInPx: number = 0.00001) => { + const originalElement = cloneJSON(h.elements[0]); + API.executeAction(actionFlipHorizontal); + const newElement = h.elements[0]; + await checkElementsBoundingBox(originalElement, newElement, toleranceInPx); +}; + +const checkTwoPointsLineHorizontalFlip = async () => { + const originalElement = cloneJSON(h.elements[0]) as ExcalidrawLinearElement; + API.executeAction(actionFlipHorizontal); + const newElement = h.elements[0] as ExcalidrawLinearElement; + await waitFor(() => { + expect(originalElement.points[0][0]).toBeCloseTo( + -newElement.points[0][0], + 5, + ); + expect(originalElement.points[0][1]).toBeCloseTo( + newElement.points[0][1], + 5, + ); + expect(originalElement.points[1][0]).toBeCloseTo( + -newElement.points[1][0], + 5, + ); + expect(originalElement.points[1][1]).toBeCloseTo( + newElement.points[1][1], + 5, + ); + }); +}; + +const checkTwoPointsLineVerticalFlip = async () => { + const originalElement = cloneJSON(h.elements[0]) as ExcalidrawLinearElement; + API.executeAction(actionFlipVertical); + const newElement = h.elements[0] as ExcalidrawLinearElement; + await waitFor(() => { + expect(originalElement.points[0][0]).toBeCloseTo( + newElement.points[0][0], + 5, + ); + expect(originalElement.points[0][1]).toBeCloseTo( + -newElement.points[0][1], + 5, + ); + expect(originalElement.points[1][0]).toBeCloseTo( + newElement.points[1][0], + 5, + ); + expect(originalElement.points[1][1]).toBeCloseTo( + -newElement.points[1][1], + 5, + ); + }); +}; + +const checkRotatedHorizontalFlip = async ( + expectedAngle: number, + toleranceInPx: number = 0.00001, +) => { + const originalElement = cloneJSON(h.elements[0]); + API.executeAction(actionFlipHorizontal); + const newElement = h.elements[0]; + await waitFor(() => { + expect(newElement.angle).toBeCloseTo(expectedAngle); + }); + await checkElementsBoundingBox(originalElement, newElement, toleranceInPx); +}; + +const checkRotatedVerticalFlip = async ( + expectedAngle: number, + toleranceInPx: number = 0.00001, +) => { + const originalElement = cloneJSON(h.elements[0]); + API.executeAction(actionFlipVertical); + const newElement = h.elements[0]; + await waitFor(() => { + expect(newElement.angle).toBeCloseTo(expectedAngle); + }); + await checkElementsBoundingBox(originalElement, newElement, toleranceInPx); +}; + +const checkVerticalFlip = async (toleranceInPx: number = 0.00001) => { + const originalElement = cloneJSON(h.elements[0]); + + API.executeAction(actionFlipVertical); + + const newElement = h.elements[0]; + await checkElementsBoundingBox(originalElement, newElement, toleranceInPx); +}; + +const checkVerticalHorizontalFlip = async (toleranceInPx: number = 0.00001) => { + const originalElement = cloneJSON(h.elements[0]); + + API.executeAction(actionFlipHorizontal); + API.executeAction(actionFlipVertical); + + const newElement = h.elements[0]; + await checkElementsBoundingBox(originalElement, newElement, toleranceInPx); +}; + +const TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS = 5; +const MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS = 20; + +// Rectangle element +describe("rectangle", () => { + it("flips an unrotated rectangle horizontally correctly", async () => { + createAndSelectOneRectangle(); + await checkHorizontalFlip(); + }); + + it("flips an unrotated rectangle vertically correctly", async () => { + createAndSelectOneRectangle(); + + await checkVerticalFlip(); + }); + + it("flips a rotated rectangle horizontally correctly", async () => { + const originalAngle = (3 * Math.PI) / 4; + const expectedAngle = (5 * Math.PI) / 4; + + createAndSelectOneRectangle(originalAngle); + + await checkRotatedHorizontalFlip(expectedAngle); + }); + + it("flips a rotated rectangle vertically correctly", async () => { + const originalAngle = (3 * Math.PI) / 4; + const expectedAgnle = (5 * Math.PI) / 4; + + createAndSelectOneRectangle(originalAngle); + + await checkRotatedVerticalFlip(expectedAgnle); + }); +}); + +// Diamond element +describe("diamond", () => { + it("flips an unrotated diamond horizontally correctly", async () => { + createAndSelectOneDiamond(); + + await checkHorizontalFlip(); + }); + + it("flips an unrotated diamond vertically correctly", async () => { + createAndSelectOneDiamond(); + + await checkVerticalFlip(); + }); + + it("flips a rotated diamond horizontally correctly", async () => { + const originalAngle = (5 * Math.PI) / 4; + const expectedAngle = (3 * Math.PI) / 4; + + createAndSelectOneDiamond(originalAngle); + + await checkRotatedHorizontalFlip(expectedAngle); + }); + + it("flips a rotated diamond vertically correctly", async () => { + const originalAngle = (5 * Math.PI) / 4; + const expectedAngle = (3 * Math.PI) / 4; + + createAndSelectOneDiamond(originalAngle); + + await checkRotatedVerticalFlip(expectedAngle); + }); +}); + +// Ellipse element +describe("ellipse", () => { + it("flips an unrotated ellipse horizontally correctly", async () => { + createAndSelectOneEllipse(); + + await checkHorizontalFlip(); + }); + + it("flips an unrotated ellipse vertically correctly", async () => { + createAndSelectOneEllipse(); + + await checkVerticalFlip(); + }); + + it("flips a rotated ellipse horizontally correctly", async () => { + const originalAngle = (7 * Math.PI) / 4; + const expectedAngle = Math.PI / 4; + + createAndSelectOneEllipse(originalAngle); + + await checkRotatedHorizontalFlip(expectedAngle); + }); + + it("flips a rotated ellipse vertically correctly", async () => { + const originalAngle = (7 * Math.PI) / 4; + const expectedAngle = Math.PI / 4; + + createAndSelectOneEllipse(originalAngle); + + await checkRotatedVerticalFlip(expectedAngle); + }); +}); + +// Arrow element +describe("arrow", () => { + it("flips an unrotated arrow horizontally with line inside min/max points bounds", async () => { + const arrow = createLinearElementWithCurveInsideMinMaxPoints("arrow"); + API.setElements([arrow]); + API.setAppState({ selectedElementIds: { [arrow.id]: true } }); + await checkHorizontalFlip( + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); + + it("flips an unrotated arrow vertically with line inside min/max points bounds", async () => { + const arrow = createLinearElementWithCurveInsideMinMaxPoints("arrow"); + API.setElements([arrow]); + API.setAppState({ selectedElementIds: { [arrow.id]: true } }); + + await checkVerticalFlip(50); + }); + + it("flips a rotated arrow horizontally with line inside min/max points bounds", async () => { + const originalAngle = (Math.PI / 4) as Radians; + const expectedAngle = ((7 * Math.PI) / 4) as Radians; + const line = createLinearElementWithCurveInsideMinMaxPoints("arrow"); + API.setElements([line]); + API.setAppState({ + selectedElementIds: { + ...h.state.selectedElementIds, + [line.id]: true, + }, + }); + API.updateElement(line, { + angle: originalAngle, + }); + + await checkRotatedHorizontalFlip( + expectedAngle, + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); + + it("flips a rotated arrow vertically with line inside min/max points bounds", async () => { + const originalAngle = (Math.PI / 4) as Radians; + const expectedAngle = ((7 * Math.PI) / 4) as Radians; + const line = createLinearElementWithCurveInsideMinMaxPoints("arrow"); + API.setElements([line]); + API.setAppState({ + selectedElementIds: { + ...h.state.selectedElementIds, + [line.id]: true, + }, + }); + API.updateElement(line, { + angle: originalAngle, + }); + + await checkRotatedVerticalFlip( + expectedAngle, + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); + + //TODO: elements with curve outside minMax points have a wrong bounding box!!! + it.skip("flips an unrotated arrow horizontally with line outside min/max points bounds", async () => { + const arrow = createLinearElementsWithCurveOutsideMinMaxPoints("arrow"); + API.setElements([arrow]); + API.setAppState({ selectedElementIds: { [arrow.id]: true } }); + + await checkHorizontalFlip( + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); + + //TODO: elements with curve outside minMax points have a wrong bounding box!!! + it.skip("flips a rotated arrow horizontally with line outside min/max points bounds", async () => { + const originalAngle = (Math.PI / 4) as Radians; + const expectedAngle = ((7 * Math.PI) / 4) as Radians; + const line = createLinearElementsWithCurveOutsideMinMaxPoints("arrow"); + API.updateElement(line, { angle: originalAngle }); + API.setElements([line]); + API.setAppState({ selectedElementIds: { [line.id]: true } }); + + await checkRotatedVerticalFlip( + expectedAngle, + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); + + //TODO: elements with curve outside minMax points have a wrong bounding box!!! + it.skip("flips an unrotated arrow vertically with line outside min/max points bounds", async () => { + const arrow = createLinearElementsWithCurveOutsideMinMaxPoints("arrow"); + API.setElements([arrow]); + API.setAppState({ selectedElementIds: { [arrow.id]: true } }); + + await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS); + }); + + //TODO: elements with curve outside minMax points have a wrong bounding box!!! + it.skip("flips a rotated arrow vertically with line outside min/max points bounds", async () => { + const originalAngle = (Math.PI / 4) as Radians; + const expectedAngle = ((7 * Math.PI) / 4) as Radians; + const line = createLinearElementsWithCurveOutsideMinMaxPoints("arrow"); + API.updateElement(line, { angle: originalAngle }); + API.setElements([line]); + API.setAppState({ selectedElementIds: { [line.id]: true } }); + + await checkRotatedVerticalFlip( + expectedAngle, + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); + + it("flips an unrotated arrow horizontally correctly", async () => { + createAndSelectOneArrow(); + await checkHorizontalFlip( + TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); + + it("flips an unrotated arrow vertically correctly", async () => { + createAndSelectOneArrow(); + await checkVerticalFlip(TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS); + }); + + it("flips a two points arrow horizontally correctly", async () => { + createAndSelectOneArrow(); + await checkTwoPointsLineHorizontalFlip(); + }); + + it("flips a two points arrow vertically correctly", async () => { + createAndSelectOneArrow(); + await checkTwoPointsLineVerticalFlip(); + }); +}); + +// Line element +describe("line", () => { + it("flips an unrotated line horizontally with line inside min/max points bounds", async () => { + const line = createLinearElementWithCurveInsideMinMaxPoints("line"); + API.setElements([line]); + API.setAppState({ selectedElementIds: { [line.id]: true } }); + + await checkHorizontalFlip( + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); + + it("flips an unrotated line vertically with line inside min/max points bounds", async () => { + const line = createLinearElementWithCurveInsideMinMaxPoints("line"); + API.setElements([line]); + API.setAppState({ selectedElementIds: { [line.id]: true } }); + + await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS); + }); + + it("flips an unrotated line horizontally correctly", async () => { + createAndSelectOneLine(); + await checkHorizontalFlip( + TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); + //TODO: elements with curve outside minMax points have a wrong bounding box + it.skip("flips an unrotated line horizontally with line outside min/max points bounds", async () => { + const line = createLinearElementsWithCurveOutsideMinMaxPoints("line"); + API.setElements([line]); + API.setAppState({ selectedElementIds: { [line.id]: true } }); + + await checkHorizontalFlip( + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); + + //TODO: elements with curve outside minMax points have a wrong bounding box + it.skip("flips an unrotated line vertically with line outside min/max points bounds", async () => { + const line = createLinearElementsWithCurveOutsideMinMaxPoints("line"); + API.setElements([line]); + API.setAppState({ selectedElementIds: { [line.id]: true } }); + + await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS); + }); + + //TODO: elements with curve outside minMax points have a wrong bounding box + it.skip("flips a rotated line horizontally with line outside min/max points bounds", async () => { + const originalAngle = (Math.PI / 4) as Radians; + const expectedAngle = ((7 * Math.PI) / 4) as Radians; + const line = createLinearElementsWithCurveOutsideMinMaxPoints("line"); + API.updateElement(line, { angle: originalAngle }); + API.setElements([line]); + API.setAppState({ selectedElementIds: { [line.id]: true } }); + + await checkRotatedHorizontalFlip( + expectedAngle, + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); + + //TODO: elements with curve outside minMax points have a wrong bounding box + it.skip("flips a rotated line vertically with line outside min/max points bounds", async () => { + const originalAngle = (Math.PI / 4) as Radians; + const expectedAngle = ((7 * Math.PI) / 4) as Radians; + const line = createLinearElementsWithCurveOutsideMinMaxPoints("line"); + API.updateElement(line, { angle: originalAngle }); + API.setElements([line]); + API.setAppState({ selectedElementIds: { [line.id]: true } }); + + await checkRotatedVerticalFlip( + expectedAngle, + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); + + it("flips an unrotated line vertically correctly", async () => { + createAndSelectOneLine(); + await checkVerticalFlip(TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS); + }); + + it("flips a rotated line horizontally with line inside min/max points bounds", async () => { + const originalAngle = (Math.PI / 4) as Radians; + const expectedAngle = ((7 * Math.PI) / 4) as Radians; + const line = createLinearElementWithCurveInsideMinMaxPoints("line"); + API.setElements([line]); + API.setAppState({ + selectedElementIds: { + ...h.state.selectedElementIds, + [line.id]: true, + }, + }); + API.updateElement(line, { + angle: originalAngle, + }); + + await checkRotatedHorizontalFlip( + expectedAngle, + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); + + it("flips a rotated line vertically with line inside min/max points bounds", async () => { + const originalAngle = (Math.PI / 4) as Radians; + const expectedAngle = ((7 * Math.PI) / 4) as Radians; + const line = createLinearElementWithCurveInsideMinMaxPoints("line"); + API.setElements([line]); + API.setAppState({ + selectedElementIds: { + ...h.state.selectedElementIds, + [line.id]: true, + }, + }); + API.updateElement(line, { + angle: originalAngle, + }); + + await checkRotatedVerticalFlip( + expectedAngle, + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); + + it("flips a two points line horizontally correctly", async () => { + createAndSelectOneLine(); + await checkTwoPointsLineHorizontalFlip(); + }); + + it("flips a two points line vertically correctly", async () => { + createAndSelectOneLine(); + await checkTwoPointsLineVerticalFlip(); + }); +}); + +// Draw element +describe("freedraw", () => { + it("flips an unrotated drawing horizontally correctly", async () => { + const draw = createAndReturnOneDraw(); + // select draw, since not done automatically + API.setAppState({ + selectedElementIds: { + ...h.state.selectedElementIds, + [draw.id]: true, + }, + }); + await checkHorizontalFlip(); + }); + + it("flips an unrotated drawing vertically correctly", async () => { + const draw = createAndReturnOneDraw(); + // select draw, since not done automatically + API.setAppState({ + selectedElementIds: { + ...h.state.selectedElementIds, + [draw.id]: true, + }, + }); + await checkVerticalFlip(); + }); + + it("flips a rotated drawing horizontally correctly", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (7 * Math.PI) / 4; + + const draw = createAndReturnOneDraw(originalAngle); + // select draw, since not done automatically + API.setAppState({ + selectedElementIds: { + ...h.state.selectedElementIds, + [draw.id]: true, + }, + }); + + await checkRotatedHorizontalFlip(expectedAngle); + }); + + it("flips a rotated drawing vertically correctly", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (7 * Math.PI) / 4; + + const draw = createAndReturnOneDraw(originalAngle); + // select draw, since not done automatically + API.setAppState({ + selectedElementIds: { + ...h.state.selectedElementIds, + [draw.id]: true, + }, + }); + + await checkRotatedVerticalFlip(expectedAngle); + }); +}); + +//image +//TODO: currently there is no test for pixel colors at flipped positions. +describe("image", () => { + const createImage = async () => { + const sendPasteEvent = (file?: File) => { + const clipboardEvent = createPasteEvent({ files: file ? [file] : [] }); + document.dispatchEvent(clipboardEvent); + }; + + sendPasteEvent(await API.loadFile("./fixtures/smiley_embedded_v2.png")); + }; + + it("flips an unrotated image horizontally correctly", async () => { + //paste image + await createImage(); + await waitFor(() => { + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]); + expect(API.getSelectedElements().length).toBeGreaterThan(0); + expect(API.getSelectedElements()[0].type).toEqual("image"); + expect(h.app.files.fileId).toBeDefined(); + }); + await checkHorizontalFlip(); + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([-1, 1]); + expect(h.elements[0].angle).toBeCloseTo(0); + }); + + it("flips an unrotated image vertically correctly", async () => { + //paste image + await createImage(); + await waitFor(() => { + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]); + expect(API.getSelectedElements().length).toBeGreaterThan(0); + expect(API.getSelectedElements()[0].type).toEqual("image"); + expect(h.app.files.fileId).toBeDefined(); + }); + + await checkVerticalFlip(); + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, -1]); + expect(h.elements[0].angle).toBeCloseTo(0); + }); + + it("flips an rotated image horizontally correctly", async () => { + const originalAngle = (Math.PI / 4) as Radians; + const expectedAngle = ((7 * Math.PI) / 4) as Radians; + //paste image + await createImage(); + await waitFor(() => { + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]); + expect(API.getSelectedElements().length).toBeGreaterThan(0); + expect(API.getSelectedElements()[0].type).toEqual("image"); + expect(h.app.files.fileId).toBeDefined(); + }); + API.updateElement(h.elements[0], { + angle: originalAngle, + }); + await checkRotatedHorizontalFlip(expectedAngle); + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([-1, 1]); + }); + + it("flips an rotated image vertically correctly", async () => { + const originalAngle = (Math.PI / 4) as Radians; + const expectedAngle = ((7 * Math.PI) / 4) as Radians; + //paste image + await createImage(); + await waitFor(() => { + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]); + expect(h.elements[0].angle).toEqual(0); + expect(API.getSelectedElements().length).toBeGreaterThan(0); + expect(API.getSelectedElements()[0].type).toEqual("image"); + expect(h.app.files.fileId).toBeDefined(); + }); + API.updateElement(h.elements[0], { + angle: originalAngle, + }); + + await checkRotatedVerticalFlip(expectedAngle); + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, -1]); + expect(h.elements[0].angle).toBeCloseTo(expectedAngle); + }); + + it("flips an image both vertically & horizontally", async () => { + //paste image + await createImage(); + await waitFor(() => { + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]); + expect(API.getSelectedElements().length).toBeGreaterThan(0); + expect(API.getSelectedElements()[0].type).toEqual("image"); + expect(h.app.files.fileId).toBeDefined(); + }); + + await checkVerticalHorizontalFlip(); + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([-1, -1]); + expect(h.elements[0].angle).toBeCloseTo(0); + }); +}); + +describe("mutliple elements", () => { + it("with bound text flip correctly", async () => { + UI.clickTool("arrow"); + fireEvent.click(screen.getByTitle("Architect")); + const arrow = UI.createElement("arrow", { + x: 0, + y: 0, + width: 180, + height: 80, + }); + + Keyboard.keyPress(KEYS.ENTER); + let editor = document.querySelector<HTMLTextAreaElement>( + ".excalidraw-textEditorContainer > textarea", + )!; + fireEvent.input(editor, { target: { value: "arrow" } }); + Keyboard.exitTextEditor(editor); + + const rectangle = UI.createElement("rectangle", { + x: 0, + y: 100, + width: 100, + height: 100, + }); + + Keyboard.keyPress(KEYS.ENTER); + editor = document.querySelector<HTMLTextAreaElement>( + ".excalidraw-textEditorContainer > textarea", + )!; + fireEvent.input(editor, { target: { value: "rect\ntext" } }); + Keyboard.exitTextEditor(editor); + + mouse.select([arrow, rectangle]); + API.executeAction(actionFlipHorizontal); + API.executeAction(actionFlipVertical); + + const arrowText = h.elements[1] as ExcalidrawTextElementWithContainer; + const arrowTextPos = getBoundTextElementPosition( + arrow.get(), + arrowText, + arrayToMap(h.elements), + )!; + const rectText = h.elements[3] as ExcalidrawTextElementWithContainer; + + expect(arrow.x).toBeCloseTo(180); + expect(arrow.y).toBeCloseTo(200); + expect(arrow.points[1][0]).toBeCloseTo(-180); + expect(arrow.points[1][1]).toBeCloseTo(-80); + + expect(arrowTextPos.x - (arrow.x - arrow.width)).toBeCloseTo( + arrow.x - (arrowTextPos.x + arrowText.width), + ); + expect(arrowTextPos.y - (arrow.y - arrow.height)).toBeCloseTo( + arrow.y - (arrowTextPos.y + arrowText.height), + ); + + expect(rectangle.x).toBeCloseTo(80); + expect(rectangle.y).toBeCloseTo(0); + + expect(rectText.x - rectangle.x).toBeCloseTo( + rectangle.x + rectangle.width - (rectText.x + rectText.width), + ); + expect(rectText.y - rectangle.y).toBeCloseTo( + rectangle.y + rectangle.height - (rectText.y + rectText.height), + ); + }); +}); diff --git a/packages/excalidraw/tests/fractionalIndex.test.ts b/packages/excalidraw/tests/fractionalIndex.test.ts new file mode 100644 index 0000000..b57af01 --- /dev/null +++ b/packages/excalidraw/tests/fractionalIndex.test.ts @@ -0,0 +1,807 @@ +/* eslint-disable no-lone-blocks */ +import { + syncInvalidIndices, + syncMovedIndices, + validateFractionalIndices, +} from "../fractionalIndex"; +import { API } from "./helpers/api"; +import { arrayToMap } from "../utils"; +import { InvalidFractionalIndexError } from "../errors"; +import type { ExcalidrawElement, FractionalIndex } from "../element/types"; +import { deepCopyElement } from "../element/newElement"; +import { generateKeyBetween } from "fractional-indexing"; + +describe("sync invalid indices with array order", () => { + describe("should NOT sync empty array", () => { + testMovedIndicesSync({ + elements: [], + movedElements: [], + expect: { + unchangedElements: [], + validInput: true, + }, + }); + + testInvalidIndicesSync({ + elements: [], + expect: { + unchangedElements: [], + validInput: true, + }, + }); + }); + + describe("should NOT sync when index is well defined", () => { + testMovedIndicesSync({ + elements: [{ id: "A", index: "a1" }], + movedElements: [], + expect: { + unchangedElements: ["A"], + validInput: true, + }, + }); + + testInvalidIndicesSync({ + elements: [{ id: "A", index: "a1" }], + expect: { + unchangedElements: ["A"], + validInput: true, + }, + }); + }); + + describe("should NOT sync when indices are well defined", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a2" }, + { id: "C", index: "a3" }, + ], + movedElements: [], + expect: { + unchangedElements: ["A", "B", "C"], + validInput: true, + }, + }); + + testInvalidIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a2" }, + { id: "C", index: "a3" }, + ], + expect: { + unchangedElements: ["A", "B", "C"], + validInput: true, + }, + }); + }); + + describe("should sync when fractional index is not defined", () => { + testMovedIndicesSync({ + elements: [{ id: "A" }], + movedElements: ["A"], + expect: { + unchangedElements: [], + }, + }); + + testInvalidIndicesSync({ + elements: [{ id: "A" }], + expect: { + unchangedElements: [], + }, + }); + }); + + describe("should sync when fractional indices are duplicated", () => { + testInvalidIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a1" }, + ], + expect: { + unchangedElements: ["A"], + }, + }); + + testInvalidIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a1" }, + ], + expect: { + unchangedElements: ["A"], + }, + }); + }); + + describe("should sync when a fractional index is out of order", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a2" }, + { id: "B", index: "a1" }, + ], + movedElements: ["B"], + expect: { + unchangedElements: ["A"], + }, + }); + + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a2" }, + { id: "B", index: "a1" }, + ], + movedElements: ["A"], + expect: { + unchangedElements: ["B"], + }, + }); + + testInvalidIndicesSync({ + elements: [ + { id: "A", index: "a2" }, + { id: "B", index: "a1" }, + ], + expect: { + unchangedElements: ["A"], + }, + }); + }); + + describe("should sync when fractional indices are out of order", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a3" }, + { id: "B", index: "a2" }, + { id: "C", index: "a1" }, + ], + movedElements: ["B", "C"], + expect: { + unchangedElements: ["A"], + }, + }); + + testInvalidIndicesSync({ + elements: [ + { id: "A", index: "a3" }, + { id: "B", index: "a2" }, + { id: "C", index: "a1" }, + ], + expect: { + unchangedElements: ["A"], + }, + }); + }); + + describe("should sync when incorrect fractional index is in between correct ones ", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a0" }, + { id: "C", index: "a2" }, + ], + movedElements: ["B"], + expect: { + unchangedElements: ["A", "C"], + }, + }); + + testInvalidIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a0" }, + { id: "C", index: "a2" }, + ], + expect: { + unchangedElements: ["A", "C"], + }, + }); + }); + + describe("should sync when incorrect fractional index is on top and duplicated below", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a2" }, + { id: "C", index: "a1" }, + ], + movedElements: ["C"], + expect: { + unchangedElements: ["A", "B"], + }, + }); + + testInvalidIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a2" }, + { id: "C", index: "a1" }, + ], + expect: { + unchangedElements: ["A", "B"], + }, + }); + }); + + describe("should sync when given a mix of duplicate / invalid indices", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a0" }, + { id: "B", index: "a2" }, + { id: "C", index: "a1" }, + { id: "D", index: "a1" }, + { id: "E", index: "a2" }, + ], + movedElements: ["C", "D", "E"], + expect: { + unchangedElements: ["A", "B"], + }, + }); + + testInvalidIndicesSync({ + elements: [ + { id: "A", index: "a0" }, + { id: "B", index: "a2" }, + { id: "C", index: "a1" }, + { id: "D", index: "a1" }, + { id: "E", index: "a2" }, + ], + expect: { + unchangedElements: ["A", "B"], + }, + }); + }); + + describe("should sync when given a mix of undefined / invalid indices", () => { + testMovedIndicesSync({ + elements: [ + { id: "A" }, + { id: "B" }, + { id: "C", index: "a0" }, + { id: "D", index: "a2" }, + { id: "E" }, + { id: "F", index: "a3" }, + { id: "G" }, + { id: "H", index: "a1" }, + { id: "I", index: "a2" }, + { id: "J" }, + ], + movedElements: ["A", "B", "E", "G", "H", "I", "J"], + expect: { + unchangedElements: ["C", "D", "F"], + }, + }); + + testInvalidIndicesSync({ + elements: [ + { id: "A" }, + { id: "B" }, + { id: "C", index: "a0" }, + { id: "D", index: "a2" }, + { id: "E" }, + { id: "F", index: "a3" }, + { id: "G" }, + { id: "H", index: "a1" }, + { id: "I", index: "a2" }, + { id: "J" }, + ], + expect: { + unchangedElements: ["C", "D", "F"], + }, + }); + }); + + describe("should sync all moved elements regardless of their validity", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a2" }, + { id: "B", index: "a4" }, + ], + movedElements: ["A"], + expect: { + validInput: true, + unchangedElements: ["B"], + }, + }); + + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a2" }, + { id: "B", index: "a4" }, + ], + movedElements: ["B"], + expect: { + validInput: true, + unchangedElements: ["A"], + }, + }); + + testMovedIndicesSync({ + elements: [ + { id: "C", index: "a2" }, + { id: "D", index: "a3" }, + { id: "A", index: "a0" }, + { id: "B", index: "a1" }, + ], + movedElements: ["C", "D"], + expect: { + unchangedElements: ["A", "B"], + }, + }); + + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a2" }, + { id: "D", index: "a4" }, + { id: "C", index: "a3" }, + { id: "F", index: "a6" }, + { id: "E", index: "a5" }, + { id: "H", index: "a8" }, + { id: "G", index: "a7" }, + { id: "I", index: "a9" }, + ], + movedElements: ["D", "F", "H"], + expect: { + unchangedElements: ["A", "B", "C", "E", "G", "I"], + }, + }); + + { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a0" }, + { id: "C", index: "a2" }, + ], + movedElements: ["B", "C"], + expect: { + unchangedElements: ["A"], + }, + }); + + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a0" }, + { id: "C", index: "a2" }, + ], + movedElements: ["A", "B"], + expect: { + unchangedElements: ["C"], + }, + }); + } + + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a0" }, + { id: "B", index: "a2" }, + { id: "C", index: "a1" }, + { id: "D", index: "a1" }, + { id: "E", index: "a2" }, + ], + movedElements: ["B", "D", "E"], + expect: { + unchangedElements: ["A", "C"], + }, + }); + + testMovedIndicesSync({ + elements: [ + { id: "A" }, + { id: "B" }, + { id: "C", index: "a0" }, + { id: "D", index: "a2" }, + { id: "E" }, + { id: "F", index: "a3" }, + { id: "G" }, + { id: "H", index: "a1" }, + { id: "I", index: "a2" }, + { id: "J" }, + ], + movedElements: ["A", "B", "D", "E", "F", "G", "J"], + expect: { + unchangedElements: ["C", "H", "I"], + }, + }); + }); + + describe("should generate fractions for explicitly moved elements", () => { + describe("should generate a fraction between 'A' and 'C'", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + // doing actual fractions, without jitter 'a1' becomes 'a1V' + // as V is taken as the charset's middle-right value + { id: "B", index: "a1" }, + { id: "C", index: "a2" }, + ], + movedElements: ["B"], + expect: { + unchangedElements: ["A", "C"], + }, + }); + + testInvalidIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a1" }, + { id: "C", index: "a2" }, + ], + expect: { + // as above, B will become fractional + unchangedElements: ["A", "C"], + }, + }); + }); + + describe("should generate fractions given duplicated indices", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a01" }, + { id: "B", index: "a01" }, + { id: "C", index: "a01" }, + { id: "D", index: "a01" }, + { id: "E", index: "a02" }, + { id: "F", index: "a02" }, + { id: "G", index: "a02" }, + ], + movedElements: ["B", "C", "D", "E", "F"], + expect: { + unchangedElements: ["A", "G"], + }, + }); + + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a01" }, + { id: "B", index: "a01" }, + { id: "C", index: "a01" }, + { id: "D", index: "a01" }, + { id: "E", index: "a02" }, + { id: "F", index: "a02" }, + { id: "G", index: "a02" }, + ], + movedElements: ["A", "C", "D", "E", "G"], + expect: { + unchangedElements: ["B", "F"], + }, + }); + + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a01" }, + { id: "B", index: "a01" }, + { id: "C", index: "a01" }, + { id: "D", index: "a01" }, + { id: "E", index: "a02" }, + { id: "F", index: "a02" }, + { id: "G", index: "a02" }, + ], + movedElements: ["B", "C", "D", "F", "G"], + expect: { + unchangedElements: ["A", "E"], + }, + }); + + testInvalidIndicesSync({ + elements: [ + { id: "A", index: "a01" }, + { id: "B", index: "a01" }, + { id: "C", index: "a01" }, + { id: "D", index: "a01" }, + { id: "E", index: "a02" }, + { id: "F", index: "a02" }, + { id: "G", index: "a02" }, + ], + expect: { + // notice fallback considers first item (E) as a valid one + unchangedElements: ["A", "E"], + }, + }); + }); + }); + + describe("should be able to sync 20K invalid indices", () => { + const length = 20_000; + + describe("should sync all empty indices", () => { + const elements = Array.from({ length }).map((_, index) => ({ + id: `A_${index}`, + })); + + testMovedIndicesSync({ + // elements without fractional index + elements, + movedElements: Array.from({ length }).map((_, index) => `A_${index}`), + expect: { + unchangedElements: [], + }, + }); + + testInvalidIndicesSync({ + // elements without fractional index + elements, + expect: { + unchangedElements: [], + }, + }); + }); + + describe("should sync all but last index given a growing array of indices", () => { + let lastIndex: string | null = null; + + const elements = Array.from({ length }).map((_, index) => { + // going up from 'a0' + lastIndex = generateKeyBetween(lastIndex, null); + + return { + id: `A_${index}`, + // assigning the last generated index, so sync can go down from there + // without jitter lastIndex is 'c4BZ' for 20000th element + index: index === length - 1 ? lastIndex : undefined, + }; + }); + const movedElements = Array.from({ length }).map( + (_, index) => `A_${index}`, + ); + // remove last element + movedElements.pop(); + + testMovedIndicesSync({ + elements, + movedElements, + expect: { + unchangedElements: [`A_${length - 1}`], + }, + }); + + testInvalidIndicesSync({ + elements, + expect: { + unchangedElements: [`A_${length - 1}`], + }, + }); + }); + + describe("should sync all but first index given a declining array of indices", () => { + let lastIndex: string | null = null; + + const elements = Array.from({ length }).map((_, index) => { + // going down from 'a0' + lastIndex = generateKeyBetween(null, lastIndex); + + return { + id: `A_${index}`, + // without jitter lastIndex is 'XvoR' for 20000th element + index: lastIndex, + }; + }); + const movedElements = Array.from({ length }).map( + (_, index) => `A_${index}`, + ); + // remove first element + movedElements.shift(); + + testMovedIndicesSync({ + elements, + movedElements, + expect: { + unchangedElements: [`A_0`], + }, + }); + + testInvalidIndicesSync({ + elements, + expect: { + unchangedElements: [`A_0`], + }, + }); + }); + }); + + describe("should automatically fallback to fixing all invalid indices", () => { + describe("should fallback to syncing duplicated indices when moved elements are empty", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a1" }, + { id: "C", index: "a1" }, + ], + // the validation will throw as nothing was synced + // therefore it will lead to triggering the fallback and fixing all invalid indices + movedElements: [], + expect: { + unchangedElements: ["A"], + }, + }); + }); + + describe("should fallback to syncing undefined / invalid indices when moved elements are empty", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B" }, + { id: "C", index: "a0" }, + ], + // since elements are invalid, this will fail the validation + // leading to fallback fixing "B" and "C" + movedElements: [], + expect: { + unchangedElements: ["A"], + }, + }); + }); + + describe("should fallback to syncing unordered indices when moved element is invalid", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a1" }, + { id: "B", index: "a2" }, + { id: "C", index: "a1" }, + ], + movedElements: ["A"], + expect: { + unchangedElements: ["A", "B"], + }, + }); + }); + + describe("should fallback when trying to generate an index in between unordered elements", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a2" }, + { id: "B" }, + { id: "C", index: "a1" }, + ], + // 'B' is invalid, but so is 'C', which was not marked as moved + // therefore it will try to generate a key between 'a2' and 'a1' + // which it cannot do, thus will throw during generation and automatically fallback + movedElements: ["B"], + expect: { + unchangedElements: ["A"], + }, + }); + }); + + describe("should fallback when trying to generate an index in between duplicate indices", () => { + testMovedIndicesSync({ + elements: [ + { id: "A", index: "a01" }, + { id: "B" }, + { id: "C" }, + { id: "D", index: "a01" }, + { id: "E", index: "a01" }, + { id: "F", index: "a01" }, + { id: "G" }, + { id: "I", index: "a03" }, + { id: "H" }, + ], + // missed "E" therefore upper bound for 'B' is a01, while lower bound is 'a02' + // therefore, similarly to above, it will fail during key generation and lead to fallback + movedElements: ["B", "C", "D", "F", "G", "H"], + expect: { + unchangedElements: ["A", "I"], + }, + }); + }); + }); +}); + +function testMovedIndicesSync(args: { + elements: { id: string; index?: string }[]; + movedElements: string[]; + expect: { + unchangedElements: string[]; + validInput?: true; + }; +}) { + const [elements, movedElements] = prepareArguments( + args.elements, + args.movedElements, + ); + const expectUnchangedElements = arrayToMap( + args.expect.unchangedElements.map((x) => ({ id: x })), + ); + + test( + "should sync invalid indices of moved elements or fallback", + elements, + movedElements, + expectUnchangedElements, + args.expect.validInput, + ); +} + +function testInvalidIndicesSync(args: { + elements: { id: string; index?: string }[]; + expect: { + unchangedElements: string[]; + validInput?: true; + }; +}) { + const [elements] = prepareArguments(args.elements); + const expectUnchangedElements = arrayToMap( + args.expect.unchangedElements.map((x) => ({ id: x })), + ); + + test( + "should sync invalid indices of all elements", + elements, + undefined, + expectUnchangedElements, + args.expect.validInput, + ); +} + +function prepareArguments( + elementsLike: { id: string; index?: string }[], + movedElementsIds?: string[], +): [ExcalidrawElement[], Map<string, ExcalidrawElement> | undefined] { + const elements = elementsLike.map((x) => + API.createElement({ id: x.id, index: x.index as FractionalIndex }), + ); + const movedMap = arrayToMap(movedElementsIds || []); + const movedElements = movedElementsIds + ? arrayToMap(elements.filter((x) => movedMap.has(x.id))) + : undefined; + + return [elements, movedElements]; +} + +function test( + name: string, + elements: ExcalidrawElement[], + movedElements: Map<string, ExcalidrawElement> | undefined, + expectUnchangedElements: Map<string, { id: string }>, + expectValidInput?: boolean, +) { + it(name, () => { + // ensure the input is invalid (unless the flag is on) + if (!expectValidInput) { + expect(() => + validateFractionalIndices(elements, { + shouldThrow: true, + includeBoundTextValidation: true, + ignoreLogs: true, + }), + ).toThrowError(InvalidFractionalIndexError); + } + + // clone due to mutation + const clonedElements = elements.map((x) => deepCopyElement(x)); + + // act + const syncedElements = movedElements + ? syncMovedIndices(clonedElements, movedElements) + : syncInvalidIndices(clonedElements); + + expect(syncedElements.length).toBe(elements.length); + expect(() => + validateFractionalIndices(syncedElements, { + shouldThrow: true, + includeBoundTextValidation: true, + ignoreLogs: true, + }), + ).not.toThrowError(InvalidFractionalIndexError); + + syncedElements.forEach((synced, index) => { + const element = elements[index]; + // ensure the order hasn't changed + expect(synced.id).toBe(element.id); + + if (expectUnchangedElements.has(synced.id)) { + // ensure we didn't mutate where we didn't want to mutate + expect(synced.index).toBe(elements[index].index); + expect(synced.version).toBe(elements[index].version); + } else { + expect(synced.index).not.toBe(elements[index].index); + // ensure we mutated just once, even with fallback triggered + expect(synced.version).toBe(elements[index].version + 1); + } + }); + }); +} diff --git a/packages/excalidraw/tests/helpers/api.ts b/packages/excalidraw/tests/helpers/api.ts new file mode 100644 index 0000000..218f371 --- /dev/null +++ b/packages/excalidraw/tests/helpers/api.ts @@ -0,0 +1,511 @@ +import type { + ExcalidrawElement, + ExcalidrawGenericElement, + ExcalidrawTextElement, + ExcalidrawLinearElement, + ExcalidrawFreeDrawElement, + ExcalidrawImageElement, + FileId, + ExcalidrawFrameElement, + ExcalidrawElementType, + ExcalidrawMagicFrameElement, + ExcalidrawElbowArrowElement, + ExcalidrawArrowElement, + FixedSegment, +} from "../../element/types"; +import { newElement, newTextElement, newLinearElement } from "../../element"; +import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS } from "../../constants"; +import { getDefaultAppState } from "../../appState"; +import { GlobalTestState, createEvent, fireEvent, act } from "../test-utils"; +import fs from "fs"; +import util from "util"; +import path from "path"; +import { getMimeType } from "../../data/blob"; +import { + newArrowElement, + newEmbeddableElement, + newFrameElement, + newFreeDrawElement, + newIframeElement, + newImageElement, + newMagicFrameElement, +} from "../../element/newElement"; +import type { AppState } from "../../types"; +import { getSelectedElements } from "../../scene/selection"; +import { isLinearElementType } from "../../element/typeChecks"; +import type { Mutable } from "../../utility-types"; +import { assertNever } from "../../utils"; +import type App from "../../components/App"; +import { createTestHook } from "../../components/App"; +import type { Action } from "../../actions/types"; +import { mutateElement } from "../../element/mutateElement"; +import { pointFrom, type LocalPoint, type Radians } from "@excalidraw/math"; +import { selectGroupsForSelectedElements } from "../../groups"; + +const readFile = util.promisify(fs.readFile); +// so that window.h is available when App.tsx is not imported as well. +createTestHook(); + +const { h } = window; + +export class API { + static updateScene: InstanceType<typeof App>["updateScene"] = (...args) => { + act(() => { + h.app.updateScene(...args); + }); + }; + static setAppState: React.Component<any, AppState>["setState"] = ( + state, + cb, + ) => { + act(() => { + h.setState(state, cb); + }); + }; + + static setElements = (elements: readonly ExcalidrawElement[]) => { + act(() => { + h.elements = elements; + }); + }; + + static setSelectedElements = (elements: ExcalidrawElement[], editingGroupId?: string | null) => { + act(() => { + h.setState({ + ...selectGroupsForSelectedElements( + { + editingGroupId: editingGroupId ?? null, + selectedElementIds: elements.reduce((acc, element) => { + acc[element.id] = true; + return acc; + }, {} as Record<ExcalidrawElement["id"], true>), + }, + elements, + h.state, + h.app, + ) + }); + }); + }; + + // eslint-disable-next-line prettier/prettier + static updateElement = <T extends ExcalidrawElement>( + ...args: Parameters<typeof mutateElement<T>> + ) => { + act(() => { + mutateElement<T>(...args); + }); + }; + + static getSelectedElements = ( + includeBoundTextElement: boolean = false, + includeElementsInFrames: boolean = false, + ): ExcalidrawElement[] => { + return getSelectedElements(h.elements, h.state, { + includeBoundTextElement, + includeElementsInFrames, + }); + }; + + static getSelectedElement = (): ExcalidrawElement => { + const selectedElements = API.getSelectedElements(); + if (selectedElements.length !== 1) { + throw new Error( + `expected 1 selected element; got ${selectedElements.length}`, + ); + } + return selectedElements[0]; + }; + + static getUndoStack = () => { + // @ts-ignore + return h.history.undoStack; + }; + + static getRedoStack = () => { + // @ts-ignore + return h.history.redoStack; + }; + + static getSnapshot = () => { + return Array.from(h.store.snapshot.elements.values()); + }; + + static clearSelection = () => { + act(() => { + // @ts-ignore + h.app.clearSelection(null); + }); + expect(API.getSelectedElements().length).toBe(0); + }; + + static getElement = <T extends ExcalidrawElement>(element: T): T => { + return h.app.scene.getElementsMapIncludingDeleted().get(element.id) as T || element; + } + + static createElement = < + T extends Exclude<ExcalidrawElementType, "selection"> = "rectangle", + >({ + // @ts-ignore + type = "rectangle", + id, + x = 0, + y = x, + width = 100, + height = width, + isDeleted = false, + groupIds = [], + ...rest + }: { + type?: T; + x?: number; + y?: number; + height?: number; + width?: number; + angle?: number; + id?: string; + isDeleted?: boolean; + frameId?: ExcalidrawElement["id"] | null; + index?: ExcalidrawElement["index"]; + groupIds?: ExcalidrawElement["groupIds"]; + // generic element props + strokeColor?: ExcalidrawGenericElement["strokeColor"]; + backgroundColor?: ExcalidrawGenericElement["backgroundColor"]; + fillStyle?: ExcalidrawGenericElement["fillStyle"]; + strokeWidth?: ExcalidrawGenericElement["strokeWidth"]; + strokeStyle?: ExcalidrawGenericElement["strokeStyle"]; + roundness?: ExcalidrawGenericElement["roundness"]; + roughness?: ExcalidrawGenericElement["roughness"]; + opacity?: ExcalidrawGenericElement["opacity"]; + // text props + text?: T extends "text" ? ExcalidrawTextElement["text"] : never; + fontSize?: T extends "text" ? ExcalidrawTextElement["fontSize"] : never; + fontFamily?: T extends "text" ? ExcalidrawTextElement["fontFamily"] : never; + textAlign?: T extends "text" ? ExcalidrawTextElement["textAlign"] : never; + verticalAlign?: T extends "text" + ? ExcalidrawTextElement["verticalAlign"] + : never; + boundElements?: ExcalidrawGenericElement["boundElements"]; + containerId?: T extends "text" + ? ExcalidrawTextElement["containerId"] + : never; + points?: T extends "arrow" | "line" | "freedraw" ? readonly LocalPoint[] : never; + locked?: boolean; + fileId?: T extends "image" ? string : never; + scale?: T extends "image" ? ExcalidrawImageElement["scale"] : never; + status?: T extends "image" ? ExcalidrawImageElement["status"] : never; + startBinding?: T extends "arrow" + ? ExcalidrawArrowElement["startBinding"] | ExcalidrawElbowArrowElement["startBinding"] + : never; + endBinding?: T extends "arrow" + ? ExcalidrawArrowElement["endBinding"] | ExcalidrawElbowArrowElement["endBinding"] + : never; + startArrowhead?: T extends "arrow" + ? ExcalidrawArrowElement["startArrowhead"] | ExcalidrawElbowArrowElement["startArrowhead"] + : never; + endArrowhead?: T extends "arrow" + ? ExcalidrawArrowElement["endArrowhead"] | ExcalidrawElbowArrowElement["endArrowhead"] + : never; + elbowed?: boolean; + fixedSegments?: FixedSegment[] | null; + }): T extends "arrow" | "line" + ? ExcalidrawLinearElement + : T extends "freedraw" + ? ExcalidrawFreeDrawElement + : T extends "text" + ? ExcalidrawTextElement + : T extends "image" + ? ExcalidrawImageElement + : T extends "frame" + ? ExcalidrawFrameElement + : T extends "magicframe" + ? ExcalidrawMagicFrameElement + : ExcalidrawGenericElement => { + let element: Mutable<ExcalidrawElement> = null!; + + const appState = h?.state || getDefaultAppState(); + + const base: Omit< + ExcalidrawGenericElement, + | "id" + | "type" + | "version" + | "versionNonce" + | "isDeleted" + | "groupIds" + | "link" + | "updated" + > = { + seed: 1, + x, + y, + width, + height, + frameId: rest.frameId ?? null, + index: rest.index ?? null, + angle: (rest.angle ?? 0) as Radians, + strokeColor: rest.strokeColor ?? appState.currentItemStrokeColor, + backgroundColor: + rest.backgroundColor ?? appState.currentItemBackgroundColor, + fillStyle: rest.fillStyle ?? appState.currentItemFillStyle, + strokeWidth: rest.strokeWidth ?? appState.currentItemStrokeWidth, + strokeStyle: rest.strokeStyle ?? appState.currentItemStrokeStyle, + roundness: ( + rest.roundness === undefined + ? appState.currentItemRoundness === "round" + : rest.roundness + ) + ? { + type: isLinearElementType(type) + ? ROUNDNESS.PROPORTIONAL_RADIUS + : ROUNDNESS.ADAPTIVE_RADIUS, + } + : null, + roughness: rest.roughness ?? appState.currentItemRoughness, + opacity: rest.opacity ?? appState.currentItemOpacity, + boundElements: rest.boundElements ?? null, + locked: rest.locked ?? false, + }; + switch (type) { + case "rectangle": + case "diamond": + case "ellipse": + element = newElement({ + type: type as "rectangle" | "diamond" | "ellipse", + ...base, + }); + break; + case "embeddable": + element = newEmbeddableElement({ + type: "embeddable", + ...base, + }); + break; + case "iframe": + element = newIframeElement({ + type: "iframe", + ...base, + }); + break; + case "text": + const fontSize = rest.fontSize ?? appState.currentItemFontSize; + const fontFamily = rest.fontFamily ?? appState.currentItemFontFamily; + element = newTextElement({ + ...base, + text: rest.text || "test", + fontSize, + fontFamily, + textAlign: rest.textAlign ?? appState.currentItemTextAlign, + verticalAlign: rest.verticalAlign ?? DEFAULT_VERTICAL_ALIGN, + containerId: rest.containerId ?? undefined, + }); + element.width = width; + element.height = height; + break; + case "freedraw": + element = newFreeDrawElement({ + type: type as "freedraw", + simulatePressure: true, + points: rest.points, + ...base, + }); + break; + case "arrow": + element = newArrowElement({ + ...base, + width, + height, + type, + points: rest.points ?? [ + pointFrom<LocalPoint>(0, 0), + pointFrom<LocalPoint>(100, 100), + ], + elbowed: rest.elbowed ?? false, + }); + break; + case "line": + element = newLinearElement({ + ...base, + width, + height, + type, + points: rest.points ?? [ + pointFrom<LocalPoint>(0, 0), + pointFrom<LocalPoint>(100, 100), + ], + }); + break; + case "image": + element = newImageElement({ + ...base, + width, + height, + type, + fileId: (rest.fileId as string as FileId) ?? null, + status: rest.status || "saved", + scale: rest.scale || [1, 1], + }); + break; + case "frame": + element = newFrameElement({ ...base, width, height }); + break; + case "magicframe": + element = newMagicFrameElement({ ...base, width, height }); + break; + default: + assertNever( + type, + `API.createElement: unimplemented element type ${type}}`, + ); + break; + } + if (element.type === "arrow") { + element.startBinding = rest.startBinding ?? null; + element.endBinding = rest.endBinding ?? null; + element.startArrowhead = rest.startArrowhead ?? null; + element.endArrowhead = rest.endArrowhead ?? null; + } + if (id) { + element.id = id; + } + if (isDeleted) { + element.isDeleted = isDeleted; + } + if (groupIds) { + element.groupIds = groupIds; + } + return element as any; + }; + + static createTextContainer = (opts?: { + frameId?: ExcalidrawElement["id"]; + groupIds?: ExcalidrawElement["groupIds"]; + label?: { + text?: string; + frameId?: ExcalidrawElement["id"] | null; + groupIds?: ExcalidrawElement["groupIds"]; + }; + }) => { + const rectangle = API.createElement({ + type: "rectangle", + frameId: opts?.frameId || null, + groupIds: opts?.groupIds, + }); + + const text = API.createElement({ + type: "text", + text: opts?.label?.text || "sample-text", + width: 50, + height: 20, + fontSize: 16, + containerId: rectangle.id, + frameId: + opts?.label?.frameId === undefined + ? opts?.frameId ?? null + : opts?.label?.frameId ?? null, + groupIds: opts?.label?.groupIds === undefined + ? opts?.groupIds + : opts?.label?.groupIds , + + }); + + mutateElement( + rectangle, + { + boundElements: [{ type: "text", id: text.id }], + }, + false, + ); + + return [rectangle, text]; + }; + + static createLabeledArrow = (opts?: { + frameId?: ExcalidrawElement["id"]; + label?: { + text?: string; + frameId?: ExcalidrawElement["id"] | null; + }; + }) => { + const arrow = API.createElement({ + type: "arrow", + frameId: opts?.frameId || null, + }); + + const text = API.createElement({ + type: "text", + id: "text2", + width: 50, + height: 20, + containerId: arrow.id, + frameId: + opts?.label?.frameId === undefined + ? opts?.frameId ?? null + : opts?.label?.frameId ?? null, + }); + + mutateElement( + arrow, + { + boundElements: [{ type: "text", id: text.id }], + }, + false, + ); + + return [arrow, text]; + }; + + static readFile = async <T extends "utf8" | null>( + filepath: string, + encoding?: T, + ): Promise<T extends "utf8" ? string : Buffer> => { + filepath = path.isAbsolute(filepath) + ? filepath + : path.resolve(path.join(__dirname, "../", filepath)); + return readFile(filepath, { encoding }) as any; + }; + + static loadFile = async (filepath: string) => { + const { base, ext } = path.parse(filepath); + return new File([await API.readFile(filepath, null)], base, { + type: getMimeType(ext), + }); + }; + + static drop = async (blob: Blob) => { + const fileDropEvent = createEvent.drop(GlobalTestState.interactiveCanvas); + const text = await new Promise<string>((resolve, reject) => { + try { + const reader = new FileReader(); + reader.onload = () => { + resolve(reader.result as string); + }; + reader.readAsText(blob); + } catch (error: any) { + reject(error); + } + }); + + const files = [blob] as File[] & { item: (index: number) => File }; + files.item = (index: number) => files[index]; + + Object.defineProperty(fileDropEvent, "dataTransfer", { + value: { + files, + getData: (type: string) => { + if (type === blob.type) { + return text; + } + return ""; + }, + }, + }); + await fireEvent(GlobalTestState.interactiveCanvas, fileDropEvent); + }; + + static executeAction = (action: Action) => { + act(() => { + h.app.actionManager.executeAction(action); + }); + }; +} diff --git a/packages/excalidraw/tests/helpers/colorize.ts b/packages/excalidraw/tests/helpers/colorize.ts new file mode 100644 index 0000000..ca9c39d --- /dev/null +++ b/packages/excalidraw/tests/helpers/colorize.ts @@ -0,0 +1,2 @@ +export const yellow = (str: string) => `\u001b[33m${str}\u001b[0m`; +export const red = (str: string) => `\u001b[31m${str}\u001b[0m`; diff --git a/packages/excalidraw/tests/helpers/mocks.ts b/packages/excalidraw/tests/helpers/mocks.ts new file mode 100644 index 0000000..a87523e --- /dev/null +++ b/packages/excalidraw/tests/helpers/mocks.ts @@ -0,0 +1,32 @@ +import { vi } from "vitest"; +import * as MermaidToExcalidraw from "@excalidraw/mermaid-to-excalidraw"; +import type { parseMermaidToExcalidraw } from "@excalidraw/mermaid-to-excalidraw"; +import React from "react"; + +export const mockMermaidToExcalidraw = (opts: { + parseMermaidToExcalidraw: typeof parseMermaidToExcalidraw; + mockRef?: boolean; +}) => { + vi.mock("@excalidraw/mermaid-to-excalidraw", async (importActual) => { + const module = (await importActual()) as any; + + return { + __esModule: true, + ...module, + }; + }); + const parseMermaidToExcalidrawSpy = vi.spyOn( + MermaidToExcalidraw, + "parseMermaidToExcalidraw", + ); + + parseMermaidToExcalidrawSpy.mockImplementation(opts.parseMermaidToExcalidraw); + + if (opts.mockRef) { + vi.spyOn(React, "useRef").mockReturnValue({ + current: { + parseMermaidToExcalidraw: parseMermaidToExcalidrawSpy, + }, + }); + } +}; diff --git a/packages/excalidraw/tests/helpers/polyfills.ts b/packages/excalidraw/tests/helpers/polyfills.ts new file mode 100644 index 0000000..967e8d4 --- /dev/null +++ b/packages/excalidraw/tests/helpers/polyfills.ts @@ -0,0 +1,95 @@ +import { URL } from "node:url"; + +class ClipboardEvent { + constructor( + type: "paste" | "copy", + eventInitDict: { + clipboardData: DataTransfer; + }, + ) { + return Object.assign( + new Event("paste", { + bubbles: true, + cancelable: true, + composed: true, + }), + { + clipboardData: eventInitDict.clipboardData, + }, + ) as any as ClipboardEvent; + } +} + +type DataKind = "string" | "file"; + +class DataTransferItem { + kind: DataKind; + type: string; + data: string | Blob; + + constructor(kind: DataKind, type: string, data: string | Blob) { + this.kind = kind; + this.type = type; + this.data = data; + } + + getAsString(callback: (data: string) => void): void { + if (this.kind === "string") { + callback(this.data as string); + } + } + + getAsFile(): File | null { + if (this.kind === "file" && this.data instanceof File) { + return this.data; + } + return null; + } +} + +class DataTransferList { + items: DataTransferItem[] = []; + + add(data: string | File, type: string = ""): void { + if (typeof data === "string") { + this.items.push(new DataTransferItem("string", type, data)); + } else if (data instanceof File) { + this.items.push(new DataTransferItem("file", type, data)); + } + } + + clear(): void { + this.items = []; + } +} + +class DataTransfer { + public items: DataTransferList = new DataTransferList(); + private _types: Record<string, string> = {}; + + get files() { + return this.items.items + .filter((item) => item.kind === "file") + .map((item) => item.getAsFile()!); + } + + add(data: string | File, type: string = ""): void { + this.items.add(data, type); + } + + setData(type: string, value: string) { + this._types[type] = value; + } + + getData(type: string) { + return this._types[type] || ""; + } +} + +export const testPolyfills = { + ClipboardEvent, + DataTransfer, + DataTransferItem, + // https://github.com/vitest-dev/vitest/pull/4164#issuecomment-2172729965 + URL, +}; diff --git a/packages/excalidraw/tests/helpers/ui.ts b/packages/excalidraw/tests/helpers/ui.ts new file mode 100644 index 0000000..b9b7023 --- /dev/null +++ b/packages/excalidraw/tests/helpers/ui.ts @@ -0,0 +1,647 @@ +import type { + ExcalidrawElement, + ExcalidrawLinearElement, + ExcalidrawTextElement, + ExcalidrawArrowElement, + ExcalidrawRectangleElement, + ExcalidrawEllipseElement, + ExcalidrawDiamondElement, + ExcalidrawTextContainer, + ExcalidrawTextElementWithContainer, + ExcalidrawImageElement, +} from "../../element/types"; +import type { TransformHandleType } from "../../element/transformHandles"; +import { + getTransformHandles, + getTransformHandlesFromCoords, + OMIT_SIDES_FOR_FRAME, + OMIT_SIDES_FOR_MULTIPLE_ELEMENTS, + type TransformHandle, + type TransformHandleDirection, +} from "../../element/transformHandles"; +import { KEYS } from "../../keys"; +import { act, fireEvent, GlobalTestState, screen } from "../test-utils"; +import { mutateElement } from "../../element/mutateElement"; +import { API } from "./api"; +import { + isLinearElement, + isFreeDrawElement, + isTextElement, + isFrameLikeElement, +} from "../../element/typeChecks"; +import { getCommonBounds, getElementPointsCoords } from "../../element/bounds"; +import { getTextEditor } from "../queries/dom"; +import { arrayToMap } from "../../utils"; +import { createTestHook } from "../../components/App"; +import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math"; +import { pointFrom, pointRotateRads } from "@excalidraw/math"; +import { cropElement } from "../../element/cropElement"; +import type { ToolType } from "../../types"; + +// so that window.h is available when App.tsx is not imported as well. +createTestHook(); + +const { h } = window; + +let altKey = false; +let shiftKey = false; +let ctrlKey = false; + +export type KeyboardModifiers = { + alt?: boolean; + shift?: boolean; + ctrl?: boolean; +}; +export class Keyboard { + static withModifierKeys = (modifiers: KeyboardModifiers, cb: () => void) => { + const prevAltKey = altKey; + const prevShiftKey = shiftKey; + const prevCtrlKey = ctrlKey; + + altKey = !!modifiers.alt; + shiftKey = !!modifiers.shift; + ctrlKey = !!modifiers.ctrl; + + try { + cb(); + } finally { + altKey = prevAltKey; + shiftKey = prevShiftKey; + ctrlKey = prevCtrlKey; + } + }; + + static keyDown = ( + key: string, + target: HTMLElement | Document | Window = document, + ) => { + fireEvent.keyDown(target, { + key, + ctrlKey, + shiftKey, + altKey, + }); + }; + + static keyUp = ( + key: string, + target: HTMLElement | Document | Window = document, + ) => { + fireEvent.keyUp(target, { + key, + ctrlKey, + shiftKey, + altKey, + }); + }; + + static keyPress = (key: string, target?: HTMLElement | Document | Window) => { + Keyboard.keyDown(key, target); + Keyboard.keyUp(key, target); + }; + + static codeDown = (code: string) => { + fireEvent.keyDown(document, { + code, + ctrlKey, + shiftKey, + altKey, + }); + }; + + static codeUp = (code: string) => { + fireEvent.keyUp(document, { + code, + ctrlKey, + shiftKey, + altKey, + }); + }; + + static codePress = (code: string) => { + Keyboard.codeDown(code); + Keyboard.codeUp(code); + }; + + static undo = () => { + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress("z"); + }); + }; + + static redo = () => { + Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => { + Keyboard.keyPress("z"); + }); + }; + + static exitTextEditor = (textarea: HTMLTextAreaElement) => { + fireEvent.keyDown(textarea, { key: KEYS.ESCAPE }); + }; +} + +const getElementPointForSelection = ( + element: ExcalidrawElement, +): GlobalPoint => { + const { x, y, width, height, angle } = element; + const target = pointFrom<GlobalPoint>( + x + + (isLinearElement(element) || isFreeDrawElement(element) ? 0 : width / 2), + y, + ); + let center: GlobalPoint; + + if (isLinearElement(element)) { + const bounds = getElementPointsCoords(element, element.points); + center = pointFrom( + (bounds[0] + bounds[2]) / 2, + (bounds[1] + bounds[3]) / 2, + ); + } else { + center = pointFrom(x + width / 2, y + height / 2); + } + + if (isTextElement(element)) { + return center; + } + + return pointRotateRads(target, center, angle); +}; + +export class Pointer { + public clientX = 0; + public clientY = 0; + + constructor( + private readonly pointerType: "mouse" | "touch" | "pen", + private readonly pointerId = 1, + ) {} + + reset() { + this.clientX = 0; + this.clientY = 0; + } + + getPosition() { + return [this.clientX, this.clientY]; + } + + restorePosition(x = 0, y = 0) { + this.clientX = x; + this.clientY = y; + fireEvent.pointerMove(GlobalTestState.interactiveCanvas, this.getEvent()); + } + + private getEvent() { + return { + clientX: this.clientX, + clientY: this.clientY, + pointerType: this.pointerType, + pointerId: this.pointerId, + altKey, + shiftKey, + ctrlKey, + }; + } + + // incremental (moving by deltas) + // --------------------------------------------------------------------------- + + move(dx: number, dy: number) { + if (dx !== 0 || dy !== 0) { + this.clientX += dx; + this.clientY += dy; + fireEvent.pointerMove(GlobalTestState.interactiveCanvas, this.getEvent()); + } + } + + down(dx = 0, dy = 0) { + this.move(dx, dy); + fireEvent.pointerDown(GlobalTestState.interactiveCanvas, this.getEvent()); + } + + up(dx = 0, dy = 0) { + this.move(dx, dy); + fireEvent.pointerUp(GlobalTestState.interactiveCanvas, this.getEvent()); + } + + click(dx = 0, dy = 0) { + this.down(dx, dy); + this.up(); + } + + doubleClick(dx = 0, dy = 0) { + this.move(dx, dy); + fireEvent.doubleClick(GlobalTestState.interactiveCanvas, this.getEvent()); + } + + // absolute coords + // --------------------------------------------------------------------------- + + moveTo(x: number = this.clientX, y: number = this.clientY) { + this.clientX = x; + this.clientY = y; + // fire "mousemove" to update editor cursor position + fireEvent.mouseMove(document, this.getEvent()); + fireEvent.pointerMove(GlobalTestState.interactiveCanvas, this.getEvent()); + } + + downAt(x = this.clientX, y = this.clientY) { + this.clientX = x; + this.clientY = y; + fireEvent.pointerDown(GlobalTestState.interactiveCanvas, this.getEvent()); + } + + upAt(x = this.clientX, y = this.clientY) { + this.clientX = x; + this.clientY = y; + fireEvent.pointerUp(GlobalTestState.interactiveCanvas, this.getEvent()); + } + + clickAt(x: number, y: number) { + this.downAt(x, y); + this.upAt(); + } + + rightClickAt(x: number, y: number) { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: x, + clientY: y, + }); + } + + doubleClickAt(x: number, y: number) { + this.moveTo(x, y); + fireEvent.doubleClick(GlobalTestState.interactiveCanvas, this.getEvent()); + } + + // --------------------------------------------------------------------------- + + select( + /** if multiple elements supplied, they're shift-selected */ + elements: ExcalidrawElement | ExcalidrawElement[], + ) { + API.clearSelection(); + + Keyboard.withModifierKeys({ shift: true }, () => { + elements = Array.isArray(elements) ? elements : [elements]; + elements.forEach((element) => { + this.reset(); + this.click(...getElementPointForSelection(element)); + }); + }); + + this.reset(); + } + + clickOn(element: ExcalidrawElement) { + this.reset(); + this.click(...getElementPointForSelection(element)); + this.reset(); + } + + doubleClickOn(element: ExcalidrawElement) { + this.reset(); + this.doubleClick(...getElementPointForSelection(element)); + this.reset(); + } +} + +const mouse = new Pointer("mouse"); + +const transform = ( + element: ExcalidrawElement | ExcalidrawElement[], + handle: TransformHandleType, + mouseMove: [deltaX: number, deltaY: number], + keyboardModifiers: KeyboardModifiers = {}, +) => { + const elements = Array.isArray(element) ? element : [element]; + act(() => { + h.setState({ + selectedElementIds: elements.reduce( + (acc, e) => ({ + ...acc, + [e.id]: true, + }), + {}, + ), + }); + }); + let handleCoords: TransformHandle | undefined; + if (elements.length === 1) { + handleCoords = getTransformHandles( + elements[0], + h.state.zoom, + arrayToMap(h.elements), + "mouse", + {}, + )[handle]; + } else { + const [x1, y1, x2, y2] = getCommonBounds(elements); + const isFrameSelected = elements.some(isFrameLikeElement); + const transformHandles = getTransformHandlesFromCoords( + [x1, y1, x2, y2, (x1 + x2) / 2, (y1 + y2) / 2], + 0 as Radians, + h.state.zoom, + "mouse", + isFrameSelected ? OMIT_SIDES_FOR_FRAME : OMIT_SIDES_FOR_MULTIPLE_ELEMENTS, + ); + handleCoords = transformHandles[handle]; + } + + if (!handleCoords) { + throw new Error(`There is no "${handle}" handle for this selection`); + } + + const clientX = handleCoords[0] + handleCoords[2] / 2; + const clientY = handleCoords[1] + handleCoords[3] / 2; + + Keyboard.withModifierKeys(keyboardModifiers, () => { + mouse.reset(); + mouse.down(clientX, clientY); + mouse.move(mouseMove[0], mouseMove[1]); + mouse.up(); + }); +}; + +const proxy = <T extends ExcalidrawElement>( + element: T, +): typeof element & { + /** Returns the actual, current element from the elements array, instead of + the proxy */ + get(): typeof element; +} => { + return new Proxy( + {}, + { + get(target, prop) { + const currentElement = h.elements.find( + ({ id }) => id === element.id, + ) as any; + if (prop === "get") { + if (currentElement.hasOwnProperty("get")) { + throw new Error( + "trying to get `get` test property, but ExcalidrawElement seems to define its own", + ); + } + return () => currentElement; + } + return currentElement[prop]; + }, + }, + ) as any; +}; + +/** Tools that can be used to draw shapes */ +type DrawingToolName = Exclude<ToolType, "lock" | "selection" | "eraser">; + +type Element<T extends DrawingToolName> = T extends "line" | "freedraw" + ? ExcalidrawLinearElement + : T extends "arrow" + ? ExcalidrawArrowElement + : T extends "text" + ? ExcalidrawTextElement + : T extends "rectangle" + ? ExcalidrawRectangleElement + : T extends "ellipse" + ? ExcalidrawEllipseElement + : T extends "diamond" + ? ExcalidrawDiamondElement + : ExcalidrawElement; + +export class UI { + static clickTool = (toolName: ToolType | "lock") => { + fireEvent.click(GlobalTestState.renderResult.getByToolName(toolName)); + }; + + static clickLabeledElement = (label: string) => { + const element = document.querySelector(`[aria-label='${label}']`); + if (!element) { + throw new Error(`No labeled element found: ${label}`); + } + fireEvent.click(element); + }; + + static clickOnTestId = (testId: string) => { + const element = document.querySelector(`[data-testid='${testId}']`); + // const element = GlobalTestState.renderResult.queryByTestId(testId); + if (!element) { + throw new Error(`No element with testid "${testId}" found`); + } + fireEvent.click(element); + }; + + static clickByTitle = (title: string) => { + fireEvent.click(screen.getByTitle(title)); + }; + + /** + * Creates an Excalidraw element, and returns a proxy that wraps it so that + * accessing props will return the latest ones from the object existing in + * the app's elements array. This is because across the app lifecycle we tend + * to recreate element objects and the returned reference will become stale. + * + * If you need to get the actual element, not the proxy, call `get()` method + * on the proxy object. + */ + static createElement<T extends DrawingToolName>( + type: T, + { + position = 0, + x = position, + y = position, + size = 10, + width: initialWidth = size, + height: initialHeight = initialWidth, + angle = 0, + points: initialPoints, + }: { + position?: number; + x?: number; + y?: number; + size?: number; + width?: number; + height?: number; + angle?: number; + points?: T extends "line" | "arrow" | "freedraw" ? LocalPoint[] : never; + } = {}, + ): Element<T> & { + /** Returns the actual, current element from the elements array, instead + of the proxy */ + get(): Element<T>; + } { + const width = initialWidth ?? initialHeight ?? size; + const height = initialHeight ?? size; + const points: LocalPoint[] = initialPoints ?? [ + pointFrom(0, 0), + pointFrom(width, height), + ]; + + UI.clickTool(type); + + if (type === "text") { + mouse.reset(); + mouse.click(x, y); + } else if ((type === "line" || type === "arrow") && points.length > 2) { + points.forEach((point) => { + mouse.reset(); + mouse.click(x + point[0], y + point[1]); + }); + Keyboard.keyPress(KEYS.ESCAPE); + } else if (type === "freedraw" && points.length > 2) { + const firstPoint = points[0]; + mouse.reset(); + mouse.down(x + firstPoint[0], y + firstPoint[1]); + points + .slice(1) + .forEach((point) => mouse.moveTo(x + point[0], y + point[1])); + mouse.upAt(); + Keyboard.keyPress(KEYS.ESCAPE); + } else { + mouse.reset(); + mouse.down(x, y); + mouse.reset(); + mouse.up(x + width, y + height); + } + const origElement = h.elements[h.elements.length - 1] as any; + + if (angle !== 0) { + act(() => { + mutateElement(origElement, { angle }); + }); + } + + return proxy(origElement); + } + + static async editText< + T extends ExcalidrawTextElement | ExcalidrawTextContainer, + >(element: T, text: string) { + const textEditorSelector = ".excalidraw-textEditorContainer > textarea"; + const openedEditor = + document.querySelector<HTMLTextAreaElement>(textEditorSelector); + + if (!openedEditor) { + mouse.select(element); + Keyboard.keyPress(KEYS.ENTER); + } + + const editor = await getTextEditor(textEditorSelector); + if (!editor) { + throw new Error("Can't find wysiwyg text editor in the dom"); + } + + fireEvent.input(editor, { target: { value: text } }); + act(() => { + editor.blur(); + }); + + return isTextElement(element) + ? element + : proxy( + h.elements[ + h.elements.length - 1 + ] as ExcalidrawTextElementWithContainer, + ); + } + + static updateInput = (input: HTMLInputElement, value: string | number) => { + act(() => { + input.focus(); + fireEvent.change(input, { target: { value: String(value) } }); + input.blur(); + }); + }; + + static resize( + element: ExcalidrawElement | ExcalidrawElement[], + handle: TransformHandleDirection, + mouseMove: [deltaX: number, deltaY: number], + keyboardModifiers: KeyboardModifiers = {}, + ) { + return transform(element, handle, mouseMove, keyboardModifiers); + } + + static crop( + element: ExcalidrawImageElement, + handle: TransformHandleDirection, + naturalWidth: number, + naturalHeight: number, + mouseMove: [deltaX: number, deltaY: number], + keepAspectRatio = false, + ) { + const handleCoords = getTransformHandles( + element, + h.state.zoom, + arrayToMap(h.elements), + "mouse", + {}, + )[handle]!; + + const clientX = handleCoords[0] + handleCoords[2] / 2; + const clientY = handleCoords[1] + handleCoords[3] / 2; + + const mutations = cropElement( + element, + handle, + naturalWidth, + naturalHeight, + clientX + mouseMove[0], + clientY + mouseMove[1], + keepAspectRatio ? element.width / element.height : undefined, + ); + + API.updateElement(element, mutations); + } + + static rotate( + element: ExcalidrawElement | ExcalidrawElement[], + mouseMove: [deltaX: number, deltaY: number], + keyboardModifiers: KeyboardModifiers = {}, + ) { + return transform(element, "rotation", mouseMove, keyboardModifiers); + } + + static group(elements: ExcalidrawElement[]) { + mouse.select(elements); + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.G); + }); + } + + static ungroup(elements: ExcalidrawElement[]) { + mouse.select(elements); + Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => { + Keyboard.keyPress(KEYS.G); + }); + } + + static queryContextMenu = () => { + return GlobalTestState.renderResult.container.querySelector( + ".context-menu", + ) as HTMLElement | null; + }; + + static queryStats = () => { + return GlobalTestState.renderResult.container.querySelector( + ".exc-stats", + ) as HTMLElement | null; + }; + + static queryStatsProperty = (label: string) => { + const elementStats = UI.queryStats()?.querySelector("#elementStats"); + + expect(elementStats).not.toBeNull(); + + if (elementStats) { + return ( + elementStats?.querySelector( + `.exc-stats__row .drag-input-container[data-testid="${label}"]`, + ) || null + ); + } + + return null; + }; +} diff --git a/packages/excalidraw/tests/history.test.tsx b/packages/excalidraw/tests/history.test.tsx new file mode 100644 index 0000000..ffddec4 --- /dev/null +++ b/packages/excalidraw/tests/history.test.tsx @@ -0,0 +1,4938 @@ +import React from "react"; +import "../global.d.ts"; +import * as StaticScene from "../renderer/staticScene"; +import { + GlobalTestState, + act, + assertSelectedElements, + render, + togglePopover, + getCloneByOrigId, +} from "./test-utils"; +import "../../utils/test-utils"; +import { Excalidraw } from "../index"; +import { Keyboard, Pointer, UI } from "./helpers/ui"; +import { API } from "./helpers/api"; +import { getDefaultAppState } from "../appState"; +import { fireEvent, queryByTestId, waitFor } from "@testing-library/react"; +import { createUndoAction, createRedoAction } from "../actions/actionHistory"; +import { actionToggleViewMode } from "../actions/actionToggleViewMode"; +import { EXPORT_DATA_TYPES, MIME_TYPES, ORIG_ID } from "../constants"; +import type { AppState } from "../types"; +import { arrayToMap } from "../utils"; +import { + COLOR_PALETTE, + DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX, + DEFAULT_ELEMENT_STROKE_COLOR_INDEX, +} from "../colors"; +import { KEYS } from "../keys"; +import { newElementWith } from "../element/mutateElement"; +import type { + ExcalidrawElbowArrowElement, + ExcalidrawFrameElement, + ExcalidrawGenericElement, + ExcalidrawLinearElement, + ExcalidrawTextElement, + FixedPointBinding, + FractionalIndex, + SceneElementsMap, +} from "../element/types"; +import { + actionSendBackward, + actionBringForward, + actionSendToBack, +} from "../actions"; +import { vi } from "vitest"; +import { queryByText } from "@testing-library/react"; +import { HistoryEntry } from "../history"; +import { AppStateChange, ElementsChange } from "../change"; +import { Snapshot, CaptureUpdateAction } from "../store"; +import type { LocalPoint, Radians } from "@excalidraw/math"; +import { pointFrom } from "@excalidraw/math"; + +const { h } = window; + +const mouse = new Pointer("mouse"); + +const checkpoint = (name: string) => { + expect(renderStaticScene.mock.calls.length).toMatchSnapshot( + `[${name}] number of renders`, + ); + // `scrolledOutside` does not appear to be stable between test runs + // `selectedLinearElemnt` includes `startBindingElement` containing seed and versionNonce + const { + name: _, + scrolledOutside, + selectedLinearElement, + ...strippedAppState + } = h.state; + expect(strippedAppState).toMatchSnapshot(`[${name}] appState`); + expect(h.history).toMatchSnapshot(`[${name}] history`); + expect(h.elements.length).toMatchSnapshot(`[${name}] number of elements`); + h.elements + .map(({ seed, versionNonce, ...strippedElement }) => strippedElement) + .forEach((element, i) => + expect(element).toMatchSnapshot(`[${name}] element ${i}`), + ); +}; + +const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene"); + +const transparent = COLOR_PALETTE.transparent; +const black = COLOR_PALETTE.black; +const red = COLOR_PALETTE.red[DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX]; +const blue = COLOR_PALETTE.blue[DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX]; +const yellow = COLOR_PALETTE.yellow[DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX]; +const violet = COLOR_PALETTE.violet[DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX]; + +describe("history", () => { + beforeEach(() => { + renderStaticScene.mockClear(); + }); + + afterEach(() => { + checkpoint("end of test"); + }); + + describe("singleplayer undo/redo", () => { + it("should not collapse when applying corrupted history entry", async () => { + await render(<Excalidraw handleKeyboardGlobally={true} />); + const rect = API.createElement({ type: "rectangle" }); + + API.setElements([rect]); + + const corrupedEntry = HistoryEntry.create( + AppStateChange.empty(), + ElementsChange.empty(), + ); + + vi.spyOn(corrupedEntry, "applyTo").mockImplementation(() => { + throw new Error("Oh no, I am corrupted!"); + }); + + (h.history as any).undoStack.push(corrupedEntry); + + const appState = getDefaultAppState() as AppState; + + try { + // due to this we unfortunately we couldn't do simple .toThrow() + act( + () => + h.history.undo( + arrayToMap(h.elements) as SceneElementsMap, + appState, + Snapshot.empty(), + ) as any, + ); + } catch (e) { + expect(e).toBeInstanceOf(Error); + } + // we popped the entry, even though it is corrupted, so the user could perform subsequent undo/redo and would not be stuck on this entry forever + expect(API.getUndoStack().length).toBe(0); + // we pushed the entr, as we don't want just lose it and throw it away - it might be perfectly valid on subsequent redo + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect.id, isDeleted: false }), // no changes detected + ]); + + try { + // due to this we unfortunately we couldn't do simple .toThrow() + act( + () => + h.history.redo( + arrayToMap(h.elements) as SceneElementsMap, + appState, + Snapshot.empty(), + ) as any, + ); + } catch (e) { + expect(e).toBeInstanceOf(Error); + } + expect(API.getUndoStack().length).toBe(1); // vice versa for redo + expect(API.getRedoStack().length).toBe(0); // vice versa for undo + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect.id, isDeleted: false }), + ]); + }); + + it("should not end up with history entry when there are no appstate changes", async () => { + await render(<Excalidraw handleKeyboardGlobally={true} />); + const rect1 = API.createElement({ type: "rectangle", groupIds: ["A"] }); + const rect2 = API.createElement({ type: "rectangle", groupIds: ["A"] }); + + API.setElements([rect1, rect2]); + mouse.select(rect1); + assertSelectedElements([rect1, rect2]); + expect(h.state.selectedGroupIds).toEqual({ A: true }); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + + mouse.select(rect2); + assertSelectedElements([rect1, rect2]); + expect(h.state.selectedGroupIds).toEqual({ A: true }); + expect(API.getUndoStack().length).toBe(1); // no new entry was created + expect(API.getRedoStack().length).toBe(0); + }); + + it("should not end up with history entry when there are no elements changes", async () => { + await render(<Excalidraw handleKeyboardGlobally={true} />); + + const rect1 = API.createElement({ type: "rectangle" }); + const rect2 = API.createElement({ type: "rectangle" }); + + API.updateScene({ + elements: [rect1, rect2], + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); + + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id, isDeleted: false }), + expect.objectContaining({ id: rect2.id, isDeleted: false }), + ]); + + API.updateScene({ + elements: [rect1, rect2], + captureUpdate: CaptureUpdateAction.IMMEDIATELY, // even though the flag is on, same elements are passed, nothing to commit + }); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id, isDeleted: false }), + expect.objectContaining({ id: rect2.id, isDeleted: false }), + ]); + }); + + it("should not clear the redo stack on standalone appstate change", async () => { + await render(<Excalidraw handleKeyboardGlobally={true} />); + + const rect1 = UI.createElement("rectangle", { x: 10 }); + const rect2 = UI.createElement("rectangle", { x: 20 }); + + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(0); + assertSelectedElements(rect2); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id, isDeleted: false }), + expect.objectContaining({ id: rect2.id, isDeleted: false }), + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(1); + assertSelectedElements(rect1); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id, isDeleted: false }), + expect.objectContaining({ id: rect2.id, isDeleted: true }), + ]); + + mouse.clickAt(-10, -10); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(1); // we still have a possibility to redo! + expect(API.getSelectedElements().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id, isDeleted: false }), + expect.objectContaining({ id: rect2.id, isDeleted: true }), + ]); + + mouse.downAt(0, 0); + mouse.moveTo(50, 50); + mouse.upAt(50, 50); + expect(API.getUndoStack().length).toBe(3); + expect(API.getRedoStack().length).toBe(1); // even after re-select! + assertSelectedElements(rect1); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id, isDeleted: false }), + expect.objectContaining({ id: rect2.id, isDeleted: true }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(4); + expect(API.getRedoStack().length).toBe(0); + assertSelectedElements(rect2); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id, isDeleted: false }), + expect.objectContaining({ id: rect2.id, isDeleted: false }), + ]); + }); + + it("should not override appstate changes when redo stack is not cleared", async () => { + await render(<Excalidraw handleKeyboardGlobally={true} />); + + const rect = UI.createElement("rectangle", { x: 10 }); + togglePopover("Background"); + UI.clickOnTestId("color-red"); + UI.clickOnTestId("color-blue"); + + expect(API.getUndoStack().length).toBe(3); + expect(API.getRedoStack().length).toBe(0); + assertSelectedElements(rect); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect.id, backgroundColor: blue }), + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(1); + assertSelectedElements(rect); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect.id, backgroundColor: red }), + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(2); + assertSelectedElements(rect); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect.id, backgroundColor: transparent }), + ]); + + mouse.clickAt(-10, -10); + expect(API.getUndoStack().length).toBe(2); // pushed appstate change, + expect(API.getRedoStack().length).toBe(2); // redo stack is not cleared + expect(API.getSelectedElements().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect.id, backgroundColor: transparent }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(3); + expect(API.getRedoStack().length).toBe(1); + expect(API.getSelectedElements().length).toBe(0); // previously the item was selected, not it is not + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect.id, backgroundColor: red }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(4); + expect(API.getRedoStack().length).toBe(0); + expect(API.getSelectedElements().length).toBe(0); // previously the item was selected, not it is not + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect.id, backgroundColor: blue }), + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(3); + expect(API.getRedoStack().length).toBe(1); + expect(API.getSelectedElements().length).toBe(0); // previously the item was selected, not it is not + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect.id, backgroundColor: red }), + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(2); + expect(API.getSelectedElements().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect.id, backgroundColor: transparent }), + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(3); + assertSelectedElements(rect); // get's reselected with out pushed entry! + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect.id, backgroundColor: transparent }), + ]); + }); + + it("should clear the redo stack on elements change", async () => { + await render(<Excalidraw handleKeyboardGlobally={true} />); + + const rect1 = UI.createElement("rectangle", { x: 10 }); + + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + assertSelectedElements(rect1); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id, isDeleted: false }), + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + expect(API.getSelectedElements()).toEqual([]); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id, isDeleted: true }), + ]); + + const rect2 = UI.createElement("rectangle", { x: 20 }); + + assertSelectedElements(rect2); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); // redo stack got cleared + expect(API.getSnapshot()).toEqual([ + expect.objectContaining({ id: rect1.id, isDeleted: true }), + expect.objectContaining({ id: rect2.id, isDeleted: false }), + ]); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id, isDeleted: true }), + expect.objectContaining({ id: rect2.id, isDeleted: false }), + ]); + }); + + it("should iterate through the history when selection changes do not produce visible change", async () => { + await render(<Excalidraw handleKeyboardGlobally={true} />); + + const rect = UI.createElement("rectangle", { x: 10 }); + + mouse.clickAt(-10, -10); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(0); + expect(API.getSelectedElements().length).toBe(0); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(1); + assertSelectedElements(rect); + + mouse.clickAt(-10, -10); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(1); + expect(API.getSelectedElements().length).toBe(0); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(2); // now we have two same redos + assertSelectedElements(rect); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(1); // didn't iterate through completely, as first redo already results in a visible change + expect(API.getSelectedElements().length).toBe(0); + + Keyboard.redo(); // acceptable empty redo + expect(API.getUndoStack().length).toBe(3); + expect(API.getRedoStack().length).toBe(0); + expect(API.getSelectedElements().length).toBe(0); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(1); + assertSelectedElements(rect); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); // now we iterated through the same undos! + expect(API.getRedoStack().length).toBe(3); + expect(API.getSelectedElements().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect.id, isDeleted: true }), + ]); + }); + + it("should end up with no history entry after initializing scene", async () => { + await render( + <Excalidraw + initialData={{ + elements: [API.createElement({ type: "rectangle", id: "A" })], + appState: { + zenModeEnabled: true, + }, + }} + />, + ); + + await waitFor(() => { + expect(h.state.zenModeEnabled).toBe(true); + expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]); + expect(h.history.isUndoStackEmpty).toBeTruthy(); + }); + + const undoAction = createUndoAction(h.history, h.store); + const redoAction = createRedoAction(h.history, h.store); + // noop + API.executeAction(undoAction); + expect(h.elements).toEqual([ + expect.objectContaining({ id: "A", isDeleted: false }), + ]); + const rectangle = UI.createElement("rectangle"); + expect(h.elements).toEqual([ + expect.objectContaining({ id: "A" }), + expect.objectContaining({ id: rectangle.id }), + ]); + API.executeAction(undoAction); + expect(h.elements).toEqual([ + expect.objectContaining({ id: "A", isDeleted: false }), + expect.objectContaining({ id: rectangle.id, isDeleted: true }), + ]); + + // noop + API.executeAction(undoAction); + expect(h.elements).toEqual([ + expect.objectContaining({ id: "A", isDeleted: false }), + expect.objectContaining({ id: rectangle.id, isDeleted: true }), + ]); + expect(API.getUndoStack().length).toBe(0); + + API.executeAction(redoAction); + expect(h.elements).toEqual([ + expect.objectContaining({ id: "A", isDeleted: false }), + expect.objectContaining({ id: rectangle.id, isDeleted: false }), + ]); + expect(API.getUndoStack().length).toBe(1); + }); + + it("should create new history entry on scene import via drag&drop", async () => { + await render( + <Excalidraw + initialData={{ + elements: [API.createElement({ type: "rectangle", id: "A" })], + appState: { + viewBackgroundColor: "#FFF", + }, + }} + />, + ); + + await waitFor(() => expect(h.state.viewBackgroundColor).toBe("#FFF")); + await waitFor(() => + expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]), + ); + + await API.drop( + new Blob( + [ + JSON.stringify({ + type: EXPORT_DATA_TYPES.excalidraw, + appState: { + ...getDefaultAppState(), + viewBackgroundColor: "#000", + }, + elements: [API.createElement({ type: "rectangle", id: "B" })], + }), + ], + { type: MIME_TYPES.json }, + ), + ); + + await waitFor(() => expect(API.getUndoStack().length).toBe(1)); + expect(h.state.viewBackgroundColor).toBe("#000"); + expect(API.getSnapshot()).toEqual([ + expect.objectContaining({ id: "A", isDeleted: true }), + expect.objectContaining({ id: "B", isDeleted: false }), + ]); + expect(h.elements).toEqual([ + expect.objectContaining({ id: "B", isDeleted: false }), + ]); + + const undoAction = createUndoAction(h.history, h.store); + const redoAction = createRedoAction(h.history, h.store); + API.executeAction(undoAction); + + expect(API.getSnapshot()).toEqual([ + expect.objectContaining({ id: "A", isDeleted: false }), + expect.objectContaining({ id: "B", isDeleted: true }), + ]); + expect(h.elements).toEqual([ + expect.objectContaining({ id: "A", isDeleted: false }), + expect.objectContaining({ id: "B", isDeleted: true }), + ]); + expect(h.state.viewBackgroundColor).toBe("#FFF"); + + API.executeAction(redoAction); + expect(h.state.viewBackgroundColor).toBe("#000"); + expect(API.getSnapshot()).toEqual([ + expect.objectContaining({ id: "A", isDeleted: true }), + expect.objectContaining({ id: "B", isDeleted: false }), + ]); + expect(h.elements).toEqual([ + expect.objectContaining({ id: "A", isDeleted: true }), + expect.objectContaining({ id: "B", isDeleted: false }), + ]); + }); + + it("should support appstate name or viewBackgroundColor change", async () => { + await render( + <Excalidraw + handleKeyboardGlobally={true} + initialData={{ + appState: { + name: "Old name", + viewBackgroundColor: "#FFF", + }, + }} + />, + ); + + expect(h.state.isLoading).toBe(false); + expect(h.state.name).toBe("Old name"); + + API.updateScene({ + appState: { + name: "New name", + }, + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); + + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.state.name).toBe("New name"); + + API.updateScene({ + appState: { + viewBackgroundColor: "#000", + }, + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(0); + expect(h.state.name).toBe("New name"); + expect(h.state.viewBackgroundColor).toBe("#000"); + + // just to double check that same change is not recorded + API.updateScene({ + appState: { + name: "New name", + viewBackgroundColor: "#000", + }, + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(0); + expect(h.state.name).toBe("New name"); + expect(h.state.viewBackgroundColor).toBe("#000"); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(1); + expect(h.state.name).toBe("New name"); + expect(h.state.viewBackgroundColor).toBe("#FFF"); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(2); + expect(h.state.name).toBe("Old name"); + expect(h.state.viewBackgroundColor).toBe("#FFF"); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(1); + expect(h.state.name).toBe("New name"); + expect(h.state.viewBackgroundColor).toBe("#FFF"); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(0); + expect(h.state.name).toBe("New name"); + expect(h.state.viewBackgroundColor).toBe("#000"); + }); + + it("should support element creation, deletion and appstate element selection change", async () => { + await render(<Excalidraw handleKeyboardGlobally={true} />); + + const rect1 = UI.createElement("rectangle", { x: 10 }); + const rect2 = UI.createElement("rectangle", { x: 20, y: 20 }); + const rect3 = UI.createElement("rectangle", { x: 40, y: 40 }); + + mouse.select([rect2, rect3]); + Keyboard.keyDown(KEYS.DELETE); + + expect(API.getUndoStack().length).toBe(6); + + Keyboard.undo(); + assertSelectedElements(rect2, rect3); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id }), + expect.objectContaining({ id: rect2.id, isDeleted: false }), + expect.objectContaining({ id: rect3.id, isDeleted: false }), + ]); + + Keyboard.undo(); + assertSelectedElements(rect2); + + Keyboard.undo(); + assertSelectedElements(rect3); + + Keyboard.undo(); + assertSelectedElements(rect2); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id }), + expect.objectContaining({ id: rect2.id }), + expect.objectContaining({ id: rect3.id, isDeleted: true }), + ]); + + Keyboard.undo(); + assertSelectedElements(rect1); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id }), + expect.objectContaining({ id: rect2.id, isDeleted: true }), + expect.objectContaining({ id: rect3.id, isDeleted: true }), + ]); + + Keyboard.undo(); + assertSelectedElements(); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id, isDeleted: true }), + expect.objectContaining({ id: rect2.id, isDeleted: true }), + expect.objectContaining({ id: rect3.id, isDeleted: true }), + ]); + + // no-op + Keyboard.undo(); + assertSelectedElements(); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id, isDeleted: true }), + expect.objectContaining({ id: rect2.id, isDeleted: true }), + expect.objectContaining({ id: rect3.id, isDeleted: true }), + ]); + + Keyboard.redo(); + assertSelectedElements(rect1); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id }), + expect.objectContaining({ id: rect2.id, isDeleted: true }), + expect.objectContaining({ id: rect3.id, isDeleted: true }), + ]); + + Keyboard.redo(); + assertSelectedElements(rect2); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id }), + expect.objectContaining({ id: rect2.id }), + expect.objectContaining({ id: rect3.id, isDeleted: true }), + ]); + + Keyboard.redo(); + assertSelectedElements(rect3); + + Keyboard.redo(); + assertSelectedElements(rect2); + + Keyboard.redo(); + assertSelectedElements(rect2, rect3); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id }), + expect.objectContaining({ id: rect2.id, isDeleted: false }), + expect.objectContaining({ id: rect3.id, isDeleted: false }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(6); + expect(API.getRedoStack().length).toBe(0); + assertSelectedElements(); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id, isDeleted: false }), + expect.objectContaining({ id: rect2.id, isDeleted: true }), + expect.objectContaining({ id: rect3.id, isDeleted: true }), + ]); + + // no-op + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(6); + expect(API.getRedoStack().length).toBe(0); + assertSelectedElements(); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id, isDeleted: false }), + expect.objectContaining({ id: rect2.id, isDeleted: true }), + expect.objectContaining({ id: rect3.id, isDeleted: true }), + ]); + }); + + it("should support linear element creation and points manipulation through the editor", async () => { + await render(<Excalidraw handleKeyboardGlobally={true} />); + + // create three point arrow + UI.clickTool("arrow"); + mouse.click(0, 0); + mouse.click(10, 10); + mouse.click(10, -10); + + // actionFinalize + Keyboard.keyPress(KEYS.ENTER); + + // open editor + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.ENTER); + }); + + // move point + mouse.downAt(20, 0); + mouse.moveTo(20, 20); + mouse.up(); + + // leave editor + Keyboard.keyPress(KEYS.ESCAPE); + + expect(API.getUndoStack().length).toBe(6); + expect(API.getRedoStack().length).toBe(0); + expect(assertSelectedElements(h.elements[0])); + expect(h.state.editingLinearElement).toBeNull(); + expect(h.state.selectedLinearElement).not.toBeNull(); + expect(h.elements).toEqual([ + expect.objectContaining({ + isDeleted: false, + points: [ + [0, 0], + [10, 10], + [20, 20], + ], + }), + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(5); + expect(API.getRedoStack().length).toBe(1); + expect(assertSelectedElements(h.elements[0])); + expect(h.state.editingLinearElement?.elementId).toBe(h.elements[0].id); + expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id); + expect(h.elements).toEqual([ + expect.objectContaining({ + isDeleted: false, + points: [ + [0, 0], + [10, 10], + [20, 20], + ], + }), + ]); + + // making sure clicking on points in the editor does not generate new history entries! + mouse.clickAt(0, 0); + mouse.clickAt(10, 10); + mouse.clickAt(20, 20); + expect(API.getUndoStack().length).toBe(5); + expect(API.getRedoStack().length).toBe(1); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(4); + expect(API.getRedoStack().length).toBe(2); + expect(assertSelectedElements(h.elements[0])); + expect(h.state.editingLinearElement?.elementId).toBe(h.elements[0].id); + expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id); + expect(h.elements).toEqual([ + expect.objectContaining({ + isDeleted: false, + points: [ + [0, 0], + [10, 10], + [20, 0], + ], + }), + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(3); + expect(API.getRedoStack().length).toBe(3); + expect(assertSelectedElements(h.elements[0])); + expect(h.state.editingLinearElement).toBeNull(); // undo `open editor` + expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id); + expect(h.elements).toEqual([ + expect.objectContaining({ + isDeleted: false, + points: [ + [0, 0], + [10, 10], + [20, 0], + ], + }), + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(4); + expect(assertSelectedElements(h.elements[0])); + expect(h.state.editingLinearElement).toBeNull(); + expect(h.state.selectedLinearElement).toBeNull(); // undo `actionFinalize` + expect(h.elements).toEqual([ + expect.objectContaining({ + isDeleted: false, + points: [ + [0, 0], + [10, 10], + [20, 0], + ], + }), + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(5); + expect(assertSelectedElements(h.elements[0])); + expect(h.state.editingLinearElement).toBeNull(); + expect(h.state.selectedLinearElement).toBeNull(); + expect(h.elements).toEqual([ + expect.objectContaining({ + isDeleted: false, + points: [ + [0, 0], + [10, 10], + ], + }), + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(6); + expect(API.getSelectedElements().length).toBe(0); + expect(h.state.editingLinearElement).toBeNull(); + expect(h.state.selectedLinearElement).toBeNull(); + expect(h.elements).toEqual([ + expect.objectContaining({ + isDeleted: true, + points: [ + [0, 0], + [10, 10], + ], + }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(5); + expect(assertSelectedElements(h.elements[0])); + expect(h.state.editingLinearElement).toBeNull(); + expect(h.state.selectedLinearElement).toBeNull(); + expect(h.elements).toEqual([ + expect.objectContaining({ + isDeleted: false, + points: [ + [0, 0], + [10, 10], + ], + }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(4); + expect(assertSelectedElements(h.elements[0])); + expect(h.state.editingLinearElement).toBeNull(); + expect(h.state.selectedLinearElement).toBeNull(); // undo `actionFinalize` + expect(h.elements).toEqual([ + expect.objectContaining({ + isDeleted: false, + points: [ + [0, 0], + [10, 10], + [20, 0], + ], + }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(3); + expect(API.getRedoStack().length).toBe(3); + expect(assertSelectedElements(h.elements[0])); + expect(h.state.editingLinearElement).toBeNull(); // undo `open editor` + expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id); + expect(h.elements).toEqual([ + expect.objectContaining({ + isDeleted: false, + points: [ + [0, 0], + [10, 10], + [20, 0], + ], + }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(4); + expect(API.getRedoStack().length).toBe(2); + expect(assertSelectedElements(h.elements[0])); + expect(h.state.editingLinearElement?.elementId).toBe(h.elements[0].id); + expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id); + expect(h.elements).toEqual([ + expect.objectContaining({ + isDeleted: false, + points: [ + [0, 0], + [10, 10], + [20, 0], + ], + }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(5); + expect(API.getRedoStack().length).toBe(1); + expect(assertSelectedElements(h.elements[0])); + expect(h.state.editingLinearElement?.elementId).toBe(h.elements[0].id); + expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id); + expect(h.elements).toEqual([ + expect.objectContaining({ + isDeleted: false, + points: [ + [0, 0], + [10, 10], + [20, 20], + ], + }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(6); + expect(API.getRedoStack().length).toBe(0); + expect(assertSelectedElements(h.elements[0])); + expect(h.state.editingLinearElement).toBeNull(); + expect(h.state.selectedLinearElement).not.toBeNull(); + expect(h.elements).toEqual([ + expect.objectContaining({ + isDeleted: false, + points: [ + [0, 0], + [10, 10], + [20, 20], + ], + }), + ]); + }); + + it("should create entry when selecting freedraw", async () => { + await render(<Excalidraw handleKeyboardGlobally={true} />); + + UI.clickTool("rectangle"); + mouse.down(-10, -10); + mouse.up(10, 10); + + UI.clickTool("freedraw"); + mouse.down(40, -20); + mouse.up(50, 10); + + const rectangle = h.elements[0]; + const freedraw1 = h.elements[1]; + + expect(API.getUndoStack().length).toBe(3); + expect(API.getRedoStack().length).toBe(0); + expect(API.getSelectedElements().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rectangle.id }), + expect.objectContaining({ id: freedraw1.id, strokeColor: black }), + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(1); + expect(API.getSelectedElements().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rectangle.id }), + expect.objectContaining({ + id: freedraw1.id, + strokeColor: black, + isDeleted: true, + }), + ]); + + togglePopover("Stroke"); + UI.clickOnTestId("color-red"); + mouse.down(40, -20); + mouse.up(50, 10); + + const freedraw2 = h.elements[2]; + + expect(API.getUndoStack().length).toBe(3); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rectangle.id }), + expect.objectContaining({ + id: freedraw1.id, + strokeColor: black, + isDeleted: true, + }), + expect.objectContaining({ + id: freedraw2.id, + strokeColor: COLOR_PALETTE.red[DEFAULT_ELEMENT_STROKE_COLOR_INDEX], + }), + ]); + + // ensure we don't end up with duplicated entries + UI.clickTool("freedraw"); + expect(API.getUndoStack().length).toBe(3); + expect(API.getRedoStack().length).toBe(0); + }); + + it("should support duplication of groups, appstate group selection and editing group", async () => { + await render(<Excalidraw handleKeyboardGlobally={true} />); + const rect1 = API.createElement({ + type: "rectangle", + groupIds: ["A"], + x: 0, + }); + const rect2 = API.createElement({ + type: "rectangle", + groupIds: ["A"], + x: 100, + }); + + API.setElements([rect1, rect2]); + mouse.select(rect1); + assertSelectedElements([rect1, rect2]); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.state.editingGroupId).toBeNull(); + expect(h.state.selectedGroupIds).toEqual({ A: true }); + + // inside the editing group + mouse.doubleClickOn(rect2); + assertSelectedElements([rect2]); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(0); + expect(h.state.editingGroupId).toBe("A"); + expect(h.state.selectedGroupIds).not.toEqual({ A: true }); + + mouse.clickOn(rect1); + assertSelectedElements([rect1]); + expect(API.getUndoStack().length).toBe(3); + expect(API.getRedoStack().length).toBe(0); + expect(h.state.editingGroupId).toBe("A"); + expect(h.state.selectedGroupIds).not.toEqual({ A: true }); + + Keyboard.undo(); + assertSelectedElements([rect2]); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(1); + expect(h.state.editingGroupId).toBe("A"); + expect(h.state.selectedGroupIds).not.toEqual({ A: true }); + + Keyboard.undo(); + assertSelectedElements([rect1, rect2]); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(2); + expect(h.state.editingGroupId).toBeNull(); + expect(h.state.selectedGroupIds).toEqual({ A: true }); + + Keyboard.redo(); + assertSelectedElements([rect2]); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(1); + expect(h.state.editingGroupId).toBe("A"); + expect(h.state.selectedGroupIds).not.toEqual({ A: true }); + + Keyboard.redo(); + assertSelectedElements([rect1]); + expect(API.getUndoStack().length).toBe(3); + expect(API.getRedoStack().length).toBe(0); + expect(h.state.editingGroupId).toBe("A"); + expect(h.state.selectedGroupIds).not.toEqual({ A: true }); + + Keyboard.undo(); + assertSelectedElements([rect2]); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(1); + expect(h.state.editingGroupId).toBe("A"); + expect(h.state.selectedGroupIds).not.toEqual({ A: true }); + + Keyboard.undo(); + assertSelectedElements([rect1, rect2]); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(2); + expect(h.state.editingGroupId).toBeNull(); + expect(h.state.selectedGroupIds).toEqual({ A: true }); + + // outside the editing group, testing duplication + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress("d"); + }); + assertSelectedElements([h.elements[2], h.elements[3]]); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements.length).toBe(4); + expect(h.state.editingGroupId).toBeNull(); + expect(h.state.selectedGroupIds).not.toEqual( + expect.objectContaining({ A: true }), + ); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements.length).toBe(4); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id, isDeleted: false }), + expect.objectContaining({ id: rect2.id, isDeleted: false }), + expect.objectContaining({ [ORIG_ID]: rect1.id, isDeleted: true }), + expect.objectContaining({ [ORIG_ID]: rect2.id, isDeleted: true }), + ]); + expect(h.state.editingGroupId).toBeNull(); + expect(h.state.selectedGroupIds).toEqual({ A: true }); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements.length).toBe(4); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id, isDeleted: false }), + expect.objectContaining({ id: rect2.id, isDeleted: false }), + expect.objectContaining({ [ORIG_ID]: rect1.id, isDeleted: false }), + expect.objectContaining({ [ORIG_ID]: rect2.id, isDeleted: false }), + ]); + expect(h.state.editingGroupId).toBeNull(); + expect(h.state.selectedGroupIds).not.toEqual( + expect.objectContaining({ A: true }), + ); + + // undo again, and duplicate once more + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress("z"); + Keyboard.keyPress("d"); + }); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements.length).toBe(6); + expect(h.elements).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: rect1.id, isDeleted: false }), + expect.objectContaining({ id: rect2.id, isDeleted: false }), + expect.objectContaining({ [ORIG_ID]: rect1.id, isDeleted: true }), + expect.objectContaining({ [ORIG_ID]: rect2.id, isDeleted: true }), + expect.objectContaining({ + [ORIG_ID]: getCloneByOrigId(rect1.id)?.id, + isDeleted: false, + }), + expect.objectContaining({ + [ORIG_ID]: getCloneByOrigId(rect2.id)?.id, + isDeleted: false, + }), + ]), + ); + expect(h.state.editingGroupId).toBeNull(); + expect(h.state.selectedGroupIds).not.toEqual( + expect.objectContaining({ A: true }), + ); + }); + + it("should support changes in elements' order", async () => { + await render(<Excalidraw handleKeyboardGlobally={true} />); + + const rect1 = UI.createElement("rectangle", { x: 10 }); + const rect2 = UI.createElement("rectangle", { x: 20, y: 20 }); + const rect3 = UI.createElement("rectangle", { x: 40, y: 40 }); + + API.executeAction(actionSendBackward); + + expect(API.getUndoStack().length).toBe(4); + expect(API.getRedoStack().length).toBe(0); + assertSelectedElements(rect3); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(3); + expect(API.getRedoStack().length).toBe(1); + assertSelectedElements(rect3); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id }), + expect.objectContaining({ id: rect2.id }), + expect.objectContaining({ id: rect3.id }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(4); + expect(API.getRedoStack().length).toBe(0); + assertSelectedElements(rect3); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id }), + expect.objectContaining({ id: rect3.id }), + expect.objectContaining({ id: rect2.id }), + ]); + + mouse.select([rect1, rect3]); + expect(API.getUndoStack().length).toBe(6); + expect(API.getRedoStack().length).toBe(0); + assertSelectedElements([rect1, rect3]); + + API.executeAction(actionBringForward); + + expect(API.getUndoStack().length).toBe(7); + expect(API.getRedoStack().length).toBe(0); + assertSelectedElements([rect1, rect3]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(6); + expect(API.getRedoStack().length).toBe(1); + assertSelectedElements([rect1, rect3]); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id }), + expect.objectContaining({ id: rect3.id }), + expect.objectContaining({ id: rect2.id }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(7); + expect(API.getRedoStack().length).toBe(0); + assertSelectedElements([rect1, rect3]); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect2.id }), + expect.objectContaining({ id: rect1.id }), + expect.objectContaining({ id: rect3.id }), + ]); + }); + + describe("should support bidirectional bindings", async () => { + let rect1: ExcalidrawGenericElement; + let rect2: ExcalidrawGenericElement; + let text: ExcalidrawTextElement; + let arrow: ExcalidrawLinearElement; + + const rect1Props = { + type: "rectangle", + height: 100, + width: 100, + x: -100, + y: -50, + } as const; + + const rect2Props = { + type: "rectangle", + height: 100, + width: 100, + x: 100, + y: -50, + } as const; + + const textProps = { + type: "text", + x: -200, + text: "ola", + } as const; + + beforeEach(async () => { + await render(<Excalidraw handleKeyboardGlobally={true} />); + + rect1 = API.createElement({ ...rect1Props }); + text = API.createElement({ ...textProps }); + rect2 = API.createElement({ ...rect2Props }); + + API.updateScene({ + elements: [rect1, text, rect2], + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); + + // bind text1 to rect1 + mouse.select([rect1, text]); + fireEvent.contextMenu(GlobalTestState.interactiveCanvas); + fireEvent.click( + queryByText( + document.querySelector(".context-menu") as HTMLElement, + "Bind text to the container", + )!, + ); + + expect(API.getUndoStack().length).toBe(4); + expect(text.containerId).toBe(rect1.id); + expect(rect1.boundElements).toStrictEqual([ + { id: text.id, type: "text" }, + ]); + + // bind arrow to rect1 and rect2 + UI.clickTool("arrow"); + mouse.down(0, 0); + mouse.up(100, 0); + + arrow = h.elements[3] as ExcalidrawLinearElement; + + expect(API.getUndoStack().length).toBe(5); + expect(arrow.startBinding).toEqual({ + elementId: rect1.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }); + expect(arrow.endBinding).toEqual({ + elementId: rect2.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }); + expect(rect1.boundElements).toStrictEqual([ + { id: text.id, type: "text" }, + { id: arrow.id, type: "arrow" }, + ]); + expect(rect2.boundElements).toStrictEqual([ + { id: arrow.id, type: "arrow" }, + ]); + }); + + it("should unbind arrow from non deleted bindable elements on undo and rebind on redo", async () => { + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(4); + expect(API.getRedoStack().length).toBe(1); + expect(arrow.startBinding).toEqual({ + elementId: rect1.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }); + expect(arrow.endBinding).toEqual({ + elementId: rect2.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: rect1.id, + boundElements: [{ id: text.id, type: "text" }], + }), + expect.objectContaining({ id: text.id }), + expect.objectContaining({ id: rect2.id, boundElements: [] }), + expect.objectContaining({ id: arrow.id, isDeleted: true }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(5); + expect(API.getRedoStack().length).toBe(0); + expect(arrow.startBinding).toEqual({ + elementId: rect1.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }); + expect(arrow.endBinding).toEqual({ + elementId: rect2.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: rect1.id, + boundElements: [ + { id: text.id, type: "text" }, + { id: arrow.id, type: "arrow" }, + ], + }), + expect.objectContaining({ id: text.id }), + expect.objectContaining({ + id: rect2.id, + boundElements: [{ id: arrow.id, type: "arrow" }], + }), + expect.objectContaining({ id: arrow.id, isDeleted: false }), + ]); + }); + + it("should unbind arrow from non deleted bindable elements on deletion and rebind on undo", async () => { + Keyboard.keyDown(KEYS.DELETE); + expect(API.getUndoStack().length).toBe(6); + expect(API.getRedoStack().length).toBe(0); + expect(arrow.startBinding).toEqual({ + elementId: rect1.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }); + expect(arrow.endBinding).toEqual({ + elementId: rect2.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: rect1.id, + boundElements: [{ id: text.id, type: "text" }], + }), + expect.objectContaining({ id: text.id }), + expect.objectContaining({ id: rect2.id, boundElements: [] }), + expect.objectContaining({ id: arrow.id, isDeleted: true }), + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(5); + expect(API.getRedoStack().length).toBe(1); + expect(arrow.startBinding).toEqual({ + elementId: rect1.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }); + expect(arrow.endBinding).toEqual({ + elementId: rect2.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: rect1.id, + boundElements: [ + { id: text.id, type: "text" }, + { id: arrow.id, type: "arrow" }, + ], + }), + expect.objectContaining({ id: text.id }), + expect.objectContaining({ + id: rect2.id, + boundElements: [{ id: arrow.id, type: "arrow" }], + }), + expect.objectContaining({ id: arrow.id, isDeleted: false }), + ]); + }); + + it("should unbind everything from non deleted elements when iterating through the whole undo stack and vice versa rebind everything on redo", async () => { + Keyboard.undo(); + Keyboard.undo(); + Keyboard.undo(); + Keyboard.undo(); + Keyboard.undo(); + + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(5); + expect(h.elements).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: rect1.id, + boundElements: [], + isDeleted: true, + }), + expect.objectContaining({ + id: text.id, + containerId: null, + isDeleted: true, + }), + expect.objectContaining({ + id: rect2.id, + boundElements: [], + isDeleted: true, + }), + expect.objectContaining({ + id: arrow.id, + startBinding: expect.objectContaining({ + elementId: rect1.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }), + endBinding: expect.objectContaining({ + elementId: rect2.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }), + isDeleted: true, + }), + ]), + ); + + Keyboard.redo(); + Keyboard.redo(); + Keyboard.redo(); + Keyboard.redo(); + Keyboard.redo(); + + expect(API.getUndoStack().length).toBe(5); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: rect1.id, + boundElements: expect.arrayContaining([ + { id: text.id, type: "text" }, + { id: arrow.id, type: "arrow" }, + ]), + isDeleted: false, + }), + expect.objectContaining({ + id: text.id, + containerId: rect1.id, + isDeleted: false, + }), + expect.objectContaining({ + id: rect2.id, + boundElements: [{ id: arrow.id, type: "arrow" }], + isDeleted: false, + }), + expect.objectContaining({ + id: arrow.id, + startBinding: expect.objectContaining({ + elementId: rect1.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }), + endBinding: expect.objectContaining({ + elementId: rect2.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }), + isDeleted: false, + }), + ]), + ); + }); + + it("should unbind rectangle from arrow on deletion and rebind on undo", async () => { + mouse.select(rect1); + Keyboard.keyPress(KEYS.DELETE); + expect(API.getUndoStack().length).toBe(7); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: rect1.id, + boundElements: [ + { id: text.id, type: "text" }, + { id: arrow.id, type: "arrow" }, + ], + isDeleted: true, + }), + expect.objectContaining({ + id: text.id, + containerId: rect1.id, + isDeleted: true, + }), + expect.objectContaining({ + id: rect2.id, + boundElements: [{ id: arrow.id, type: "arrow" }], + isDeleted: false, + }), + expect.objectContaining({ + id: arrow.id, + startBinding: null, + endBinding: expect.objectContaining({ + elementId: rect2.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }), + isDeleted: false, + }), + ]), + ); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(6); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: rect1.id, + boundElements: expect.arrayContaining([ + { id: arrow.id, type: "arrow" }, + { id: text.id, type: "text" }, // order has now changed! + ]), + isDeleted: false, + }), + expect.objectContaining({ + id: text.id, + containerId: rect1.id, + isDeleted: false, + }), + expect.objectContaining({ + id: rect2.id, + boundElements: [{ id: arrow.id, type: "arrow" }], + isDeleted: false, + }), + expect.objectContaining({ + id: arrow.id, + startBinding: expect.objectContaining({ + elementId: rect1.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }), + endBinding: expect.objectContaining({ + elementId: rect2.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }), + isDeleted: false, + }), + ]), + ); + }); + + it("should unbind rectangles from arrow on deletion and rebind on undo", async () => { + mouse.select([rect1, rect2]); + Keyboard.keyPress(KEYS.DELETE); + expect(API.getUndoStack().length).toBe(8); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: rect1.id, + boundElements: [ + { id: text.id, type: "text" }, + { id: arrow.id, type: "arrow" }, + ], + isDeleted: true, + }), + expect.objectContaining({ + id: text.id, + containerId: rect1.id, + isDeleted: true, + }), + expect.objectContaining({ + id: rect2.id, + boundElements: [{ id: arrow.id, type: "arrow" }], + isDeleted: true, + }), + expect.objectContaining({ + id: arrow.id, + startBinding: null, + endBinding: null, + isDeleted: false, + }), + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(7); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: rect1.id, + boundElements: expect.arrayContaining([ + { id: arrow.id, type: "arrow" }, + { id: text.id, type: "text" }, // order has now changed! + ]), + isDeleted: false, + }), + expect.objectContaining({ + id: text.id, + containerId: rect1.id, + isDeleted: false, + }), + expect.objectContaining({ + id: rect2.id, + boundElements: [{ id: arrow.id, type: "arrow" }], + isDeleted: false, + }), + expect.objectContaining({ + id: arrow.id, + startBinding: expect.objectContaining({ + elementId: rect1.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }), + endBinding: expect.objectContaining({ + elementId: rect2.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }), + isDeleted: false, + }), + ]), + ); + }); + }); + + it("should disable undo/redo buttons when stacks empty", async () => { + const { container } = await render( + <Excalidraw + initialData={{ + elements: [API.createElement({ type: "rectangle", id: "A" })], + }} + />, + ); + + const undoAction = createUndoAction(h.history, h.store); + const redoAction = createRedoAction(h.history, h.store); + + await waitFor(() => { + expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]); + expect(h.history.isUndoStackEmpty).toBeTruthy(); + expect(h.history.isRedoStackEmpty).toBeTruthy(); + }); + + const undoButton = queryByTestId(container, "button-undo"); + const redoButton = queryByTestId(container, "button-redo"); + + expect(undoButton).toBeDisabled(); + expect(redoButton).toBeDisabled(); + + const rectangle = UI.createElement("rectangle"); + expect(h.elements).toEqual([ + expect.objectContaining({ id: "A" }), + expect.objectContaining({ id: rectangle.id }), + ]); + + expect(h.history.isUndoStackEmpty).toBeFalsy(); + expect(h.history.isRedoStackEmpty).toBeTruthy(); + expect(undoButton).not.toBeDisabled(); + expect(redoButton).toBeDisabled(); + + API.executeAction(undoAction); + + expect(h.history.isUndoStackEmpty).toBeTruthy(); + expect(h.history.isRedoStackEmpty).toBeFalsy(); + expect(undoButton).toBeDisabled(); + expect(redoButton).not.toBeDisabled(); + + API.executeAction(redoAction); + + expect(h.history.isUndoStackEmpty).toBeFalsy(); + expect(h.history.isRedoStackEmpty).toBeTruthy(); + expect(undoButton).not.toBeDisabled(); + expect(redoButton).toBeDisabled(); + }); + + it("remounting undo/redo buttons should initialize undo/redo state correctly", async () => { + const { container } = await render( + <Excalidraw + initialData={{ + elements: [API.createElement({ type: "rectangle", id: "A" })], + }} + />, + ); + + const undoAction = createUndoAction(h.history, h.store); + + await waitFor(() => { + expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]); + expect(h.history.isUndoStackEmpty).toBeTruthy(); + expect(h.history.isRedoStackEmpty).toBeTruthy(); + }); + + expect(queryByTestId(container, "button-undo")).toBeDisabled(); + expect(queryByTestId(container, "button-redo")).toBeDisabled(); + + // testing undo button + // ----------------------------------------------------------------------- + + const rectangle = UI.createElement("rectangle"); + expect(h.elements).toEqual([ + expect.objectContaining({ id: "A" }), + expect.objectContaining({ id: rectangle.id }), + ]); + + expect(h.history.isUndoStackEmpty).toBeFalsy(); + expect(h.history.isRedoStackEmpty).toBeTruthy(); + expect(queryByTestId(container, "button-undo")).not.toBeDisabled(); + expect(queryByTestId(container, "button-redo")).toBeDisabled(); + + API.executeAction(actionToggleViewMode); + expect(h.state.viewModeEnabled).toBe(true); + + expect(queryByTestId(container, "button-undo")).toBeNull(); + expect(queryByTestId(container, "button-redo")).toBeNull(); + + API.executeAction(actionToggleViewMode); + expect(h.state.viewModeEnabled).toBe(false); + + await waitFor(() => { + expect(queryByTestId(container, "button-undo")).not.toBeDisabled(); + expect(queryByTestId(container, "button-redo")).toBeDisabled(); + }); + + // testing redo button + // ----------------------------------------------------------------------- + + API.executeAction(undoAction); + + expect(h.history.isUndoStackEmpty).toBeTruthy(); + expect(h.history.isRedoStackEmpty).toBeFalsy(); + expect(queryByTestId(container, "button-undo")).toBeDisabled(); + expect(queryByTestId(container, "button-redo")).not.toBeDisabled(); + + API.executeAction(actionToggleViewMode); + expect(h.state.viewModeEnabled).toBe(true); + + expect(queryByTestId(container, "button-undo")).toBeNull(); + expect(queryByTestId(container, "button-redo")).toBeNull(); + + API.executeAction(actionToggleViewMode); + expect(h.state.viewModeEnabled).toBe(false); + + expect(h.history.isUndoStackEmpty).toBeTruthy(); + expect(h.history.isRedoStackEmpty).toBeFalsy(); + expect(queryByTestId(container, "button-undo")).toBeDisabled(); + expect(queryByTestId(container, "button-redo")).not.toBeDisabled(); + }); + }); + + describe("multiplayer undo/redo", () => { + // Util to check that we end up in the same state after series of undo / redo + function runTwice(callback: () => void) { + for (let i = 0; i < 2; i++) { + callback(); + } + } + + beforeEach(async () => { + await render( + <Excalidraw handleKeyboardGlobally={true} isCollaborating={true} />, + ); + }); + + it("should not override remote changes on different elements", async () => { + UI.createElement("rectangle", { x: 10 }); + togglePopover("Background"); + UI.clickOnTestId("color-red"); + + expect(API.getUndoStack().length).toBe(2); + + expect(h.elements).toEqual([ + expect.objectContaining({ backgroundColor: red }), + ]); + + // Simulate remote update + API.updateScene({ + elements: [ + ...h.elements, + API.createElement({ + type: "rectangle", + strokeColor: blue, + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + Keyboard.undo(); + expect(h.elements).toEqual([ + expect.objectContaining({ backgroundColor: transparent }), + expect.objectContaining({ strokeColor: blue }), + ]); + + Keyboard.redo(); + expect(h.elements).toEqual([ + expect.objectContaining({ backgroundColor: red }), + expect.objectContaining({ strokeColor: blue }), + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getUndoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ backgroundColor: transparent }), + expect.objectContaining({ strokeColor: blue }), + ]); + }); + + it("should not override remote changes on different properties", async () => { + UI.createElement("rectangle", { x: 10 }); + togglePopover("Background"); + UI.clickOnTestId("color-red"); + + expect(API.getUndoStack().length).toBe(2); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { + strokeColor: yellow, + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + Keyboard.undo(); + expect(h.elements).toEqual([ + expect.objectContaining({ + backgroundColor: transparent, + strokeColor: yellow, + }), + ]); + + Keyboard.redo(); + expect(h.elements).toEqual([ + expect.objectContaining({ + backgroundColor: red, + strokeColor: yellow, + }), + ]); + }); + + // https://www.figma.com/blog/how-figmas-multiplayer-technology-works/#implementing-undo + // This is due to the fact that deltas are updated in `applyLatestChanges`. + it("should update history entries after remote changes on the same properties", async () => { + UI.createElement("rectangle", { x: 10 }); + togglePopover("Background"); + UI.clickOnTestId("color-red"); + UI.clickOnTestId("color-blue"); + + // At this point we have all the history entries created, no new entries will be created, only existing entries will get inversed and updated + expect(API.getUndoStack().length).toBe(3); + + Keyboard.undo(); + expect(h.elements).toEqual([ + expect.objectContaining({ backgroundColor: red }), + ]); + + Keyboard.redo(); + expect(h.elements).toEqual([ + expect.objectContaining({ backgroundColor: blue }), + ]); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { + backgroundColor: yellow, + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + // At this point our entry gets updated from `red` -> `blue` into `red` -> `yellow` + Keyboard.undo(); + expect(h.elements).toEqual([ + expect.objectContaining({ backgroundColor: red }), + ]); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { + backgroundColor: violet, + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + // At this point our (inversed) entry gets updated from `red` -> `yellow` into `violet` -> `yellow` + Keyboard.redo(); + expect(h.elements).toEqual([ + expect.objectContaining({ backgroundColor: yellow }), + ]); + + Keyboard.undo(); + expect(h.elements).toEqual([ + expect.objectContaining({ backgroundColor: violet }), + ]); + + Keyboard.undo(); + expect(h.elements).toEqual([ + expect.objectContaining({ backgroundColor: transparent }), + ]); + }); + + it("should redraw arrows on undo", () => { + const rect = API.createElement({ + type: "rectangle", + id: "KPrBI4g_v9qUB1XxYLgSz", + x: 873, + y: 212, + width: 157, + height: 126, + }); + const diamond = API.createElement({ + id: "u2JGnnmoJ0VATV4vCNJE5", + type: "diamond", + x: 1152, + y: 516, + width: 124, + height: 129, + }); + const arrow = API.createElement({ + type: "arrow", + id: "6Rm4g567UQM4WjLwej2Vc", + elbowed: true, + }); + + API.updateScene({ + elements: [rect, diamond], + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); + + // Connect the arrow + API.updateScene({ + elements: [ + { + ...rect, + boundElements: [ + { + id: "6Rm4g567UQM4WjLwej2Vc", + type: "arrow", + }, + ], + }, + { + ...diamond, + boundElements: [ + { + id: "6Rm4g567UQM4WjLwej2Vc", + type: "arrow", + }, + ], + }, + { + ...arrow, + x: 1035, + y: 274.9, + width: 178.9000000000001, + height: 236.10000000000002, + points: [ + pointFrom(0, 0), + pointFrom(178.9000000000001, 0), + pointFrom(178.9000000000001, 236.10000000000002), + ], + startBinding: { + elementId: "KPrBI4g_v9qUB1XxYLgSz", + focus: -0.001587301587301948, + gap: 5, + fixedPoint: [1.0318471337579618, 0.49920634920634904], + } as FixedPointBinding, + endBinding: { + elementId: "u2JGnnmoJ0VATV4vCNJE5", + focus: -0.0016129032258049847, + gap: 3.537079145500037, + fixedPoint: [0.4991935483870975, -0.03875193720914723], + } as FixedPointBinding, + }, + ], + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); + + Keyboard.undo(); + + API.updateScene({ + elements: h.elements.map((el) => + el.id === "KPrBI4g_v9qUB1XxYLgSz" + ? { + ...el, + x: 600, + y: 0, + } + : el, + ), + captureUpdate: CaptureUpdateAction.NEVER, + }); + + Keyboard.undo(); + + const modifiedArrow = h.elements.filter( + (el) => el.type === "arrow", + )[0] as ExcalidrawElbowArrowElement; + expect(modifiedArrow.points).toCloselyEqualPoints([ + [0, 0], + [178.9, 0], + [178.9, 236.1], + ]); + }); + + // TODO: #7348 ideally we should not override, but since the order of groupIds matters, right now we cannot ensure that with postprocssed groupIds the order will be consistent after series or undos/redos, we don't postprocess them at all + // in other words, if we would postprocess groupIds, the groupIds order on "redo" below would be ["B", "A"] instead of ["A", "B"] + it("should override remotely added groups on undo, but restore them on redo", async () => { + const rect1 = API.createElement({ type: "rectangle" }); + const rect2 = API.createElement({ type: "rectangle" }); + + // Initialize scene + API.updateScene({ + elements: [rect1, rect2], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + // Simulate local update + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { groupIds: ["A"] }), + newElementWith(h.elements[1], { groupIds: ["A"] }), + ], + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); + + const rect3 = API.createElement({ type: "rectangle", groupIds: ["B"] }); + const rect4 = API.createElement({ type: "rectangle", groupIds: ["B"] }); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { groupIds: ["A", "B"] }), + newElementWith(h.elements[1], { groupIds: ["A", "B"] }), + rect3, + rect4, + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id, groupIds: [] }), + expect.objectContaining({ id: rect2.id, groupIds: [] }), + expect.objectContaining({ id: rect3.id, groupIds: ["B"] }), + expect.objectContaining({ id: rect4.id, groupIds: ["B"] }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id, groupIds: ["A", "B"] }), + expect.objectContaining({ id: rect2.id, groupIds: ["A", "B"] }), + expect.objectContaining({ id: rect3.id, groupIds: ["B"] }), + expect.objectContaining({ id: rect4.id, groupIds: ["B"] }), + ]); + }); + + it("should override remotely added points on undo, but restore them on redo", async () => { + UI.clickTool("arrow"); + mouse.click(0, 0); + mouse.click(10, 10); + mouse.click(20, 20); + + // actionFinalize + Keyboard.keyPress(KEYS.ENTER); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(h.elements[0] as ExcalidrawLinearElement, { + points: [ + pointFrom(0, 0), + pointFrom(5, 5), + pointFrom(10, 10), + pointFrom(15, 15), + pointFrom(20, 20), + ] as LocalPoint[], + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + Keyboard.undo(); // undo `actionFinalize` + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(2); + expect(h.elements).toEqual([ + expect.objectContaining({ + points: [ + [0, 0], + // overriding all the remote points as they are not being postprocessed (as we cannot ensure the order consistency similar to groupIds) + // but in this case it might not make even sense to combine the points, as in some cases the linear element might lead unexpected results + [10, 10], + ], + }), + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(3); + expect(h.elements).toEqual([ + expect.objectContaining({ + isDeleted: true, + points: [ + [0, 0], + [10, 10], + ], + }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(2); + expect(h.elements).toEqual([ + expect.objectContaining({ + isDeleted: false, + points: [ + [0, 0], + [10, 10], + ], + }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ + points: [ + [0, 0], + [5, 5], + [10, 10], + [15, 15], + [20, 20], + ], + }), + ]); + + Keyboard.redo(); // redo `actionFinalize` + expect(API.getUndoStack().length).toBe(3); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + points: [ + [0, 0], + [5, 5], + [10, 10], + [15, 15], + [20, 20], + ], + }), + ]); + }); + + it("should redistribute deltas when element gets removed locally but is restored remotely", async () => { + UI.createElement("rectangle", { x: 10 }); + Keyboard.keyDown(KEYS.DELETE); + + expect(API.getUndoStack().length).toBe(2); + expect(h.elements).toEqual([ + expect.objectContaining({ + backgroundColor: transparent, + isDeleted: true, + }), + ]); + + // Simulate remote update & restore + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { + backgroundColor: yellow, + isDeleted: false, // undeletion might happen due to concurrency between clients + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + expect(API.getSelectedElements()).toEqual([]); + expect(h.elements).toEqual([ + expect.objectContaining({ + backgroundColor: yellow, + isDeleted: false, + }), + ]); + + // inserted.isDeleted: true is updated with the latest changes to false + // deleted.isDeleted and inserted.isDeleted are the same and therefore removed delta becomes an updated delta + Keyboard.undo(); + expect(assertSelectedElements(h.elements[0])); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ + backgroundColor: yellow, + isDeleted: false, + }), + ]); + + Keyboard.undo(); + expect(API.getSelectedElements()).toEqual([]); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(2); + expect(h.elements).toEqual([ + expect.objectContaining({ + backgroundColor: yellow, + isDeleted: true, + }), + ]); + + Keyboard.redo(); + expect(assertSelectedElements(h.elements[0])); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ + backgroundColor: yellow, + isDeleted: false, + }), + ]); + + Keyboard.redo(); + expect(assertSelectedElements([])); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + backgroundColor: yellow, + isDeleted: false, // isDeleted got updated + }), + ]); + }); + + it("should iterate through the history when when element change relates to remotely deleted element", async () => { + UI.createElement("rectangle", { x: 10 }); + togglePopover("Background"); + UI.clickOnTestId("color-red"); + + expect(API.getUndoStack().length).toBe(2); + + expect(h.elements).toEqual([ + expect.objectContaining({ backgroundColor: red }), + ]); + + // Simulate remote update & deletion + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { + backgroundColor: yellow, + isDeleted: true, + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + expect(h.elements).toEqual([ + expect.objectContaining({ + backgroundColor: yellow, + isDeleted: true, + }), + ]); + + // Will iterate through undo stack since applying the change + // results in no visible change on a deleted element + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(2); + expect(h.elements).toEqual([ + expect.objectContaining({ + backgroundColor: transparent, + isDeleted: true, + }), + ]); + + // We reached the bottom, again we iterate through invisible changes and reach the top + Keyboard.redo(); + assertSelectedElements(); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + backgroundColor: yellow, // the color still get's updated + isDeleted: true, // but the element remains deleted + }), + ]); + }); + + it("should iterate through the history when element changes relate only to remotely deleted elements", async () => { + const rect1 = UI.createElement("rectangle", { x: 10 }); + + const rect2 = UI.createElement("rectangle", { x: 20 }); + togglePopover("Background"); + UI.clickOnTestId("color-red"); + + const rect3 = UI.createElement("rectangle", { x: 30, y: 30 }); + + // move rect3 + mouse.downAt(35, 35); + mouse.moveTo(55, 55); + mouse.upAt(55, 55); + + expect(API.getUndoStack().length).toBe(5); + + // Simulate remote update + API.updateScene({ + elements: [ + h.elements[0], + newElementWith(h.elements[1], { + isDeleted: true, + }), + newElementWith(h.elements[2], { + isDeleted: true, + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(4); + expect(API.getSelectedElements()).toEqual([ + expect.objectContaining({ id: rect1.id }), + ]); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: rect1.id, + isDeleted: false, + }), + expect.objectContaining({ + id: rect2.id, + isDeleted: true, + backgroundColor: transparent, + }), + expect.objectContaining({ + id: rect3.id, + isDeleted: true, + x: 30, + y: 30, + }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(5); + expect(API.getRedoStack().length).toBe(0); + expect(API.getSelectedElements()).toEqual([]); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: rect1.id, + isDeleted: false, + }), + expect.objectContaining({ + id: rect2.id, + isDeleted: true, + backgroundColor: red, + }), + expect.objectContaining({ + id: rect3.id, + isDeleted: true, + x: 50, + y: 50, + }), + ]); + }); + + it("should iterate through the history when selected elements relate only to remotely deleted elements", async () => { + const rect1 = API.createElement({ type: "rectangle", x: 10, y: 10 }); + const rect2 = API.createElement({ type: "rectangle", x: 20, y: 20 }); + const rect3 = API.createElement({ type: "rectangle", x: 30, y: 30 }); + + API.setElements([rect1, rect2, rect3]); + mouse.select(rect1); + mouse.select([rect2, rect3]); + + expect(API.getUndoStack().length).toBe(3); + expect(API.getSelectedElements()).toEqual([ + expect.objectContaining({ id: rect2.id }), + expect.objectContaining({ id: rect3.id }), + ]); + + // Simulate remote update + API.updateScene({ + elements: [ + h.elements[0], + newElementWith(h.elements[1], { + isDeleted: true, + }), + newElementWith(h.elements[2], { + isDeleted: true, + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(2); + expect(API.getSelectedElements()).toEqual([ + expect.objectContaining({ id: rect1.id, isDeleted: false }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(3); + expect(API.getRedoStack().length).toBe(0); + // do not expect any selectedElementIds, as all relate to deleted elements + expect(API.getSelectedElements()).toEqual([]); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id, isDeleted: false }), + expect.objectContaining({ id: rect2.id, isDeleted: true }), + expect.objectContaining({ id: rect3.id, isDeleted: true }), + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(2); + expect(API.getSelectedElements()).toEqual([ + expect.objectContaining({ id: rect1.id, isDeleted: false }), + ]); + + // Simulate remote update + API.updateScene({ + elements: [ + h.elements[0], + newElementWith(h.elements[1], { + isDeleted: false, + }), + newElementWith(h.elements[2], { + isDeleted: false, + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(1); + expect(API.getSelectedElements()).toEqual([ + expect.objectContaining({ id: rect2.id, isDeleted: false }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(3); + expect(API.getRedoStack().length).toBe(0); + // now we again expect these as selected, as they got restored remotely + expect(API.getSelectedElements()).toEqual([ + expect.objectContaining({ id: rect2.id }), + expect.objectContaining({ id: rect3.id }), + ]); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id, isDeleted: false }), + expect.objectContaining({ id: rect2.id, isDeleted: false }), + expect.objectContaining({ id: rect3.id, isDeleted: false }), + ]); + }); + + it("should iterate through the history when selected groups contain only remotely deleted elements", async () => { + const rect1 = API.createElement({ + type: "rectangle", + groupIds: ["A"], + }); + const rect2 = API.createElement({ + type: "rectangle", + groupIds: ["A"], + }); + const rect3 = API.createElement({ + type: "rectangle", + groupIds: ["B"], + }); + const rect4 = API.createElement({ + type: "rectangle", + groupIds: ["B"], + }); + + // Simulate remote update + API.updateScene({ + elements: [rect1, rect2], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.A); + }); + + // Simulate remote update + API.updateScene({ + elements: [h.elements[0], h.elements[1], rect3, rect4], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.A); + }); + + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(0); + expect(h.state.selectedGroupIds).toEqual({ A: true, B: true }); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { + isDeleted: true, + }), + newElementWith(h.elements[1], { + isDeleted: true, + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(2); // iterated two steps back! + expect(h.state.selectedGroupIds).toEqual({}); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(2); // iterated two steps forward! + expect(API.getRedoStack().length).toBe(0); + expect(h.state.selectedGroupIds).toEqual({}); + + Keyboard.undo(); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { + isDeleted: false, + }), + newElementWith(h.elements[1], { + isDeleted: false, + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(1); + expect(h.state.selectedGroupIds).toEqual({ A: true }); + + // Simulate remote update + API.updateScene({ + elements: [h.elements[0], h.elements[1], rect3, rect4], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(0); + expect(h.state.selectedGroupIds).toEqual({ A: true, B: true }); + }); + + it("should iterate through the history when editing group contains only remotely deleted elements", async () => { + const rect1 = API.createElement({ + type: "rectangle", + groupIds: ["A"], + x: 0, + }); + const rect2 = API.createElement({ + type: "rectangle", + groupIds: ["A"], + x: 100, + }); + + API.setElements([rect1, rect2]); + mouse.select(rect1); + + // inside the editing group + mouse.doubleClickOn(rect2); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(0); + expect(h.state.editingGroupId).toBe("A"); + + mouse.clickAt(-10, -10); + expect(API.getSelectedElements().length).toBe(0); + expect(API.getUndoStack().length).toBe(3); + expect(API.getRedoStack().length).toBe(0); + expect(h.state.editingGroupId).toBeNull(); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { + isDeleted: true, + }), + newElementWith(h.elements[1], { + isDeleted: true, + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(3); + expect(h.state.editingGroupId).toBeNull(); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(3); + expect(API.getRedoStack().length).toBe(0); + expect(h.state.editingGroupId).toBeNull(); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { + isDeleted: false, + }), + h.elements[1], + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(1); + expect(h.state.editingGroupId).toBe("A"); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(3); + expect(API.getRedoStack().length).toBe(0); + expect(h.state.editingGroupId).toBeNull(); + }); + + it("should iterate through the history when selected or editing linear element was remotely deleted", async () => { + // create three point arrow + UI.clickTool("arrow"); + mouse.click(0, 0); + mouse.click(10, 10); + + // actionFinalize + Keyboard.keyPress(KEYS.ENTER); + + // open editor + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.ENTER); + }); + + // leave editor + Keyboard.keyPress(KEYS.ESCAPE); + + expect(API.getUndoStack().length).toBe(4); + expect(API.getRedoStack().length).toBe(0); + expect(h.state.editingLinearElement).toBeNull(); + expect(h.state.selectedLinearElement).not.toBeNull(); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { + isDeleted: true, + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(3); + expect(h.state.editingLinearElement).toBeNull(); + expect(h.state.selectedLinearElement).toBeNull(); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(4); + expect(API.getRedoStack().length).toBe(0); + expect(h.state.editingLinearElement).toBeNull(); + expect(h.state.selectedLinearElement).toBeNull(); + }); + + it("should iterate through the history when z-index changes do not produce visible change and we synced changed indices", async () => { + const rect1 = API.createElement({ type: "rectangle", x: 10, y: 10 }); // a "a0" + const rect2 = API.createElement({ type: "rectangle", x: 20, y: 20 }); // b "a1" + const rect3 = API.createElement({ type: "rectangle", x: 30, y: 30 }); // c "a2" + + API.setElements([rect1, rect2, rect3]); + + mouse.select(rect2); + + API.executeAction(actionSendToBack); + + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(0); + assertSelectedElements([rect2]); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect2.id }), // b "Zz" + expect.objectContaining({ id: rect1.id }), // a "a0" + expect.objectContaining({ id: rect3.id }), // c "a2" + ]); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(h.elements[2], { index: "Zy" as FractionalIndex }), + h.elements[0], + h.elements[1], + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(0); + assertSelectedElements([rect2]); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect3.id }), // c "Zy" + expect.objectContaining({ id: rect2.id }), // b "Zz" + expect.objectContaining({ id: rect1.id }), // a "a0" + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(1); + assertSelectedElements([rect2]); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect3.id }), // c "Zy" + expect.objectContaining({ id: rect1.id }), // a "a0" + expect.objectContaining({ id: rect2.id }), // b "a1" + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(0); + assertSelectedElements([rect2]); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect3.id }), // c "Zy" + expect.objectContaining({ id: rect2.id }), // b "Zz" + expect.objectContaining({ id: rect1.id }), // a "a0" + ]); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(h.elements[2], { index: "Zx" as FractionalIndex }), + h.elements[0], + h.elements[1], + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(0); + assertSelectedElements([rect2]); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id }), // a "Zx" + expect.objectContaining({ id: rect3.id }), // c "Zy" + expect.objectContaining({ id: rect2.id }), // b "Zz" + ]); + + Keyboard.undo(); + // We iterated two steps as there was no change in order! + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(2); + expect(API.getSelectedElements().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect1.id }), // a "Zx" + expect.objectContaining({ id: rect3.id }), // c "Zy" + expect.objectContaining({ id: rect2.id }), // b "a1" + ]); + }); + + it("should iterate through the history when z-index changes do not produce visible change and we synced all indices", async () => { + const rect1 = API.createElement({ type: "rectangle", x: 10, y: 10 }); + const rect2 = API.createElement({ type: "rectangle", x: 20, y: 20 }); + const rect3 = API.createElement({ type: "rectangle", x: 30, y: 30 }); + + API.setElements([rect1, rect2, rect3]); + + mouse.select(rect2); + + API.executeAction(actionSendToBack); + + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(0); + assertSelectedElements([rect2]); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect2.id }), + expect.objectContaining({ id: rect1.id }), + expect.objectContaining({ id: rect3.id }), + ]); + + // Simulate remote update (fixes all invalid z-indices) + API.updateScene({ + elements: [ + h.elements[2], // rect3 + h.elements[0], // rect2 + h.elements[1], // rect1 + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(1); + assertSelectedElements([rect2]); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect2.id }), + expect.objectContaining({ id: rect3.id }), + expect.objectContaining({ id: rect1.id }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(0); + assertSelectedElements([rect2]); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect3.id }), + expect.objectContaining({ id: rect2.id }), + expect.objectContaining({ id: rect1.id }), + ]); + + // Simulate remote update + API.updateScene({ + elements: [ + h.elements[1], // rect2 + h.elements[0], // rect3 + h.elements[2], // rect1 + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(2); // now we iterated two steps back! + assertSelectedElements([]); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect2.id }), + expect.objectContaining({ id: rect3.id }), + expect.objectContaining({ id: rect1.id }), + ]); + }); + + it("should not let remote changes to interfere with in progress freedraw", async () => { + UI.clickTool("freedraw"); + mouse.down(10, 10); + mouse.moveTo(30, 30); + + const rectProps = { + type: "rectangle", + strokeColor: blue, + } as const; + + // Simulate remote update + const rect = API.createElement({ ...rectProps }); + + // Simulate remote update + API.updateScene({ + elements: [...h.elements, rect], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + mouse.moveTo(60, 60); + mouse.up(); + + Keyboard.undo(); + + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: h.elements[0].id, + type: "freedraw", + isDeleted: true, + }), + expect.objectContaining(rectProps), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: h.elements[0].id, + type: "freedraw", + isDeleted: false, + }), + expect.objectContaining(rectProps), + ]); + }); + + it("should not let remote changes to interfere with in progress resizing", async () => { + const props1 = { x: 10, y: 10, width: 10, height: 10 }; + const rect1 = UI.createElement("rectangle", { ...props1 }); + + mouse.downAt(20, 20); + mouse.moveTo(40, 40); + + assertSelectedElements(rect1); + expect(API.getUndoStack().length).toBe(1); + + const rect3Props = { + type: "rectangle", + strokeColor: blue, + } as const; + + const rect3 = API.createElement({ ...rect3Props }); + + // // Simulate remote update + API.updateScene({ + elements: [...h.elements, rect3], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + mouse.moveTo(100, 100); + mouse.up(); + + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(0); + assertSelectedElements(rect1); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: rect1.id, + ...props1, + isDeleted: false, + width: 90, + height: 90, + }), + expect.objectContaining(rect3Props), + ]); + + Keyboard.undo(); + assertSelectedElements(rect1); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: rect1.id, + ...props1, + isDeleted: false, + }), + expect.objectContaining(rect3Props), + ]); + + Keyboard.undo(); + expect(API.getSelectedElements()).toEqual([]); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: rect1.id, + ...props1, + isDeleted: true, + }), + expect.objectContaining(rect3Props), + ]); + + Keyboard.redo(); + assertSelectedElements(rect1); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: rect1.id, + ...props1, + isDeleted: false, + }), + expect.objectContaining(rect3Props), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(0); + assertSelectedElements(rect1); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: rect1.id, + ...props1, + isDeleted: false, + width: 90, + height: 90, + }), + expect.objectContaining(rect3Props), + ]); + }); + + it("should not let remote changes to interfere with in progress dragging", async () => { + const rect1 = UI.createElement("rectangle", { x: 10, y: 10 }); + const rect2 = UI.createElement("rectangle", { x: 30, y: 30 }); + + mouse.select([rect1, rect2]); + mouse.downAt(20, 20); + mouse.moveTo(50, 50); + + assertSelectedElements(rect1, rect2); + expect(API.getUndoStack().length).toBe(4); + + const rect3Props = { + type: "rectangle", + strokeColor: blue, + } as const; + + const rect3 = API.createElement({ ...rect3Props }); + + // Simulate remote update + API.updateScene({ + elements: [...h.elements, rect3], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + mouse.moveTo(100, 100); + mouse.up(); + + expect(API.getUndoStack().length).toBe(5); + expect(API.getRedoStack().length).toBe(0); + assertSelectedElements(rect1, rect2); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: rect1.id, + x: 90, + y: 90, + isDeleted: false, + }), + expect.objectContaining({ + id: rect2.id, + x: 110, + y: 110, + isDeleted: false, + }), + expect.objectContaining(rect3Props), + ]); + + Keyboard.undo(); + assertSelectedElements(rect1, rect2); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: rect1.id, + x: 10, + y: 10, + isDeleted: false, + }), + expect.objectContaining({ + id: rect2.id, + x: 30, + y: 30, + isDeleted: false, + }), + expect.objectContaining(rect3Props), + ]); + + Keyboard.undo(); + assertSelectedElements(rect1); + + Keyboard.undo(); + assertSelectedElements(rect2); + + Keyboard.undo(); + assertSelectedElements(rect1); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: rect1.id, + x: 10, + y: 10, + isDeleted: false, + }), + expect.objectContaining({ + id: rect2.id, + x: 30, + y: 30, + isDeleted: true, + }), + expect.objectContaining(rect3Props), + ]); + + Keyboard.undo(); + assertSelectedElements(); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: rect1.id, + x: 10, + y: 10, + isDeleted: true, + }), + expect.objectContaining({ + id: rect2.id, + x: 30, + y: 30, + isDeleted: true, + }), + expect.objectContaining(rect3Props), + ]); + + Keyboard.redo(); + assertSelectedElements(rect1); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: rect1.id, + x: 10, + y: 10, + isDeleted: false, + }), + expect.objectContaining({ + id: rect2.id, + x: 30, + y: 30, + isDeleted: true, + }), + expect.objectContaining(rect3Props), + ]); + + Keyboard.redo(); + assertSelectedElements(rect2); + + Keyboard.redo(); + assertSelectedElements(rect1); + + Keyboard.redo(); + assertSelectedElements(rect1, rect2); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: rect1.id, + x: 10, + y: 10, + isDeleted: false, + }), + expect.objectContaining({ + id: rect2.id, + x: 30, + y: 30, + isDeleted: false, + }), + expect.objectContaining(rect3Props), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(5); + expect(API.getRedoStack().length).toBe(0); + assertSelectedElements(rect1, rect2); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: rect1.id, + x: 90, + y: 90, + isDeleted: false, + }), + expect.objectContaining({ + id: rect2.id, + x: 110, + y: 110, + isDeleted: false, + }), + expect.objectContaining(rect3Props), + ]); + }); + + describe("conflicts in bound text elements and their containers", () => { + let container: ExcalidrawGenericElement; + let text: ExcalidrawTextElement; + + const containerProps = { + type: "rectangle", + width: 100, + x: 10, + y: 10, + angle: 0, + } as const; + + const textProps = { + type: "text", + text: "que pasa", + x: 15, + y: 15, + angle: 0, + } as const; + + beforeEach(() => { + container = API.createElement({ ...containerProps }); + text = API.createElement({ ...textProps }); + }); + + it("should rebind bindings when both are updated through the history and there no conflicting updates in the meantime", async () => { + // Initialize the scene + API.updateScene({ + elements: [container, text], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + // Simulate local update + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { + boundElements: [{ id: text.id, type: "text" }], + }), + newElementWith(h.elements[1] as ExcalidrawTextElement, { + containerId: container.id, + }), + ], + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: container.id, + boundElements: [], + isDeleted: false, + }), + expect.objectContaining({ + id: text.id, + containerId: null, + isDeleted: false, + }), + ]); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { + // no conflicting updates + x: h.elements[1].x + 20, + }), + newElementWith(h.elements[1] as ExcalidrawTextElement, { + // no conflicting updates + x: h.elements[1].x + 10, + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + runTwice(() => { + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: container.id, + boundElements: [{ id: text.id, type: "text" }], + isDeleted: false, + }), + expect.objectContaining({ + id: text.id, + containerId: container.id, + isDeleted: false, + }), + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: container.id, + boundElements: [], + isDeleted: false, + }), + expect.objectContaining({ + id: text.id, + containerId: null, + isDeleted: false, + }), + ]); + }); + }); + + // TODO: #7348 we do rebind now, when we have bi-directional binding in history, to eliminate potential data-integrity issues, but we should consider not rebinding in the future + it("should rebind bindings when both are updated through the history and the container got bound to a different text in the meantime", async () => { + // Initialize the scene + API.updateScene({ + elements: [container, text], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + // Simulate local update + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { + boundElements: [{ id: text.id, type: "text" }], + }), + newElementWith(h.elements[1] as ExcalidrawTextElement, { + containerId: container.id, + }), + ], + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: container.id, + boundElements: [], + isDeleted: false, + }), + expect.objectContaining({ + id: text.id, + containerId: null, + isDeleted: false, + }), + ]); + + const remoteText = API.createElement({ + type: "text", + text: "ola", + containerId: container.id, + }); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { + boundElements: [{ id: remoteText.id, type: "text" }], + }), + remoteText, + h.elements[1], + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + runTwice(() => { + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: container.id, + // last added was `text.id`, removing `remoteText.id` + boundElements: [{ id: text.id, type: "text" }], + isDeleted: false, + }), + expect.objectContaining({ + id: remoteText.id, + // unbound as `remoteText.id` was removed + containerId: null, + isDeleted: false, + }), + expect.objectContaining({ + id: text.id, + // rebound! + containerId: container.id, + isDeleted: false, + }), + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: container.id, + boundElements: [{ id: remoteText.id, type: "text" }], + isDeleted: false, + }), + expect.objectContaining({ + id: remoteText.id, + containerId: container.id, + isDeleted: false, + }), + expect.objectContaining({ + id: text.id, + containerId: null, + isDeleted: false, + }), + ]); + }); + }); + + // TODO: #7348 we do rebind now, when we have bi-directional binding in history, to eliminate potential data-integrity issues, but we should consider not rebinding in the future + it("should rebind bindings when both are updated through the history and the text got bound to a different container in the meantime", async () => { + // Initialize the scene + API.updateScene({ + elements: [container, text], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + // Simulate local update + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { + boundElements: [{ id: text.id, type: "text" }], + }), + newElementWith(h.elements[1] as ExcalidrawTextElement, { + containerId: container.id, + }), + ], + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: container.id, + boundElements: [], + isDeleted: false, + }), + expect.objectContaining({ + id: text.id, + containerId: null, + isDeleted: false, + }), + ]); + + const remoteContainer = API.createElement({ + type: "rectangle", + width: 50, + x: 100, + boundElements: [{ id: text.id, type: "text" }], + }); + + // Simulate remote update + API.updateScene({ + elements: [ + h.elements[0], + newElementWith(remoteContainer, { + boundElements: [{ id: text.id, type: "text" }], + }), + newElementWith(h.elements[1] as ExcalidrawTextElement, { + containerId: remoteContainer.id, + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + runTwice(() => { + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: container.id, + // rebound the text as we captured the full bidirectional binding in history! + boundElements: [{ id: text.id, type: "text" }], + isDeleted: false, + }), + expect.objectContaining({ + id: remoteContainer.id, + // previous binding got unbound, as text is no longer bound to this element + boundElements: [], + isDeleted: false, + }), + expect.objectContaining({ + id: text.id, + // rebound! + containerId: container.id, + isDeleted: false, + }), + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: container.id, + // deleted binding (already during applyDelta) + boundElements: [], + isDeleted: false, + }), + expect.objectContaining({ + id: remoteContainer.id, + // #2 due to restored binding in #1, we could rebind the remote container! + boundElements: [{ id: text.id, type: "text" }], + isDeleted: false, + }), + expect.objectContaining({ + id: text.id, + // #1 due to applying latest changes to the history entries, we could restore this binding + containerId: remoteContainer.id, + isDeleted: false, + }), + ]); + }); + }); + + it("should rebind remotely added bound text when it's container is added through the history", async () => { + // Simulate local update + API.updateScene({ + elements: [container], + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { + boundElements: [{ id: text.id, type: "text" }], + }), + newElementWith(text, { containerId: container.id }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + runTwice(() => { + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ + ...containerProps, + id: container.id, + // binding from deleted to non deleted is correct! + // so that we could restore the bindings on history actions (subsequent redo in this case) + boundElements: [{ id: text.id, type: "text" }], + isDeleted: true, + }), + expect.objectContaining({ + ...textProps, + id: text.id, + // we trigger unbind - binding from non deleted to deleted cannot exist! + containerId: null, + isDeleted: false, + }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + ...containerProps, + id: container.id, + boundElements: [{ id: text.id, type: "text" }], + isDeleted: false, + }), + expect.objectContaining({ + ...textProps, + id: text.id, + // we triggered rebind! + containerId: container.id, + isDeleted: false, + }), + ]); + }); + }); + + it("should rebind remotely added container when it's bound text is added through the history", async () => { + // Simulate local update + API.updateScene({ + elements: [text], + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(container, { + boundElements: [{ id: text.id, type: "text" }], + }), + newElementWith(text, { containerId: container.id }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + runTwice(() => { + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ + ...containerProps, + id: container.id, + // we trigged unbind - bindings from non deleted to deleted cannot exist! + boundElements: [], + isDeleted: false, + }), + expect.objectContaining({ + ...textProps, + // binding from deleted to non deleted is correct, so that we could restore the bindings on history actions + containerId: container.id, + id: text.id, + isDeleted: true, + }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + ...containerProps, + id: container.id, + // we triggered rebind! + boundElements: [{ id: text.id, type: "text" }], + isDeleted: false, + }), + expect.objectContaining({ + ...textProps, + containerId: container.id, + id: text.id, + isDeleted: false, + }), + ]); + }); + }); + + it("should preserve latest remotely added binding and unbind previous one when the container is added through the history", async () => { + // Simulate local update + API.updateScene({ + elements: [container], + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { + boundElements: [{ id: text.id, type: "text" }], + }), + newElementWith(text, { + containerId: container.id, + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: container.id, + boundElements: [{ id: text.id, type: "text" }], + isDeleted: true, + }), + expect.objectContaining({ + id: text.id, + // unbound! + containerId: null, + isDeleted: false, + }), + ]); + + const remoteText = API.createElement({ + type: "text", + text: "ola", + containerId: container.id, + }); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { + boundElements: [{ id: remoteText.id, type: "text" }], + isDeleted: false, // purposefully undeleting, mimicing concurrenct update + }), + h.elements[1], + // rebinding the container with a new text element! + remoteText, + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + runTwice(() => { + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: container.id, + // previously bound text is preserved + // text bindings are not duplicated + boundElements: [{ id: remoteText.id, type: "text" }], + isDeleted: false, + }), + expect.objectContaining({ + id: text.id, + // unbound + containerId: null, + isDeleted: false, + }), + expect.objectContaining({ + id: remoteText.id, + // preserved existing binding! + containerId: container.id, + isDeleted: false, + }), + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: container.id, + boundElements: [{ id: remoteText.id, type: "text" }], + isDeleted: false, // isDeleted got remotely updated to false + }), + expect.objectContaining({ + id: text.id, + containerId: null, + isDeleted: false, + }), + expect.objectContaining({ + id: remoteText.id, + // unbound + containerId: container.id, + isDeleted: false, + }), + ]); + }); + }); + + it("should preserve latest remotely added binding and unbind previous one when the text is added through history", async () => { + // Simulate local update + API.updateScene({ + elements: [text], + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(container, { + boundElements: [{ id: text.id, type: "text" }], + }), + newElementWith(h.elements[0] as ExcalidrawTextElement, { + containerId: container.id, + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: container.id, + // unbind affected bindable element + boundElements: [], + isDeleted: false, + }), + expect.objectContaining({ + id: text.id, + containerId: container.id, + isDeleted: true, + }), + ]); + + const remoteText = API.createElement({ + type: "text", + text: "ola", + containerId: container.id, + }); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { + boundElements: [{ id: remoteText.id, type: "text" }], + }), + h.elements[1], + newElementWith(remoteText as ExcalidrawTextElement, { + containerId: container.id, + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + runTwice(() => { + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: container.id, + // previously bound text is preserved + // text bindings are not duplicated + boundElements: [{ id: remoteText.id, type: "text" }], + isDeleted: false, + }), + expect.objectContaining({ + id: text.id, + // unbound from container! + containerId: null, + isDeleted: false, + }), + expect.objectContaining({ + id: remoteText.id, + // preserved existing binding! + containerId: container.id, + isDeleted: false, + }), + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: container.id, + boundElements: [{ id: remoteText.id, type: "text" }], + isDeleted: false, + }), + expect.objectContaining({ + id: text.id, + containerId: container.id, + isDeleted: true, + }), + expect.objectContaining({ + id: remoteText.id, + containerId: container.id, + isDeleted: false, + }), + ]); + }); + }); + + it("should unbind remotely deleted bound text from container when the container is added through the history", async () => { + // Simulate local update + API.updateScene({ + elements: [container], + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { + boundElements: [{ id: text.id, type: "text" }], + }), + newElementWith(text, { + containerId: container.id, + isDeleted: true, + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + runTwice(() => { + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: container.id, + boundElements: [{ id: text.id, type: "text" }], + isDeleted: true, + }), + expect.objectContaining({ + id: text.id, + containerId: container.id, + isDeleted: true, + }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: container.id, + // unbound! + boundElements: [], + isDeleted: false, + }), + expect.objectContaining({ + id: text.id, + containerId: container.id, + isDeleted: true, + }), + ]); + }); + }); + + it("should unbind remotely deleted container from bound text when the text is added through the history", async () => { + // Simulate local update + API.updateScene({ + elements: [text], + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(container, { + boundElements: [{ id: text.id, type: "text" }], + isDeleted: true, + }), + newElementWith(h.elements[0] as ExcalidrawTextElement, { + containerId: container.id, + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + runTwice(() => { + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: container.id, + boundElements: [{ id: text.id, type: "text" }], + isDeleted: true, + }), + expect.objectContaining({ + id: text.id, + containerId: container.id, + isDeleted: true, + }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: container.id, + boundElements: [{ id: text.id, type: "text" }], + isDeleted: true, + }), + expect.objectContaining({ + id: text.id, + // unbound! + containerId: null, + isDeleted: false, + }), + ]); + }); + }); + + it("should redraw remotely added bound text when it's container is updated through the history", async () => { + // Initialize the scene + API.updateScene({ + elements: [container], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + // Simulate local update + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { + x: 200, + y: 200, + angle: 90 as Radians, + }), + ], + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); + + Keyboard.undo(); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { + boundElements: [{ id: text.id, type: "text" }], + }), + newElementWith(text, { containerId: container.id }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + expect(h.elements).toEqual([ + expect.objectContaining({ + ...containerProps, + id: container.id, + boundElements: [{ id: text.id, type: "text" }], + isDeleted: false, + }), + expect.objectContaining({ + ...textProps, + id: text.id, + containerId: container.id, + isDeleted: false, + }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + ...containerProps, + x: 200, + y: 200, + angle: 90, + id: container.id, + boundElements: [{ id: text.id, type: "text" }], + isDeleted: false, + }), + expect.objectContaining({ + ...textProps, + // text element got redrawn! + x: 205, + y: 205, + angle: 90, + id: text.id, + containerId: container.id, + isDeleted: false, + }), + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + // both elements got redrawn! + expect(h.elements).toEqual([ + expect.objectContaining({ + ...containerProps, + id: container.id, + boundElements: [{ id: text.id, type: "text" }], + isDeleted: false, + }), + expect.objectContaining({ + ...textProps, + id: text.id, + containerId: container.id, + isDeleted: false, + }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + // both elements got redrawn! + expect(h.elements).toEqual([ + expect.objectContaining({ + ...containerProps, + x: 200, + y: 200, + angle: 90, + id: container.id, + boundElements: [{ id: text.id, type: "text" }], + isDeleted: false, + }), + expect.objectContaining({ + ...textProps, + x: 205, + y: 205, + angle: 90, + id: text.id, + containerId: container.id, + isDeleted: false, + }), + ]); + }); + + // TODO: #7348 this leads to empty undo/redo and could be confusing - instead we might consider redrawing container based on the text dimensions + it("should redraw bound text to match container dimensions when the bound text is updated through the history", async () => { + // Initialize the scene + API.updateScene({ + elements: [text], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + // Simulate local update + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { + x: 205, + y: 205, + angle: 90 as Radians, + }), + ], + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); + + Keyboard.undo(); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(container, { + boundElements: [{ id: text.id, type: "text" }], + }), + newElementWith(h.elements[0] as ExcalidrawTextElement, { + containerId: container.id, + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ + ...containerProps, + id: container.id, + boundElements: [{ id: text.id, type: "text" }], + isDeleted: false, + }), + expect.objectContaining({ + ...textProps, + id: text.id, + containerId: container.id, + isDeleted: false, + }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + ...containerProps, + id: container.id, + boundElements: [{ id: text.id, type: "text" }], + isDeleted: false, + }), + expect.objectContaining({ + ...textProps, + // bound text got redrawn, as redraw is triggered based on container positon! + id: text.id, + containerId: container.id, + isDeleted: false, + }), + ]); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + // both elements got redrawn! + expect(h.elements).toEqual([ + expect.objectContaining({ + ...containerProps, + id: container.id, + boundElements: [{ id: text.id, type: "text" }], + isDeleted: false, + }), + expect.objectContaining({ + ...textProps, + id: text.id, + containerId: container.id, + isDeleted: false, + }), + ]); + }); + }); + + describe("conflicts in arrows and their bindable elements", () => { + let rect1: ExcalidrawGenericElement; + let rect2: ExcalidrawGenericElement; + + const rect1Props = { + type: "rectangle", + height: 100, + width: 100, + x: -100, + y: -50, + } as const; + + const rect2Props = { + type: "rectangle", + height: 100, + width: 100, + x: 100, + y: -50, + } as const; + + function roundToNearestHundred(number: number) { + return Math.round(number / 100) * 100; + } + + beforeEach(() => { + rect1 = API.createElement({ ...rect1Props }); + rect2 = API.createElement({ ...rect2Props }); + + // Simulate local update + API.updateScene({ + elements: [rect1, rect2], + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); + + mouse.reset(); + }); + + it("should rebind bindings when both are updated through the history and there are no conflicting updates in the meantime", async () => { + // create arrow without bindings + Keyboard.withModifierKeys({ ctrl: true }, () => { + UI.clickTool("arrow"); + mouse.down(0, 0); + mouse.up(100, 0); + }); + + const arrowId = h.elements[2].id; + + // create start binding + mouse.downAt(0, 0); + mouse.moveTo(0, 1); + mouse.moveTo(0, 0); + mouse.up(); + + // create end binding + mouse.downAt(100, 0); + mouse.moveTo(100, 1); + mouse.moveTo(100, 0); + mouse.up(); + + expect(h.elements).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: rect1.id, + boundElements: [{ id: arrowId, type: "arrow" }], + }), + expect.objectContaining({ + id: rect2.id, + boundElements: [{ id: arrowId, type: "arrow" }], + }), + expect.objectContaining({ + id: arrowId, + startBinding: expect.objectContaining({ + elementId: rect1.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }), + endBinding: expect.objectContaining({ + elementId: rect2.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }), + }), + ]), + ); + + Keyboard.undo(); // undo start binding + Keyboard.undo(); // undo end binding + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(2); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: rect1.id, + boundElements: [], + }), + expect.objectContaining({ id: rect2.id, boundElements: [] }), + expect.objectContaining({ + id: arrowId, + startBinding: null, + endBinding: null, + }), + ]); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { + // no conflicting updates + x: h.elements[1].x + 50, + }), + newElementWith(h.elements[1], { + // no conflicting updates + x: h.elements[1].x + 50, + }), + newElementWith(h.elements[2], { + // no conflicting updates + x: h.elements[1].x + 50, + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + runTwice(() => { + Keyboard.redo(); + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(4); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: rect1.id, + boundElements: expect.arrayContaining([ + { id: arrowId, type: "arrow" }, + ]), + }), + expect.objectContaining({ + id: rect2.id, + boundElements: [{ id: arrowId, type: "arrow" }], + }), + expect.objectContaining({ + id: arrowId, + startBinding: expect.objectContaining({ + elementId: rect1.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }), + endBinding: expect.objectContaining({ + elementId: rect2.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }), + }), + ]), + ); + + Keyboard.undo(); + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(2); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: rect1.id, + boundElements: [], + }), + expect.objectContaining({ id: rect2.id, boundElements: [] }), + expect.objectContaining({ + id: arrowId, + startBinding: null, + endBinding: null, + }), + ]); + }); + }); + + it("should rebind bindings when both are updated through the history and the arrow got bound to a different element in the meantime", async () => { + // create arrow without bindings + Keyboard.withModifierKeys({ ctrl: true }, () => { + UI.clickTool("arrow"); + mouse.down(0, 0); + mouse.up(100, 0); + }); + + const arrowId = h.elements[2].id; + + // create start binding + mouse.downAt(0, 0); + mouse.moveTo(0, 1); + mouse.upAt(0, 0); + + // create end binding + mouse.downAt(100, 0); + mouse.moveTo(100, 1); + mouse.upAt(100, 0); + + expect(h.elements).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: rect1.id, + boundElements: [{ id: arrowId, type: "arrow" }], + }), + expect.objectContaining({ + id: rect2.id, + boundElements: [{ id: arrowId, type: "arrow" }], + }), + expect.objectContaining({ + id: arrowId, + startBinding: expect.objectContaining({ + elementId: rect1.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }), + endBinding: expect.objectContaining({ + elementId: rect2.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }), + }), + ]), + ); + + Keyboard.undo(); + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(2); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: rect1.id, + boundElements: [], + }), + expect.objectContaining({ id: rect2.id, boundElements: [] }), + expect.objectContaining({ + id: arrowId, + startBinding: null, + endBinding: null, + }), + ]); + + const remoteContainer = API.createElement({ + type: "rectangle", + width: 50, + x: 100, + boundElements: [{ id: arrowId, type: "arrow" }], + }); + + // Simulate remote update + API.updateScene({ + elements: [ + h.elements[0], + newElementWith(h.elements[1], { boundElements: [] }), + newElementWith(h.elements[2] as ExcalidrawElbowArrowElement, { + endBinding: { + elementId: remoteContainer.id, + gap: 1, + focus: 0, + fixedPoint: [0.5, 1], + }, + }), + remoteContainer, + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + runTwice(() => { + Keyboard.redo(); + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(4); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: rect1.id, + boundElements: [{ id: arrowId, type: "arrow" }], + }), + expect.objectContaining({ + id: rect2.id, + boundElements: [{ id: arrowId, type: "arrow" }], + }), + expect.objectContaining({ + id: arrowId, + startBinding: expect.objectContaining({ + elementId: rect1.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }), + // rebound with previous rectangle + endBinding: expect.objectContaining({ + elementId: rect2.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }), + }), + expect.objectContaining({ + id: remoteContainer.id, + boundElements: [], + }), + ]), + ); + + Keyboard.undo(); + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(2); + expect(h.elements).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: rect1.id, + boundElements: [], + }), + expect.objectContaining({ + id: rect2.id, + boundElements: [], + }), + expect.objectContaining({ + id: arrowId, + startBinding: null, + endBinding: expect.objectContaining({ + // now we are back in the previous state! + elementId: remoteContainer.id, + fixedPoint: [ + expect.toBeNonNaNNumber(), + expect.toBeNonNaNNumber(), + ], + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }), + }), + expect.objectContaining({ + id: remoteContainer.id, + // leaving as bound until we can rebind arrows! + boundElements: [{ id: arrowId, type: "arrow" }], + }), + ]), + ); + }); + }); + + it("should rebind remotely added arrow when it's bindable elements are added through the history", async () => { + const arrow = API.createElement({ + type: "arrow", + startBinding: { + elementId: rect1.id, + gap: 1, + focus: 0, + fixedPoint: [1, 0.5], + }, + endBinding: { + elementId: rect2.id, + gap: 1, + focus: 0, + fixedPoint: [0.5, 1], + }, + }); + + // Simulate remote update + API.updateScene({ + elements: [ + arrow, + newElementWith(h.elements[0], { + boundElements: [{ id: arrow.id, type: "arrow" }], + }), + newElementWith(h.elements[1], { + boundElements: [{ id: arrow.id, type: "arrow" }], + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + runTwice(() => { + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: arrow.id, + startBinding: null, + endBinding: null, + }), + expect.objectContaining({ + id: rect1.id, + boundElements: [{ id: arrow.id, type: "arrow" }], + isDeleted: true, + }), + expect.objectContaining({ + id: rect2.id, + boundElements: [{ id: arrow.id, type: "arrow" }], + isDeleted: true, + }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: arrow.id, + startBinding: expect.objectContaining({ + // now we are back in the previous state! + elementId: rect1.id, + fixedPoint: [ + expect.toBeNonNaNNumber(), + expect.toBeNonNaNNumber(), + ], + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }), + endBinding: expect.objectContaining({ + // now we are back in the previous state! + elementId: rect2.id, + fixedPoint: [ + expect.toBeNonNaNNumber(), + expect.toBeNonNaNNumber(), + ], + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }), + }), + expect.objectContaining({ + id: rect1.id, + boundElements: [{ id: arrow.id, type: "arrow" }], + isDeleted: false, + }), + expect.objectContaining({ + id: rect2.id, + boundElements: [{ id: arrow.id, type: "arrow" }], + isDeleted: false, + }), + ]), + ); + }); + }); + + it("should rebind remotely added bindable elements when it's arrow is added through the history", async () => { + Keyboard.undo(); + const arrow = API.createElement({ + type: "arrow", + }); + + // Simulate local update + API.updateScene({ + elements: [arrow], + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); + + // Simulate remote update + API.updateScene({ + elements: [ + newElementWith(h.elements[0] as ExcalidrawElbowArrowElement, { + startBinding: { + elementId: rect1.id, + gap: 1, + focus: 0, + fixedPoint: [0.5, 1], + }, + endBinding: { + elementId: rect2.id, + gap: 1, + focus: 0, + fixedPoint: [1, 0.5], + }, + }), + newElementWith(rect1, { + boundElements: [{ id: arrow.id, type: "arrow" }], + }), + newElementWith(rect2, { + boundElements: [{ id: arrow.id, type: "arrow" }], + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + runTwice(() => { + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: arrow.id, + startBinding: expect.objectContaining({ + elementId: rect1.id, + fixedPoint: [ + expect.toBeNonNaNNumber(), + expect.toBeNonNaNNumber(), + ], + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }), + endBinding: expect.objectContaining({ + elementId: rect2.id, + fixedPoint: [ + expect.toBeNonNaNNumber(), + expect.toBeNonNaNNumber(), + ], + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }), + isDeleted: true, + }), + expect.objectContaining({ + id: rect1.id, + boundElements: [], + isDeleted: false, + }), + expect.objectContaining({ + id: rect2.id, + boundElements: [], + isDeleted: false, + }), + ]), + ); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: arrow.id, + startBinding: { + elementId: rect1.id, + fixedPoint: [ + expect.toBeNonNaNNumber(), + expect.toBeNonNaNNumber(), + ], + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }, + endBinding: expect.objectContaining({ + elementId: rect2.id, + fixedPoint: [ + expect.toBeNonNaNNumber(), + expect.toBeNonNaNNumber(), + ], + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }), + isDeleted: false, + }), + expect.objectContaining({ + id: rect1.id, + boundElements: [{ id: arrow.id, type: "arrow" }], + isDeleted: false, + }), + expect.objectContaining({ + id: rect2.id, + boundElements: [{ id: arrow.id, type: "arrow" }], + isDeleted: false, + }), + ]), + ); + }); + }); + + it("should unbind remotely deleted bindable elements from arrow when the arrow is added through the history", async () => {}); + + it("should update bound element points when rectangle was remotely moved and arrow is added back through the history", async () => { + // bind arrow to rect1 and rect2 + UI.clickTool("arrow"); + mouse.down(0, 0); + mouse.up(100, 0); + + const arrowId = h.elements[2].id; + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: rect1.id, + boundElements: [], + }), + expect.objectContaining({ id: rect2.id, boundElements: [] }), + expect.objectContaining({ + id: arrowId, + startBinding: expect.objectContaining({ + elementId: rect1.id, + focus: 0, + gap: 1, + }), + endBinding: expect.objectContaining({ + elementId: rect2.id, + focus: -0, + gap: 1, + }), + isDeleted: true, + }), + ]), + ); + + // Simulate remote update + API.updateScene({ + elements: [ + h.elements[0], + newElementWith(h.elements[1], { x: 500, y: -500 }), + h.elements[2], + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(0); + { + // no need to be strict about points, hence the rounding + const points = (h.elements[2] as ExcalidrawLinearElement).points[1]; + expect([ + roundToNearestHundred(points[0]), + roundToNearestHundred(points[1]), + ]).toEqual([500, -400]); + } + expect(h.elements).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: rect1.id, + boundElements: [{ id: arrowId, type: "arrow" }], + }), + expect.objectContaining({ + id: rect2.id, + boundElements: [{ id: arrowId, type: "arrow" }], + }), + expect.objectContaining({ + id: arrowId, + startBinding: expect.objectContaining({ + elementId: rect1.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }), + endBinding: expect.objectContaining({ + elementId: rect2.id, + focus: expect.toBeNonNaNNumber(), + gap: expect.toBeNonNaNNumber(), + }), + isDeleted: false, + }), + ]), + ); + }); + }); + + describe("conflicts in frames and their children", () => { + let frame: ExcalidrawFrameElement; + let rect: ExcalidrawGenericElement; + + const frameProps = { + type: "frame", + x: 0, + width: 500, + } as const; + + const rectProps = { + type: "rectangle", + width: 100, + x: 10, + y: 10, + angle: 0, + } as const; + + beforeEach(() => { + frame = API.createElement({ ...frameProps }); + rect = API.createElement({ ...rectProps }); + }); + + it("should not rebind frame child with frame when frame was remotely deleted and frame child is added back through the history ", async () => { + // Initialize the scene + API.updateScene({ + elements: [frame], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + // Simulate local update + API.updateScene({ + elements: [rect, h.elements[0]], + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); + + // Simulate local update + API.updateScene({ + elements: [ + newElementWith(h.elements[0], { + frameId: frame.id, + }), + h.elements[1], + ], + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: rect.id, + frameId: null, + isDeleted: false, + }), + expect.objectContaining({ + id: frame.id, + isDeleted: false, + }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: rect.id, + frameId: frame.id, // double check that the element is rebound + isDeleted: false, + }), + expect.objectContaining({ + id: frame.id, + isDeleted: false, + }), + ]); + + Keyboard.undo(); + Keyboard.undo(); + + // Simulate remote update + API.updateScene({ + elements: [ + h.elements[0], + newElementWith(h.elements[1], { + isDeleted: true, + }), + ], + captureUpdate: CaptureUpdateAction.NEVER, + }); + + Keyboard.redo(); + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(2); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + id: rect.id, + frameId: null, // element is not unbound from + isDeleted: false, + }), + expect.objectContaining({ + id: frame.id, + isDeleted: true, + }), + ]); + }); + }); + }); +}); diff --git a/packages/excalidraw/tests/library.test.tsx b/packages/excalidraw/tests/library.test.tsx new file mode 100644 index 0000000..7b48407 --- /dev/null +++ b/packages/excalidraw/tests/library.test.tsx @@ -0,0 +1,341 @@ +import React from "react"; +import { vi } from "vitest"; +import { fireEvent, getCloneByOrigId, render, waitFor } from "./test-utils"; +import { act, queryByTestId } from "@testing-library/react"; + +import { Excalidraw } from "../index"; +import { API } from "./helpers/api"; +import { MIME_TYPES, ORIG_ID } from "../constants"; +import type { LibraryItem, LibraryItems } from "../types"; +import { UI } from "./helpers/ui"; +import { serializeLibraryAsJSON } from "../data/json"; +import { distributeLibraryItemsOnSquareGrid } from "../data/library"; +import type { ExcalidrawGenericElement } from "../element/types"; +import { getCommonBoundingBox } from "../element/bounds"; +import { parseLibraryJSON } from "../data/blob"; + +const { h } = window; + +const libraryJSONPromise = API.readFile( + "./fixtures/fixture_library.excalidrawlib", + "utf8", +); + +const mockLibraryFilePromise = new Promise<Blob>(async (resolve, reject) => { + try { + resolve( + new Blob([await libraryJSONPromise], { type: MIME_TYPES.excalidrawlib }), + ); + } catch (error) { + reject(error); + } +}); + +vi.mock("../data/filesystem.ts", async (importOriginal) => { + const module = await importOriginal(); + return { + __esmodule: true, + //@ts-ignore + ...module, + fileOpen: vi.fn(() => mockLibraryFilePromise), + }; +}); + +describe("library", () => { + beforeEach(async () => { + await render(<Excalidraw />); + await act(() => { + return h.app.library.resetLibrary(); + }); + }); + + it("import library via drag&drop", async () => { + expect(await h.app.library.getLatestLibrary()).toEqual([]); + await API.drop( + await API.loadFile("./fixtures/fixture_library.excalidrawlib"), + ); + await waitFor(async () => { + expect(await h.app.library.getLatestLibrary()).toEqual([ + { + status: "unpublished", + elements: [expect.objectContaining({ id: "A" })], + id: "id0", + created: expect.any(Number), + }, + ]); + }); + }); + + // NOTE: mocked to test logic, not actual drag&drop via UI + it("drop library item onto canvas", async () => { + expect(h.elements).toEqual([]); + const libraryItems = parseLibraryJSON(await libraryJSONPromise); + await API.drop( + new Blob([serializeLibraryAsJSON(libraryItems)], { + type: MIME_TYPES.excalidrawlib, + }), + ); + await waitFor(() => { + expect(h.elements).toEqual([expect.objectContaining({ [ORIG_ID]: "A" })]); + }); + }); + + it("should regenerate ids but retain bindings on library insert", async () => { + const rectangle = API.createElement({ + id: "rectangle1", + type: "rectangle", + boundElements: [ + { type: "text", id: "text1" }, + { type: "arrow", id: "arrow1" }, + ], + }); + const text = API.createElement({ + id: "text1", + type: "text", + text: "ola", + containerId: "rectangle1", + }); + const arrow = API.createElement({ + id: "arrow1", + type: "arrow", + endBinding: { + elementId: "rectangle1", + focus: -1, + gap: 0, + fixedPoint: [0.5, 1], + }, + }); + + await API.drop( + new Blob( + [ + serializeLibraryAsJSON([ + { + id: "item1", + status: "published", + elements: [rectangle, text, arrow], + created: 1, + }, + ]), + ], + { + type: MIME_TYPES.excalidrawlib, + }, + ), + ); + + await waitFor(() => { + expect(h.elements).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + [ORIG_ID]: "rectangle1", + boundElements: expect.arrayContaining([ + { type: "text", id: getCloneByOrigId("text1").id }, + { type: "arrow", id: getCloneByOrigId("arrow1").id }, + ]), + }), + expect.objectContaining({ + [ORIG_ID]: "text1", + containerId: getCloneByOrigId("rectangle1").id, + }), + expect.objectContaining({ + [ORIG_ID]: "arrow1", + endBinding: expect.objectContaining({ + elementId: getCloneByOrigId("rectangle1").id, + }), + }), + ]), + ); + }); + }); + + it("should fix duplicate ids between items on insert", async () => { + // note, we're not testing for duplicate group ids and such because + // deduplication of that happens upstream in the library component + // which would be very hard to orchestrate in this test + + const elem1 = API.createElement({ + id: "elem1", + type: "rectangle", + }); + const item1: LibraryItem = { + id: "item1", + status: "published", + elements: [elem1], + created: 1, + }; + + await API.drop( + new Blob([serializeLibraryAsJSON([item1, item1])], { + type: MIME_TYPES.excalidrawlib, + }), + ); + + await waitFor(() => { + expect(h.elements).toEqual([ + expect.objectContaining({ + [ORIG_ID]: "elem1", + }), + expect.objectContaining({ + id: expect.not.stringMatching(/^elem1$/), + [ORIG_ID]: expect.not.stringMatching(/^\w+$/), + }), + ]); + }); + }); + + it("inserting library item should revert to selection tool", async () => { + UI.clickTool("rectangle"); + expect(h.elements).toEqual([]); + const libraryItems = parseLibraryJSON(await libraryJSONPromise); + await API.drop( + new Blob([serializeLibraryAsJSON(libraryItems)], { + type: MIME_TYPES.excalidrawlib, + }), + ); + await waitFor(() => { + expect(h.elements).toEqual([expect.objectContaining({ [ORIG_ID]: "A" })]); + }); + expect(h.state.activeTool.type).toBe("selection"); + }); +}); + +describe("library menu", () => { + it("should load library from file picker", async () => { + const { container } = await render(<Excalidraw />); + + const latestLibrary = await h.app.library.getLatestLibrary(); + expect(latestLibrary.length).toBe(0); + + const libraryButton = container.querySelector(".sidebar-trigger"); + + fireEvent.click(libraryButton!); + fireEvent.click( + queryByTestId( + container.querySelector(".layer-ui__library")!, + "dropdown-menu-button", + )!, + ); + fireEvent.click(queryByTestId(container, "lib-dropdown--load")!); + + const libraryItems = parseLibraryJSON(await libraryJSONPromise); + + await waitFor(async () => { + const latestLibrary = await h.app.library.getLatestLibrary(); + expect(latestLibrary.length).toBeGreaterThan(0); + expect(latestLibrary.length).toBe(libraryItems.length); + const { versionNonce, ...strippedElement } = libraryItems[0]?.elements[0]; // stripped due to mutations + expect(latestLibrary[0].elements).toEqual([ + expect.objectContaining(strippedElement), + ]); + }); + }); +}); + +describe("distributeLibraryItemsOnSquareGrid()", () => { + it("should distribute items on a grid", async () => { + const createLibraryItem = ( + elements: ExcalidrawGenericElement[], + ): LibraryItem => { + return { + id: `id-${Date.now()}`, + elements, + status: "unpublished", + created: Date.now(), + }; + }; + + const PADDING = 50; + + const el1 = API.createElement({ + id: "id1", + width: 100, + height: 100, + x: 0, + y: 0, + }); + + const el2 = API.createElement({ + id: "id2", + width: 100, + height: 80, + x: -100, + y: -50, + }); + + const el3 = API.createElement({ + id: "id3", + width: 40, + height: 50, + x: -100, + y: -50, + }); + + const el4 = API.createElement({ + id: "id4", + width: 50, + height: 50, + x: 0, + y: 0, + }); + + const el5 = API.createElement({ + id: "id5", + width: 70, + height: 100, + x: 40, + y: 0, + }); + + const libraryItems: LibraryItems = [ + createLibraryItem([el1]), + createLibraryItem([el2]), + createLibraryItem([el3]), + createLibraryItem([el4, el5]), + ]; + + const distributed = distributeLibraryItemsOnSquareGrid(libraryItems); + // assert the returned library items are flattened to elements + expect(distributed.length).toEqual( + libraryItems.map((x) => x.elements).flat().length, + ); + expect(distributed).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: el1.id, + x: 0, + y: 0, + }), + expect.objectContaining({ + id: el2.id, + x: + el1.width + + PADDING + + (getCommonBoundingBox([el4, el5]).width - el2.width) / 2, + y: Math.abs(el1.height - el2.height) / 2, + }), + expect.objectContaining({ + id: el3.id, + x: Math.abs(el1.width - el3.width) / 2, + y: + Math.max(el1.height, el2.height) + + PADDING + + Math.abs(el3.height - Math.max(el4.height, el5.height)) / 2, + }), + expect.objectContaining({ + id: el4.id, + x: Math.max(el1.width, el2.width) + PADDING, + y: Math.max(el1.height, el2.height) + PADDING, + }), + expect.objectContaining({ + id: el5.id, + x: Math.max(el1.width, el2.width) + PADDING + Math.abs(el5.x - el4.x), + y: + Math.max(el1.height, el2.height) + + PADDING + + Math.abs(el5.y - el4.y), + }), + ]), + ); + }); +}); diff --git a/packages/excalidraw/tests/linearElementEditor.test.tsx b/packages/excalidraw/tests/linearElementEditor.test.tsx new file mode 100644 index 0000000..98d6058 --- /dev/null +++ b/packages/excalidraw/tests/linearElementEditor.test.tsx @@ -0,0 +1,1380 @@ +import React from "react"; +import type { + ExcalidrawElement, + ExcalidrawLinearElement, + ExcalidrawTextElementWithContainer, + FontString, +} from "../element/types"; +import { Excalidraw, mutateElement } from "../index"; +import { reseed } from "../random"; +import * as StaticScene from "../renderer/staticScene"; +import * as InteractiveCanvas from "../renderer/interactiveScene"; + +import { Keyboard, Pointer, UI } from "./helpers/ui"; +import { + screen, + render, + fireEvent, + GlobalTestState, + unmountComponent, +} from "./test-utils"; +import { API } from "../tests/helpers/api"; +import { KEYS } from "../keys"; +import { LinearElementEditor } from "../element/linearElementEditor"; +import { act, queryByTestId, queryByText } from "@testing-library/react"; +import { + getBoundTextElementPosition, + getBoundTextMaxWidth, +} from "../element/textElement"; +import * as textElementUtils from "../element/textElement"; +import { ROUNDNESS, VERTICAL_ALIGN } from "../constants"; +import { vi } from "vitest"; +import { arrayToMap } from "../utils"; +import type { GlobalPoint } from "@excalidraw/math"; +import { pointCenter, pointFrom } from "@excalidraw/math"; +import { wrapText } from "../element/textWrapping"; + +const renderInteractiveScene = vi.spyOn( + InteractiveCanvas, + "renderInteractiveScene", +); +const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene"); + +const { h } = window; +const font = "20px Cascadia, width: Segoe UI Emoji" as FontString; + +describe("Test Linear Elements", () => { + let container: HTMLElement; + let interactiveCanvas: HTMLCanvasElement; + + beforeEach(async () => { + unmountComponent(); + localStorage.clear(); + renderInteractiveScene.mockClear(); + renderStaticScene.mockClear(); + reseed(7); + const comp = await render(<Excalidraw handleKeyboardGlobally={true} />); + h.state.width = 1000; + h.state.height = 1000; + container = comp.container; + interactiveCanvas = container.querySelector("canvas.interactive")!; + }); + + const p1 = pointFrom<GlobalPoint>(20, 20); + const p2 = pointFrom<GlobalPoint>(60, 20); + const midpoint = pointCenter<GlobalPoint>(p1, p2); + const delta = 50; + const mouse = new Pointer("mouse"); + + const createTwoPointerLinearElement = ( + type: ExcalidrawLinearElement["type"], + roundness: ExcalidrawElement["roundness"] = null, + roughness: ExcalidrawLinearElement["roughness"] = 0, + ) => { + const line = API.createElement({ + x: p1[0], + y: p1[1], + width: p2[0] - p1[0], + height: 0, + type, + roughness, + points: [pointFrom(0, 0), pointFrom(p2[0] - p1[0], p2[1] - p1[1])], + roundness, + }); + API.setElements([line]); + + mouse.clickAt(p1[0], p1[1]); + return line; + }; + + const createThreePointerLinearElement = ( + type: ExcalidrawLinearElement["type"], + roundness: ExcalidrawElement["roundness"] = null, + roughness: ExcalidrawLinearElement["roughness"] = 0, + ) => { + //dragging line from midpoint + const p3 = [midpoint[0] + delta - p1[0], midpoint[1] + delta - p1[1]]; + const line = API.createElement({ + x: p1[0], + y: p1[1], + width: p3[0] - p1[0], + height: 0, + type, + roughness, + points: [ + pointFrom(0, 0), + pointFrom(p3[0], p3[1]), + pointFrom(p2[0] - p1[0], p2[1] - p1[1]), + ], + roundness, + }); + mutateElement(line, { points: line.points }); + API.setElements([line]); + mouse.clickAt(p1[0], p1[1]); + return line; + }; + + const enterLineEditingMode = ( + line: ExcalidrawLinearElement, + selectProgrammatically = false, + ) => { + if (selectProgrammatically) { + API.setSelectedElements([line]); + } else { + mouse.clickAt(p1[0], p1[1]); + } + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.ENTER); + }); + expect(h.state.editingLinearElement?.elementId).toEqual(line.id); + }; + + const drag = (startPoint: GlobalPoint, endPoint: GlobalPoint) => { + fireEvent.pointerDown(interactiveCanvas, { + clientX: startPoint[0], + clientY: startPoint[1], + }); + fireEvent.pointerMove(interactiveCanvas, { + clientX: endPoint[0], + clientY: endPoint[1], + }); + fireEvent.pointerUp(interactiveCanvas, { + clientX: endPoint[0], + clientY: endPoint[1], + }); + }; + + const deletePoint = (point: GlobalPoint) => { + fireEvent.pointerDown(interactiveCanvas, { + clientX: point[0], + clientY: point[1], + }); + fireEvent.pointerUp(interactiveCanvas, { + clientX: point[0], + clientY: point[1], + }); + Keyboard.keyPress(KEYS.DELETE); + }; + + it("should not drag line and add midpoint until dragged beyond a threshold", () => { + createTwoPointerLinearElement("line"); + const line = h.elements[0] as ExcalidrawLinearElement; + const originalX = line.x; + const originalY = line.y; + expect(line.points.length).toEqual(2); + + mouse.clickAt(midpoint[0], midpoint[1]); + drag(midpoint, pointFrom(midpoint[0] + 1, midpoint[1] + 1)); + + expect(line.points.length).toEqual(2); + + expect(line.x).toBe(originalX); + expect(line.y).toBe(originalY); + expect(line.points.length).toEqual(2); + + drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta)); + expect(line.x).toBe(originalX); + expect(line.y).toBe(originalY); + expect(line.points.length).toEqual(3); + }); + + it("should allow dragging line from midpoint in 2 pointer lines outside editor", async () => { + createTwoPointerLinearElement("line"); + const line = h.elements[0] as ExcalidrawLinearElement; + + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`5`); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`); + expect((h.elements[0] as ExcalidrawLinearElement).points.length).toEqual(2); + + // drag line from midpoint + drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta)); + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`9`); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`); + expect(line.points.length).toEqual(3); + expect(line.points).toMatchInlineSnapshot(` + [ + [ + 0, + 0, + ], + [ + 70, + 50, + ], + [ + 40, + 0, + ], + ] + `); + }); + + it("should allow entering and exiting line editor via context menu", () => { + createTwoPointerLinearElement("line"); + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: midpoint[0], + clientY: midpoint[1], + }); + // Enter line editor + const contextMenu = document.querySelector(".context-menu"); + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + 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); + }); + + it("should enter line editor when using double clicked with ctrl key", () => { + createTwoPointerLinearElement("line"); + expect(h.state.editingLinearElement?.elementId).toBeUndefined(); + + Keyboard.withModifierKeys({ ctrl: true }, () => { + mouse.doubleClick(); + }); + expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id); + }); + + describe("Inside editor", () => { + it("should not drag line and add midpoint when dragged irrespective of threshold", () => { + createTwoPointerLinearElement("line"); + const line = h.elements[0] as ExcalidrawLinearElement; + const originalX = line.x; + const originalY = line.y; + enterLineEditingMode(line); + + expect(line.points.length).toEqual(2); + + mouse.clickAt(midpoint[0], midpoint[1]); + expect(line.points.length).toEqual(2); + + drag(midpoint, pointFrom(midpoint[0] + 1, midpoint[1] + 1)); + expect(line.x).toBe(originalX); + expect(line.y).toBe(originalY); + expect(line.points.length).toEqual(3); + }); + + 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, pointFrom(midpoint[0] + delta, midpoint[1] + delta)); + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( + `12`, + ); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`); + + expect(line.points.length).toEqual(3); + expect(line.points).toMatchInlineSnapshot(` + [ + [ + 0, + 0, + ], + [ + 70, + 50, + ], + [ + 40, + 0, + ], + ] + `); + }); + + it("should update the midpoints when element roundness 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.app.scene.getNonDeletedElementsMap(), + h.state, + ); + + // update roundness + fireEvent.click(screen.getByTitle("Round")); + + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( + `9`, + ); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`); + + const midPointsWithRoundEdge = LinearElementEditor.getEditorMidPoints( + h.elements[0] as ExcalidrawLinearElement, + h.app.scene.getNonDeletedElementsMap(), + h.state, + ); + expect(midPointsWithRoundEdge[0]).not.toEqual(midPointsWithSharpEdge[0]); + expect(midPointsWithRoundEdge[1]).not.toEqual(midPointsWithSharpEdge[1]); + + expect(midPointsWithRoundEdge).toMatchInlineSnapshot(` + [ + [ + "55.96978", + "47.44233", + ], + [ + "76.08587", + "43.29417", + ], + ] + `); + }); + + it("should update all the midpoints when element position changed", async () => { + const elementsMap = arrayToMap(h.elements); + + createThreePointerLinearElement("line", { + type: ROUNDNESS.PROPORTIONAL_RADIUS, + }); + + const line = h.elements[0] as ExcalidrawLinearElement; + expect(line.points.length).toEqual(3); + enterLineEditingMode(line); + + const points = LinearElementEditor.getPointsGlobalCoordinates( + line, + elementsMap, + ); + expect([line.x, line.y]).toEqual(points[0]); + + const midPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + h.state, + ); + + const startPoint = pointCenter(points[0], midPoints[0]!); + const deltaX = 50; + const deltaY = 20; + const endPoint = pointFrom<GlobalPoint>( + startPoint[0] + deltaX, + startPoint[1] + deltaY, + ); + + // Move the element + drag(startPoint, endPoint); + + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( + `12`, + ); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`); + + expect([line.x, line.y]).toEqual([ + points[0][0] + deltaX, + points[0][1] + deltaY, + ]); + + const newMidPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + h.state, + ); + expect(midPoints[0]).not.toEqual(newMidPoints[0]); + expect(midPoints[1]).not.toEqual(newMidPoints[1]); + expect(newMidPoints).toMatchInlineSnapshot(` + [ + [ + "105.96978", + "67.44233", + ], + [ + "126.08587", + "63.29417", + ], + ] + `); + }); + + 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 = pointFrom<GlobalPoint>(55, 45); + const lastSegmentMidpoint = pointFrom<GlobalPoint>(75, 40); + + let line: ExcalidrawLinearElement; + + beforeEach(() => { + line = createThreePointerLinearElement("line"); + + 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, + pointFrom( + firstSegmentMidpoint[0] + delta, + firstSegmentMidpoint[1] + delta, + ), + ); + expect(line.points.length).toEqual(4); + + // drag line from last segment midpoint + drag( + lastSegmentMidpoint, + pointFrom( + lastSegmentMidpoint[0] + delta, + lastSegmentMidpoint[1] + delta, + ), + ); + + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( + `16`, + ); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`); + + expect(line.points.length).toEqual(5); + + expect((h.elements[0] as ExcalidrawLinearElement).points) + .toMatchInlineSnapshot(` + [ + [ + 0, + 0, + ], + [ + 85, + 75, + ], + [ + 70, + 50, + ], + [ + 105, + 70, + ], + [ + 40, + 0, + ], + ] + `); + }); + + it("should update only the first segment midpoint when its point is dragged", async () => { + const elementsMap = arrayToMap(h.elements); + const points = LinearElementEditor.getPointsGlobalCoordinates( + line, + elementsMap, + ); + const midPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + h.state, + ); + + const hitCoords = pointFrom<GlobalPoint>(points[0][0], points[0][1]); + + // Drag from first point + drag(hitCoords, pointFrom(hitCoords[0] - delta, hitCoords[1] - delta)); + + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( + `12`, + ); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`); + + const newPoints = LinearElementEditor.getPointsGlobalCoordinates( + line, + elementsMap, + ); + expect([newPoints[0][0], newPoints[0][1]]).toEqual([ + points[0][0] - delta, + points[0][1] - delta, + ]); + + const newMidPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + 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 elementsMap = arrayToMap(h.elements); + const points = LinearElementEditor.getPointsGlobalCoordinates( + line, + elementsMap, + ); + const midPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + h.state, + ); + + const hitCoords = pointFrom<GlobalPoint>(points[0][0], points[0][1]); + + // Drag from first point + drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta)); + + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( + `12`, + ); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`); + + const newPoints = LinearElementEditor.getPointsGlobalCoordinates( + line, + elementsMap, + ); + expect([newPoints[0][0], newPoints[0][1]]).toEqual([ + points[0][0] + delta, + points[0][1] + delta, + ]); + + const newMidPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + 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, + arrayToMap(h.elements), + ); + + // dragging line from last segment midpoint + drag( + lastSegmentMidpoint, + pointFrom(lastSegmentMidpoint[0] + 50, lastSegmentMidpoint[1] + 50), + ); + expect(line.points.length).toEqual(4); + + const midPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + h.state, + ); + + // delete 3rd point + deletePoint(points[2]); + expect(line.points.length).toEqual(3); + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( + `18`, + ); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`); + + const newMidPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + 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 = pointFrom<GlobalPoint>( + 55.9697848965255, + 47.442326230998205, + ); + const lastSegmentMidpoint = pointFrom<GlobalPoint>( + 76.08587175006699, + 43.294165939653226, + ); + let line: ExcalidrawLinearElement; + + beforeEach(() => { + line = createThreePointerLinearElement("line", { + type: ROUNDNESS.PROPORTIONAL_RADIUS, + }); + 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, + pointFrom( + firstSegmentMidpoint[0] + delta, + firstSegmentMidpoint[1] + delta, + ), + ); + expect(line.points.length).toEqual(4); + + // drag line from last segment midpoint + drag( + lastSegmentMidpoint, + pointFrom( + lastSegmentMidpoint[0] + delta, + lastSegmentMidpoint[1] + delta, + ), + ); + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( + `16`, + ); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`); + expect(line.points.length).toEqual(5); + + expect((h.elements[0] as ExcalidrawLinearElement).points) + .toMatchInlineSnapshot(` + [ + [ + 0, + 0, + ], + [ + "85.96978", + "77.44233", + ], + [ + 70, + 50, + ], + [ + "106.08587", + "73.29417", + ], + [ + 40, + 0, + ], + ] + `); + }); + + it("should update all the midpoints when its point is dragged", async () => { + const elementsMap = arrayToMap(h.elements); + const points = LinearElementEditor.getPointsGlobalCoordinates( + line, + elementsMap, + ); + const midPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + h.state, + ); + + const hitCoords = pointFrom<GlobalPoint>(points[0][0], points[0][1]); + + // Drag from first point + drag(hitCoords, pointFrom(hitCoords[0] - delta, hitCoords[1] - delta)); + + const newPoints = LinearElementEditor.getPointsGlobalCoordinates( + line, + elementsMap, + ); + expect([newPoints[0][0], newPoints[0][1]]).toEqual([ + points[0][0] - delta, + points[0][1] - delta, + ]); + + const newMidPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + h.state, + ); + + expect(midPoints[0]).not.toEqual(newMidPoints[0]); + expect(midPoints[1]).not.toEqual(newMidPoints[1]); + expect(newMidPoints).toMatchInlineSnapshot(` + [ + [ + "31.88408", + "23.13276", + ], + [ + "77.74793", + "44.57841", + ], + ] + `); + }); + + it("should hide midpoints in the segment when points moved close", async () => { + const elementsMap = arrayToMap(h.elements); + const points = LinearElementEditor.getPointsGlobalCoordinates( + line, + elementsMap, + ); + const midPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + h.state, + ); + + const hitCoords = pointFrom<GlobalPoint>(points[0][0], points[0][1]); + + // Drag from first point + drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta)); + + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( + `12`, + ); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`); + + const newPoints = LinearElementEditor.getPointsGlobalCoordinates( + line, + elementsMap, + ); + expect([newPoints[0][0], newPoints[0][1]]).toEqual([ + points[0][0] + delta, + points[0][1] + delta, + ]); + + const newMidPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + 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 () => { + const elementsMap = arrayToMap(h.elements); + + drag( + lastSegmentMidpoint, + pointFrom( + lastSegmentMidpoint[0] + delta, + lastSegmentMidpoint[1] + delta, + ), + ); + expect(line.points.length).toEqual(4); + + const midPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + h.state, + ); + const points = LinearElementEditor.getPointsGlobalCoordinates( + line, + elementsMap, + ); + + // delete 3rd point + deletePoint(points[2]); + expect(line.points.length).toEqual(3); + + const newMidPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + h.state, + ); + expect(newMidPoints.length).toEqual(2); + expect(midPoints[0]).not.toEqual(newMidPoints[0]); + expect(midPoints[1]).not.toEqual(newMidPoints[1]); + expect(newMidPoints).toMatchInlineSnapshot(` + [ + [ + "55.96978", + "47.44233", + ], + [ + "76.08587", + "43.29417", + ], + ] + `); + }); + }); + + it("in-editor dragging a line point covered by another element", () => { + createTwoPointerLinearElement("line"); + const line = h.elements[0] as ExcalidrawLinearElement; + API.setElements([ + line, + API.createElement({ + type: "rectangle", + x: line.x - 50, + y: line.y - 50, + width: 100, + height: 100, + backgroundColor: "red", + fillStyle: "solid", + }), + ]); + const dragEndPositionOffset = [100, 100] as const; + API.setSelectedElements([line]); + enterLineEditingMode(line, true); + drag( + pointFrom(line.points[0][0] + line.x, line.points[0][1] + line.y), + pointFrom( + dragEndPositionOffset[0] + line.x, + dragEndPositionOffset[1] + line.y, + ), + ); + expect(line.points).toMatchInlineSnapshot(` + [ + [ + 0, + 0, + ], + [ + -60, + -100, + ], + ] + `); + }); + }); + + describe("Test bound text element", () => { + const DEFAULT_TEXT = "Online whiteboard collaboration made easy"; + + const createBoundTextElement = ( + text: string, + container: ExcalidrawLinearElement, + ) => { + const textElement = API.createElement({ + type: "text", + x: 0, + y: 0, + text: wrapText(text, font, getBoundTextMaxWidth(container, null)), + containerId: container.id, + width: 30, + height: 20, + }) as ExcalidrawTextElementWithContainer; + + container = { + ...container, + boundElements: (container.boundElements || []).concat({ + type: "text", + id: textElement.id, + }), + }; + + const elements: ExcalidrawElement[] = []; + h.elements.forEach((element) => { + if (element.id === container.id) { + elements.push(container); + } else { + elements.push(element); + } + }); + const updatedTextElement = { ...textElement, originalText: text }; + API.setElements([...elements, updatedTextElement]); + return { textElement: updatedTextElement, container }; + }; + + describe("Test getBoundTextElementPosition", () => { + it("should return correct position for 2 pointer arrow", () => { + createTwoPointerLinearElement("arrow"); + const arrow = h.elements[0] as ExcalidrawLinearElement; + const { textElement, container } = createBoundTextElement( + DEFAULT_TEXT, + arrow, + ); + const position = LinearElementEditor.getBoundTextElementPosition( + container, + textElement, + arrayToMap(h.elements), + ); + expect(position).toMatchInlineSnapshot(` + { + "x": 25, + "y": 10, + } + `); + }); + + it("should return correct position for arrow with odd points", () => { + createThreePointerLinearElement("arrow", { + type: ROUNDNESS.PROPORTIONAL_RADIUS, + }); + const arrow = h.elements[0] as ExcalidrawLinearElement; + const { textElement, container } = createBoundTextElement( + DEFAULT_TEXT, + arrow, + ); + + const position = LinearElementEditor.getBoundTextElementPosition( + container, + textElement, + arrayToMap(h.elements), + ); + expect(position).toMatchInlineSnapshot(` + { + "x": 75, + "y": 60, + } + `); + }); + + it("should return correct position for arrow with even points", () => { + createThreePointerLinearElement("arrow", { + type: ROUNDNESS.PROPORTIONAL_RADIUS, + }); + const arrow = h.elements[0] as ExcalidrawLinearElement; + const { textElement, container } = createBoundTextElement( + DEFAULT_TEXT, + arrow, + ); + enterLineEditingMode(container); + // 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 = pointFrom<GlobalPoint>( + 55.9697848965255, + 47.442326230998205, + ); + // drag line from first segment midpoint + drag( + firstSegmentMidpoint, + pointFrom( + firstSegmentMidpoint[0] + delta, + firstSegmentMidpoint[1] + delta, + ), + ); + + const position = LinearElementEditor.getBoundTextElementPosition( + container, + textElement, + arrayToMap(h.elements), + ); + expect(position).toMatchInlineSnapshot(` + { + "x": "85.82202", + "y": "75.63461", + } + `); + }); + }); + + it("should match styles for text editor", () => { + createTwoPointerLinearElement("arrow"); + Keyboard.keyPress(KEYS.ENTER); + const editor = document.querySelector( + ".excalidraw-textEditorContainer > textarea", + ) as HTMLTextAreaElement; + expect(editor).toMatchSnapshot(); + }); + + it("should bind text to arrow when double clicked", async () => { + createTwoPointerLinearElement("arrow"); + const arrow = h.elements[0] as ExcalidrawLinearElement; + + expect(h.elements.length).toBe(1); + expect(h.elements[0].id).toBe(arrow.id); + mouse.doubleClickAt(arrow.x, arrow.y); + expect(h.elements.length).toBe(2); + + const text = h.elements[1] as ExcalidrawTextElementWithContainer; + expect(text.type).toBe("text"); + expect(text.containerId).toBe(arrow.id); + mouse.down(); + const editor = document.querySelector( + ".excalidraw-textEditorContainer > textarea", + ) as HTMLTextAreaElement; + + fireEvent.change(editor, { + target: { value: DEFAULT_TEXT }, + }); + + Keyboard.exitTextEditor(editor); + expect(arrow.boundElements).toStrictEqual([ + { id: text.id, type: "text" }, + ]); + expect( + (h.elements[1] as ExcalidrawTextElementWithContainer).text, + ).toMatchSnapshot(); + }); + + it("should bind text to arrow when clicked on arrow and enter pressed", async () => { + const arrow = createTwoPointerLinearElement("arrow"); + + expect(h.elements.length).toBe(1); + expect(h.elements[0].id).toBe(arrow.id); + + Keyboard.keyPress(KEYS.ENTER); + + expect(h.elements.length).toBe(2); + + const textElement = h.elements[1] as ExcalidrawTextElementWithContainer; + expect(textElement.type).toBe("text"); + expect(textElement.containerId).toBe(arrow.id); + const editor = document.querySelector( + ".excalidraw-textEditorContainer > textarea", + ) as HTMLTextAreaElement; + + fireEvent.change(editor, { + target: { value: DEFAULT_TEXT }, + }); + Keyboard.exitTextEditor(editor); + expect(arrow.boundElements).toStrictEqual([ + { id: textElement.id, type: "text" }, + ]); + expect( + (h.elements[1] as ExcalidrawTextElementWithContainer).text, + ).toMatchSnapshot(); + }); + + it("should not bind text to line when double clicked", async () => { + const line = createTwoPointerLinearElement("line"); + + expect(h.elements.length).toBe(1); + mouse.doubleClickAt(line.x, line.y); + + expect(h.elements.length).toBe(2); + + const text = h.elements[1] as ExcalidrawTextElementWithContainer; + expect(text.type).toBe("text"); + expect(text.containerId).toBeNull(); + expect(line.boundElements).toBeNull(); + }); + + // TODO fix #7029 and rewrite this test + it.todo( + "should not rotate the bound text and update position of bound text and bounding box correctly when arrow rotated", + ); + + it("should resize and position the bound text and bounding box correctly when 3 pointer arrow element resized", () => { + createThreePointerLinearElement("arrow", { + type: ROUNDNESS.PROPORTIONAL_RADIUS, + }); + + const arrow = h.elements[0] as ExcalidrawLinearElement; + + const { textElement, container } = createBoundTextElement( + DEFAULT_TEXT, + arrow, + ); + expect(container.width).toBe(70); + expect(container.height).toBe(50); + expect( + getBoundTextElementPosition( + container, + textElement, + arrayToMap(h.elements), + ), + ).toMatchInlineSnapshot(` + { + "x": 75, + "y": 60, + } + `); + expect(textElement.text).toMatchSnapshot(); + expect( + LinearElementEditor.getElementAbsoluteCoords( + container, + h.app.scene.getNonDeletedElementsMap(), + true, + ), + ).toMatchInlineSnapshot(` + [ + 20, + 20, + 105, + 80, + "55.45894", + 45, + ] + `); + + UI.resize(container, "ne", [300, 200]); + + expect({ width: container.width, height: container.height }) + .toMatchInlineSnapshot(` + { + "height": 130, + "width": "366.11716", + } + `); + + expect( + getBoundTextElementPosition( + container, + textElement, + arrayToMap(h.elements), + ), + ).toMatchInlineSnapshot(` + { + "x": "271.11716", + "y": 45, + } + `); + expect( + (h.elements[1] as ExcalidrawTextElementWithContainer).text, + ).toMatchSnapshot(); + expect( + LinearElementEditor.getElementAbsoluteCoords( + container, + h.app.scene.getNonDeletedElementsMap(), + true, + ), + ).toMatchInlineSnapshot(` + [ + 20, + 35, + "501.11716", + 95, + "205.45894", + "52.50000", + ] + `); + }); + + it("should resize and position the bound text correctly when 2 pointer linear element resized", () => { + createTwoPointerLinearElement("arrow"); + + const arrow = h.elements[0] as ExcalidrawLinearElement; + const { textElement, container } = createBoundTextElement( + DEFAULT_TEXT, + arrow, + ); + expect(container.width).toBe(40); + const elementsMap = arrayToMap(h.elements); + expect(getBoundTextElementPosition(container, textElement, elementsMap)) + .toMatchInlineSnapshot(` + { + "x": 25, + "y": 10, + } + `); + expect(textElement.text).toMatchSnapshot(); + const points = LinearElementEditor.getPointsGlobalCoordinates( + container, + elementsMap, + ); + + // Drag from last point + drag(points[1], pointFrom(points[1][0] + 300, points[1][1])); + + expect({ width: container.width, height: container.height }) + .toMatchInlineSnapshot(` + { + "height": 130, + "width": 340, + } + `); + + expect(getBoundTextElementPosition(container, textElement, elementsMap)) + .toMatchInlineSnapshot(` + { + "x": 75, + "y": -5, + } + `); + expect(textElement.text).toMatchSnapshot(); + }); + + it("should not render vertical align tool when element selected", () => { + createTwoPointerLinearElement("arrow"); + const arrow = h.elements[0] as ExcalidrawLinearElement; + + createBoundTextElement(DEFAULT_TEXT, arrow); + API.setSelectedElements([arrow]); + + expect(queryByTestId(container, "align-top")).toBeNull(); + expect(queryByTestId(container, "align-middle")).toBeNull(); + expect(queryByTestId(container, "align-bottom")).toBeNull(); + }); + + it("should wrap the bound text when arrow bound container moves", async () => { + const rect = UI.createElement("rectangle", { + x: 400, + width: 200, + height: 500, + }); + const arrow = UI.createElement("arrow", { + x: -10, + y: 250, + width: 400, + height: 1, + }); + + mouse.select(arrow); + Keyboard.keyPress(KEYS.ENTER); + const editor = document.querySelector( + ".excalidraw-textEditorContainer > textarea", + ) as HTMLTextAreaElement; + fireEvent.change(editor, { target: { value: DEFAULT_TEXT } }); + Keyboard.exitTextEditor(editor); + + const textElement = h.elements[2] as ExcalidrawTextElementWithContainer; + + expect(arrow.endBinding?.elementId).toBe(rect.id); + expect(arrow.width).toBe(400); + expect(rect.x).toBe(400); + expect(rect.y).toBe(0); + expect( + wrapText( + textElement.originalText, + font, + getBoundTextMaxWidth(arrow, null), + ), + ).toMatchSnapshot(); + const handleBindTextResizeSpy = vi.spyOn( + textElementUtils, + "handleBindTextResize", + ); + + mouse.select(rect); + mouse.downAt(rect.x, rect.y); + mouse.moveTo(200, 0); + mouse.upAt(200, 0); + expect(arrow.width).toBeCloseTo(204, 0); + expect(rect.x).toBe(200); + expect(rect.y).toBe(0); + expect(handleBindTextResizeSpy).toHaveBeenCalledWith( + h.elements[0], + arrayToMap(h.elements), + "nw", + false, + ); + expect( + wrapText( + textElement.originalText, + font, + getBoundTextMaxWidth(arrow, null), + ), + ).toMatchSnapshot(); + }); + + it("should not render horizontal align tool when element selected", () => { + createTwoPointerLinearElement("arrow"); + const arrow = h.elements[0] as ExcalidrawLinearElement; + + createBoundTextElement(DEFAULT_TEXT, arrow); + API.setSelectedElements([arrow]); + + expect(queryByTestId(container, "align-left")).toBeNull(); + expect(queryByTestId(container, "align-horizontal-center")).toBeNull(); + expect(queryByTestId(container, "align-right")).toBeNull(); + }); + + it("should update label coords when a label binded via context menu is unbinded", async () => { + createTwoPointerLinearElement("arrow"); + const text = API.createElement({ + type: "text", + text: "Hello Excalidraw", + }); + expect(text.x).toBe(0); + expect(text.y).toBe(0); + + API.setElements([h.elements[0], text]); + + const container = h.elements[0]; + API.setSelectedElements([container, text]); + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 20, + clientY: 30, + }); + let contextMenu = document.querySelector(".context-menu"); + + fireEvent.click( + queryByText(contextMenu as HTMLElement, "Bind text to the container")!, + ); + expect(container.boundElements).toStrictEqual([ + { id: h.elements[1].id, type: "text" }, + ]); + expect(text.containerId).toBe(container.id); + expect(text.verticalAlign).toBe(VERTICAL_ALIGN.MIDDLE); + + mouse.reset(); + mouse.clickAt( + container.x + container.width / 2, + container.y + container.height / 2, + ); + mouse.down(); + mouse.up(); + API.setSelectedElements([h.elements[0], h.elements[1]]); + + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 20, + clientY: 30, + }); + contextMenu = document.querySelector(".context-menu"); + fireEvent.click(queryByText(contextMenu as HTMLElement, "Unbind text")!); + expect(container.boundElements).toEqual([]); + expect(text).toEqual( + expect.objectContaining({ + containerId: null, + width: 160, + height: 25, + x: -40, + y: 7.5, + }), + ); + }); + + it("should not update label position when arrow dragged", () => { + createTwoPointerLinearElement("arrow"); + let arrow = h.elements[0] as ExcalidrawLinearElement; + createBoundTextElement(DEFAULT_TEXT, arrow); + let label = h.elements[1] as ExcalidrawTextElementWithContainer; + expect(arrow.x).toBe(20); + expect(arrow.y).toBe(20); + expect(label.x).toBe(0); + expect(label.y).toBe(0); + mouse.reset(); + mouse.select(arrow); + mouse.select(label); + mouse.downAt(arrow.x, arrow.y); + mouse.moveTo(arrow.x + 20, arrow.y + 30); + mouse.up(arrow.x + 20, arrow.y + 30); + + arrow = h.elements[0] as ExcalidrawLinearElement; + label = h.elements[1] as ExcalidrawTextElementWithContainer; + expect(arrow.x).toBe(80); + expect(arrow.y).toBe(100); + expect(label.x).toBe(0); + expect(label.y).toBe(0); + }); + }); + + describe("Test moving linear element points", () => { + it("should move the endpoint in the negative direction correctly when the start point is also moved in the positive direction", async () => { + const line = createThreePointerLinearElement("arrow"); + const [origStartX, origStartY] = [line.x, line.y]; + + act(() => { + LinearElementEditor.movePoints(line, [ + { + index: 0, + point: pointFrom(line.points[0][0] + 10, line.points[0][1] + 10), + }, + { + index: line.points.length - 1, + point: pointFrom( + line.points[line.points.length - 1][0] - 10, + line.points[line.points.length - 1][1] - 10, + ), + }, + ]); + }); + expect(line.x).toBe(origStartX + 10); + expect(line.y).toBe(origStartY + 10); + + expect(line.points[line.points.length - 1][0]).toBe(20); + expect(line.points[line.points.length - 1][1]).toBe(-20); + }); + }); +}); diff --git a/packages/excalidraw/tests/move.test.tsx b/packages/excalidraw/tests/move.test.tsx new file mode 100644 index 0000000..528f955 --- /dev/null +++ b/packages/excalidraw/tests/move.test.tsx @@ -0,0 +1,180 @@ +import React from "react"; +import "../../utils/test-utils"; +import { render, fireEvent, act, unmountComponent } from "./test-utils"; +import { Excalidraw } from "../index"; +import * as StaticScene from "../renderer/staticScene"; +import * as InteractiveCanvas from "../renderer/interactiveScene"; +import { reseed } from "../random"; +import { bindOrUnbindLinearElement } from "../element/binding"; +import type { + ExcalidrawLinearElement, + NonDeleted, + ExcalidrawRectangleElement, +} from "../element/types"; +import { UI, Pointer, Keyboard } from "./helpers/ui"; +import { KEYS } from "../keys"; +import { vi } from "vitest"; +import type Scene from "../scene/Scene"; + +unmountComponent(); + +const renderInteractiveScene = vi.spyOn( + InteractiveCanvas, + "renderInteractiveScene", +); +const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene"); + +beforeEach(() => { + localStorage.clear(); + renderInteractiveScene.mockClear(); + renderStaticScene.mockClear(); + reseed(7); +}); + +const { h } = window; + +describe("move element", () => { + it("rectangle", async () => { + const { getByToolName, container } = await render(<Excalidraw />); + const canvas = container.querySelector("canvas.interactive")!; + + { + // create element + const tool = getByToolName("rectangle"); + fireEvent.click(tool); + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); + fireEvent.pointerUp(canvas); + + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( + `5`, + ); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`); + expect(h.state.selectionElement).toBeNull(); + expect(h.elements.length).toEqual(1); + expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); + expect([h.elements[0].x, h.elements[0].y]).toEqual([30, 20]); + + renderInteractiveScene.mockClear(); + renderStaticScene.mockClear(); + } + + fireEvent.pointerDown(canvas, { clientX: 50, clientY: 20 }); + fireEvent.pointerMove(canvas, { clientX: 20, clientY: 40 }); + fireEvent.pointerUp(canvas); + + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`3`); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`2`); + expect(h.state.selectionElement).toBeNull(); + expect(h.elements.length).toEqual(1); + expect([h.elements[0].x, h.elements[0].y]).toEqual([0, 40]); + + h.elements.forEach((element) => expect(element).toMatchSnapshot()); + }); + + it("rectangles with binding arrow", async () => { + await render(<Excalidraw handleKeyboardGlobally={true} />); + + // create elements + const rectA = UI.createElement("rectangle", { size: 100 }); + const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 }); + const arrow = UI.createElement("arrow", { x: 110, y: 50, size: 80 }); + const elementsMap = h.app.scene.getNonDeletedElementsMap(); + act(() => { + // bind line to two rectangles + bindOrUnbindLinearElement( + arrow.get() as NonDeleted<ExcalidrawLinearElement>, + rectA.get() as ExcalidrawRectangleElement, + rectB.get() as ExcalidrawRectangleElement, + elementsMap, + {} as Scene, + ); + }); + + // select the second rectangle + new Pointer("mouse").clickOn(rectB); + + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( + `17`, + ); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`13`); + expect(h.state.selectionElement).toBeNull(); + expect(h.elements.length).toEqual(3); + expect(h.state.selectedElementIds[rectB.id]).toBeTruthy(); + expect([rectA.x, rectA.y]).toEqual([0, 0]); + expect([rectB.x, rectB.y]).toEqual([200, 0]); + expect([arrow.x, arrow.y]).toEqual([110, 50]); + expect([arrow.width, arrow.height]).toEqual([80, 80]); + + renderInteractiveScene.mockClear(); + renderStaticScene.mockClear(); + + // Move selected rectangle + Keyboard.keyDown(KEYS.ARROW_RIGHT); + Keyboard.keyDown(KEYS.ARROW_DOWN); + Keyboard.keyDown(KEYS.ARROW_DOWN); + + // Check that the arrow size has been changed according to moving the rectangle + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`3`); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`3`); + expect(h.state.selectionElement).toBeNull(); + expect(h.elements.length).toEqual(3); + expect(h.state.selectedElementIds[rectB.id]).toBeTruthy(); + expect([rectA.x, rectA.y]).toEqual([0, 0]); + expect([rectB.x, rectB.y]).toEqual([201, 2]); + expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[107.07, 47.07]]); + expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[86.86, 87.3]]); + + h.elements.forEach((element) => expect(element).toMatchSnapshot()); + }); +}); + +describe("duplicate element on move when ALT is clicked", () => { + it("rectangle", async () => { + const { getByToolName, container } = await render(<Excalidraw />); + const canvas = container.querySelector("canvas.interactive")!; + + { + // create element + const tool = getByToolName("rectangle"); + fireEvent.click(tool); + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); + fireEvent.pointerUp(canvas); + + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( + `5`, + ); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`); + expect(h.state.selectionElement).toBeNull(); + expect(h.elements.length).toEqual(1); + expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); + expect([h.elements[0].x, h.elements[0].y]).toEqual([30, 20]); + + renderInteractiveScene.mockClear(); + renderStaticScene.mockClear(); + } + + fireEvent.pointerDown(canvas, { clientX: 50, clientY: 20 }); + fireEvent.pointerMove(canvas, { clientX: 20, clientY: 40, altKey: true }); + + // firing another pointerMove event with alt key pressed should NOT trigger + // another duplication + fireEvent.pointerMove(canvas, { clientX: 20, clientY: 40, altKey: true }); + fireEvent.pointerMove(canvas, { clientX: 10, clientY: 60 }); + fireEvent.pointerUp(canvas); + + // TODO: This used to be 4, but binding made it go up to 5. Do we need + // that additional render? + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`4`); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`3`); + expect(h.state.selectionElement).toBeNull(); + expect(h.elements.length).toEqual(2); + + // previous element should stay intact + expect([h.elements[0].x, h.elements[0].y]).toEqual([30, 20]); + expect([h.elements[1].x, h.elements[1].y]).toEqual([-10, 60]); + + h.elements.forEach((element) => expect(element).toMatchSnapshot()); + }); +}); diff --git a/packages/excalidraw/tests/multiPointCreate.test.tsx b/packages/excalidraw/tests/multiPointCreate.test.tsx new file mode 100644 index 0000000..df94eb9 --- /dev/null +++ b/packages/excalidraw/tests/multiPointCreate.test.tsx @@ -0,0 +1,177 @@ +import React from "react"; +import { + render, + fireEvent, + mockBoundingClientRect, + restoreOriginalGetBoundingClientRect, + unmountComponent, +} from "./test-utils"; +import { Excalidraw } from "../index"; +import * as StaticScene from "../renderer/staticScene"; +import * as InteractiveCanvas from "../renderer/interactiveScene"; +import { KEYS } from "../keys"; +import type { ExcalidrawLinearElement } from "../element/types"; +import { reseed } from "../random"; +import { vi } from "vitest"; + +unmountComponent(); + +const renderInteractiveScene = vi.spyOn( + InteractiveCanvas, + "renderInteractiveScene", +); +const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene"); + +beforeEach(() => { + localStorage.clear(); + renderInteractiveScene.mockClear(); + renderStaticScene.mockClear(); + reseed(7); +}); + +const { h } = window; + +describe("remove shape in non linear elements", () => { + beforeAll(() => { + mockBoundingClientRect({ width: 1000, height: 1000 }); + }); + + afterAll(() => { + restoreOriginalGetBoundingClientRect(); + }); + + it("rectangle", async () => { + const { getByToolName, container } = await render(<Excalidraw />); + // select tool + const tool = getByToolName("rectangle"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas.interactive")!; + + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 }); + + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`5`); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`); + expect(h.elements.length).toEqual(0); + }); + + it("ellipse", async () => { + const { getByToolName, container } = await render(<Excalidraw />); + // select tool + const tool = getByToolName("ellipse"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas.interactive")!; + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 }); + + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`5`); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`); + expect(h.elements.length).toEqual(0); + }); + + it("diamond", async () => { + const { getByToolName, container } = await render(<Excalidraw />); + // select tool + const tool = getByToolName("diamond"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas.interactive")!; + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 }); + + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`5`); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`); + expect(h.elements.length).toEqual(0); + }); +}); + +describe("multi point mode in linear elements", () => { + it("arrow", async () => { + const { getByToolName, container } = await render(<Excalidraw />); + // select tool + const tool = getByToolName("arrow"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas.interactive")!; + // first point is added on pointer down + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 30 }); + + // second point, enable multi point + fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 }); + fireEvent.pointerMove(canvas, { clientX: 50, clientY: 60 }); + + // third point + fireEvent.pointerDown(canvas, { clientX: 50, clientY: 60 }); + fireEvent.pointerUp(canvas); + fireEvent.pointerMove(canvas, { clientX: 100, clientY: 140 }); + + // done + fireEvent.pointerDown(canvas); + fireEvent.pointerUp(canvas); + fireEvent.keyDown(document, { + key: KEYS.ENTER, + }); + + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`7`); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`); + expect(h.elements.length).toEqual(1); + + const element = h.elements[0] as ExcalidrawLinearElement; + + expect(element.type).toEqual("arrow"); + expect(element.x).toEqual(30); + expect(element.y).toEqual(30); + expect(element.points).toEqual([ + [0, 0], + [20, 30], + [70, 110], + ]); + + h.elements.forEach((element) => expect(element).toMatchSnapshot()); + }); + + it("line", async () => { + const { getByToolName, container } = await render(<Excalidraw />); + // select tool + const tool = getByToolName("line"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas.interactive")!; + // first point is added on pointer down + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 30 }); + + // second point, enable multi point + fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 }); + fireEvent.pointerMove(canvas, { clientX: 50, clientY: 60 }); + + // third point + fireEvent.pointerDown(canvas, { clientX: 50, clientY: 60 }); + fireEvent.pointerUp(canvas); + fireEvent.pointerMove(canvas, { clientX: 100, clientY: 140 }); + + // done + fireEvent.pointerDown(canvas); + fireEvent.pointerUp(canvas); + fireEvent.keyDown(document, { + key: KEYS.ENTER, + }); + expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`7`); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`); + expect(h.elements.length).toEqual(1); + + const element = h.elements[0] as ExcalidrawLinearElement; + + expect(element.type).toEqual("line"); + expect(element.x).toEqual(30); + expect(element.y).toEqual(30); + expect(element.points).toEqual([ + [0, 0], + [20, 30], + [70, 110], + ]); + + h.elements.forEach((element) => expect(element).toMatchSnapshot()); + }); +}); diff --git a/packages/excalidraw/tests/packages/__snapshots__/excalidraw.test.tsx.snap b/packages/excalidraw/tests/packages/__snapshots__/excalidraw.test.tsx.snap new file mode 100644 index 0000000..4fc5864 --- /dev/null +++ b/packages/excalidraw/tests/packages/__snapshots__/excalidraw.test.tsx.snap @@ -0,0 +1,616 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu items if passed from host 1`] = ` +<div + class="dropdown-menu" + data-testid="dropdown-menu" +> + <div + class="Island dropdown-menu-container" + style="--padding: 2; z-index: 2;" + > + <button + class="dropdown-menu-item dropdown-menu-item-base" + type="button" + > + <div + class="dropdown-menu-item__icon" + /> + <div + class="dropdown-menu-item__text" + > + Click me + </div> + </button> + <a + class="dropdown-menu-item dropdown-menu-item-base" + href="https://plus.excalidraw.com/blog" + rel="noreferrer" + target="_blank" + > + <div + class="dropdown-menu-item__icon" + /> + <div + class="dropdown-menu-item__text" + > + Excalidraw blog + </div> + </a> + <div + class="dropdown-menu-item-base dropdown-menu-item-custom" + > + <button + style="height: 2rem;" + > + custom menu item + </button> + </div> + <button + aria-label="Help" + class="dropdown-menu-item dropdown-menu-item-base" + data-testid="help-menu-item" + title="Help" + type="button" + > + <div + class="dropdown-menu-item__icon" + > + <svg + aria-hidden="true" + class="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + viewBox="0 0 24 24" + > + <g + stroke-width="1.5" + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <circle + cx="12" + cy="12" + r="9" + /> + <line + x1="12" + x2="12" + y1="17" + y2="17.01" + /> + <path + d="M12 13.5a1.5 1.5 0 0 1 1 -1.5a2.6 2.6 0 1 0 -3 -4" + /> + </g> + </svg> + </div> + <div + class="dropdown-menu-item__text" + > + Help + </div> + <div + class="dropdown-menu-item__shortcut" + > + ? + </div> + </button> + </div> +</div> +`; + +exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should render menu with default items when "UIOPtions" is "undefined" 1`] = ` +<div + class="dropdown-menu" + data-testid="dropdown-menu" +> + <div + class="Island dropdown-menu-container" + style="--padding: 2; z-index: 2;" + > + <button + aria-label="Open" + class="dropdown-menu-item dropdown-menu-item-base" + data-testid="load-button" + title="Open" + type="button" + > + <div + class="dropdown-menu-item__icon" + > + <svg + aria-hidden="true" + class="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + viewBox="0 0 20 20" + > + <path + d="m9.257 6.351.183.183H15.819c.34 0 .727.182 1.051.506.323.323.505.708.505 1.05v5.819c0 .316-.183.7-.52 1.035-.337.338-.723.522-1.037.522H4.182c-.352 0-.74-.181-1.058-.5-.318-.318-.499-.705-.499-1.057V5.182c0-.351.181-.736.5-1.054.32-.321.71-.503 1.057-.503H6.53l2.726 2.726Z" + stroke-width="1.25" + /> + </svg> + </div> + <div + class="dropdown-menu-item__text" + > + Open + </div> + <div + class="dropdown-menu-item__shortcut" + > + Ctrl+O + </div> + </button> + <button + aria-label="Save to..." + class="dropdown-menu-item dropdown-menu-item-base" + data-testid="json-export-button" + title="Save to..." + type="button" + > + <div + class="dropdown-menu-item__icon" + > + <svg + aria-hidden="true" + class="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + viewBox="0 0 20 20" + > + <path + d="M3.333 14.167v1.666c0 .92.747 1.667 1.667 1.667h10c.92 0 1.667-.746 1.667-1.667v-1.666M5.833 9.167 10 13.333l4.167-4.166M10 3.333v10" + stroke-width="1.25" + /> + </svg> + </div> + <div + class="dropdown-menu-item__text" + > + Save to... + </div> + </button> + <button + aria-label="Export image..." + class="dropdown-menu-item dropdown-menu-item-base" + data-testid="image-export-button" + title="Export image..." + type="button" + > + <div + class="dropdown-menu-item__icon" + > + <svg + aria-hidden="true" + class="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + viewBox="0 0 24 24" + > + <g + stroke-width="1.25" + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M15 8h.01" + /> + <path + d="M12 20h-5a3 3 0 0 1 -3 -3v-10a3 3 0 0 1 3 -3h10a3 3 0 0 1 3 3v5" + /> + <path + d="M4 15l4 -4c.928 -.893 2.072 -.893 3 0l4 4" + /> + <path + d="M14 14l1 -1c.617 -.593 1.328 -.793 2.009 -.598" + /> + <path + d="M19 16v6" + /> + <path + d="M22 19l-3 3l-3 -3" + /> + </g> + </svg> + </div> + <div + class="dropdown-menu-item__text" + > + Export image... + </div> + <div + class="dropdown-menu-item__shortcut" + > + Ctrl+Shift+E + </div> + </button> + <button + aria-label="Help" + class="dropdown-menu-item dropdown-menu-item-base" + data-testid="help-menu-item" + title="Help" + type="button" + > + <div + class="dropdown-menu-item__icon" + > + <svg + aria-hidden="true" + class="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + viewBox="0 0 24 24" + > + <g + stroke-width="1.5" + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <circle + cx="12" + cy="12" + r="9" + /> + <line + x1="12" + x2="12" + y1="17" + y2="17.01" + /> + <path + d="M12 13.5a1.5 1.5 0 0 1 1 -1.5a2.6 2.6 0 1 0 -3 -4" + /> + </g> + </svg> + </div> + <div + class="dropdown-menu-item__text" + > + Help + </div> + <div + class="dropdown-menu-item__shortcut" + > + ? + </div> + </button> + <button + aria-label="Reset the canvas" + class="dropdown-menu-item dropdown-menu-item-base" + data-testid="clear-canvas-button" + title="Reset the canvas" + type="button" + > + <div + class="dropdown-menu-item__icon" + > + <svg + aria-hidden="true" + class="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + viewBox="0 0 20 20" + > + <path + d="M3.333 5.833h13.334M8.333 9.167v5M11.667 9.167v5M4.167 5.833l.833 10c0 .92.746 1.667 1.667 1.667h6.666c.92 0 1.667-.746 1.667-1.667l.833-10M7.5 5.833v-2.5c0-.46.373-.833.833-.833h3.334c.46 0 .833.373.833.833v2.5" + stroke-width="1.25" + /> + </svg> + </div> + <div + class="dropdown-menu-item__text" + > + Reset the canvas + </div> + </button> + <div + style="height: 1px; margin: .5rem 0px;" + /> + <div + class="dropdown-menu-group " + > + <p + class="dropdown-menu-group-title" + > + Excalidraw links + </p> + <a + aria-label="GitHub" + class="dropdown-menu-item dropdown-menu-item-base" + href="https://github.com/excalidraw/excalidraw" + rel="noreferrer" + target="_blank" + title="GitHub" + > + <div + class="dropdown-menu-item__icon" + > + <svg + aria-hidden="true" + class="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + viewBox="0 0 20 20" + > + <path + d="M7.5 15.833c-3.583 1.167-3.583-2.083-5-2.5m10 4.167v-2.917c0-.833.083-1.166-.417-1.666 2.334-.25 4.584-1.167 4.584-5a3.833 3.833 0 0 0-1.084-2.667 3.5 3.5 0 0 0-.083-2.667s-.917-.25-2.917 1.084a10.25 10.25 0 0 0-5.166 0C5.417 2.333 4.5 2.583 4.5 2.583a3.5 3.5 0 0 0-.083 2.667 3.833 3.833 0 0 0-1.084 2.667c0 3.833 2.25 4.75 4.584 5-.5.5-.5 1-.417 1.666V17.5" + stroke-width="1.25" + /> + </svg> + </div> + <div + class="dropdown-menu-item__text" + > + GitHub + </div> + </a> + <a + aria-label="Discord" + class="dropdown-menu-item dropdown-menu-item-base" + href="https://discord.gg/UexuTaE" + rel="noreferrer" + target="_blank" + title="Discord" + > + <div + class="dropdown-menu-item__icon" + > + <svg + aria-hidden="true" + class="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + viewBox="0 0 20 20" + > + <g + stroke-width="1.25" + > + <path + d="M7.5 10.833a.833.833 0 1 0 0-1.666.833.833 0 0 0 0 1.666ZM12.5 10.833a.833.833 0 1 0 0-1.666.833.833 0 0 0 0 1.666ZM6.25 6.25c2.917-.833 4.583-.833 7.5 0M5.833 13.75c2.917.833 5.417.833 8.334 0" + /> + <path + d="M12.917 14.167c0 .833 1.25 2.5 1.666 2.5 1.25 0 2.361-1.39 2.917-2.5.556-1.39.417-4.861-1.25-9.584-1.214-.846-2.5-1.116-3.75-1.25l-.833 2.084M7.083 14.167c0 .833-1.13 2.5-1.526 2.5-1.191 0-2.249-1.39-2.778-2.5-.529-1.39-.397-4.861 1.19-9.584 1.157-.846 2.318-1.116 3.531-1.25l.833 2.084" + /> + </g> + </svg> + </div> + <div + class="dropdown-menu-item__text" + > + Discord + </div> + </a> + <a + aria-label="Twitter" + class="dropdown-menu-item dropdown-menu-item-base" + href="https://twitter.com/excalidraw" + rel="noreferrer" + target="_blank" + title="Twitter" + > + <div + class="dropdown-menu-item__icon" + > + <svg + aria-hidden="true" + class="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + viewBox="0 0 24 24" + > + <g + stroke-width="1.25" + > + <path + d="M0 0h24v24H0z" + fill="none" + stroke="none" + /> + <path + d="M22 4.01c-1 .49 -1.98 .689 -3 .99c-1.121 -1.265 -2.783 -1.335 -4.38 -.737s-2.643 2.06 -2.62 3.737v1c-3.245 .083 -6.135 -1.395 -8 -4c0 0 -4.182 7.433 4 11c-1.872 1.247 -3.739 2.088 -6 2c3.308 1.803 6.913 2.423 10.034 1.517c3.58 -1.04 6.522 -3.723 7.651 -7.742a13.84 13.84 0 0 0 .497 -3.753c-.002 -.249 1.51 -2.772 1.818 -4.013z" + /> + </g> + </svg> + </div> + <div + class="dropdown-menu-item__text" + > + Twitter + </div> + </a> + </div> + <div + style="height: 1px; margin: .5rem 0px;" + /> + <button + aria-label="Dark mode" + class="dropdown-menu-item dropdown-menu-item-base" + data-testid="toggle-dark-mode" + title="Dark mode" + type="button" + > + <div + class="dropdown-menu-item__icon" + > + <svg + aria-hidden="true" + class="" + fill="none" + focusable="false" + role="img" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + viewBox="0 0 20 20" + > + <path + clip-rule="evenodd" + d="M10 2.5h.328a6.25 6.25 0 0 0 6.6 10.372A7.5 7.5 0 1 1 10 2.493V2.5Z" + stroke="currentColor" + /> + </svg> + </div> + <div + class="dropdown-menu-item__text" + > + Dark mode + </div> + <div + class="dropdown-menu-item__shortcut" + > + Shift+Alt+D + </div> + </button> + <div + style="margin-top: 0.5rem;" + > + <div + data-testid="canvas-background-label" + style="font-size: .75rem; margin-bottom: .5rem;" + > + Canvas background + </div> + <div + style="padding: 0px 0.625rem;" + > + <div> + <div + aria-modal="true" + class="color-picker-container" + role="dialog" + > + <div + class="color-picker__top-picks" + > + <button + class="color-picker__button active" + data-testid="color-top-pick-#ffffff" + style="--swatch-color: #ffffff;" + title="#ffffff" + type="button" + > + <div + class="color-picker__button-outline" + /> + </button> + <button + class="color-picker__button" + data-testid="color-top-pick-#f8f9fa" + style="--swatch-color: #f8f9fa;" + title="#f8f9fa" + type="button" + > + <div + class="color-picker__button-outline" + /> + </button> + <button + class="color-picker__button" + data-testid="color-top-pick-#f5faff" + style="--swatch-color: #f5faff;" + title="#f5faff" + type="button" + > + <div + class="color-picker__button-outline" + /> + </button> + <button + class="color-picker__button" + data-testid="color-top-pick-#fffce8" + style="--swatch-color: #fffce8;" + title="#fffce8" + type="button" + > + <div + class="color-picker__button-outline" + /> + </button> + <button + class="color-picker__button" + data-testid="color-top-pick-#fdf8f6" + style="--swatch-color: #fdf8f6;" + title="#fdf8f6" + type="button" + > + <div + class="color-picker__button-outline" + /> + </button> + </div> + <div + style="width: 1px; height: 100%; margin: 0px auto;" + /> + <button + aria-controls="radix-:r0:" + aria-expanded="false" + aria-haspopup="dialog" + aria-label="Canvas background" + class="color-picker__button active-color" + data-state="closed" + style="--swatch-color: #ffffff;" + title="Show background color picker" + type="button" + > + <div + class="color-picker__button-outline" + /> + </button> + </div> + </div> + </div> + </div> + </div> +</div> +`; diff --git a/packages/excalidraw/tests/packages/__snapshots__/utils.test.ts.snap b/packages/excalidraw/tests/packages/__snapshots__/utils.test.ts.snap new file mode 100644 index 0000000..610d97e --- /dev/null +++ b/packages/excalidraw/tests/packages/__snapshots__/utils.test.ts.snap @@ -0,0 +1,103 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`exportToSvg > with default arguments 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "amIBeingFollowed": false, + "collaborators": Map {}, + "contextMenu": null, + "currentChartType": "bar", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "draggingElement": null, + "editingElement": null, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportPadding": undefined, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridSize": null, + "isBindingEnabled": true, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "name", + "objectsSnapModeEnabled": false, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "shouldDisconnectFollowModeOnCanvasInteraction": true, + "showHyperlinkPopup": false, + "showStats": false, + "showWelcomeScreen": false, + "snapLines": [], + "startBoundElement": null, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; diff --git a/packages/excalidraw/tests/packages/events.test.tsx b/packages/excalidraw/tests/packages/events.test.tsx new file mode 100644 index 0000000..ad8b3c6 --- /dev/null +++ b/packages/excalidraw/tests/packages/events.test.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import { vi } from "vitest"; +import { Excalidraw, CaptureUpdateAction } from "../../index"; +import type { ExcalidrawImperativeAPI } from "../../types"; +import { resolvablePromise } from "../../utils"; +import { render } from "../test-utils"; +import { Pointer } from "../helpers/ui"; +import { API } from "../helpers/api"; + +describe("event callbacks", () => { + const h = window.h; + + let excalidrawAPI: ExcalidrawImperativeAPI; + + const mouse = new Pointer("mouse"); + + beforeEach(async () => { + const excalidrawAPIPromise = resolvablePromise<ExcalidrawImperativeAPI>(); + await render( + <Excalidraw + excalidrawAPI={(api) => excalidrawAPIPromise.resolve(api as any)} + />, + ); + excalidrawAPI = await excalidrawAPIPromise; + }); + + it("should trigger onChange on render", async () => { + const onChange = vi.fn(); + + const origBackgroundColor = h.state.viewBackgroundColor; + excalidrawAPI.onChange(onChange); + API.updateScene({ + appState: { viewBackgroundColor: "red" }, + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); + expect(onChange).toHaveBeenCalledWith( + // elements + [], + // appState + expect.objectContaining({ + viewBackgroundColor: "red", + }), + // files + {}, + ); + expect(onChange.mock?.lastCall?.[1].viewBackgroundColor).not.toBe( + origBackgroundColor, + ); + }); + + it("should trigger onPointerDown/onPointerUp on canvas pointerDown/pointerUp", async () => { + const onPointerDown = vi.fn(); + const onPointerUp = vi.fn(); + + excalidrawAPI.onPointerDown(onPointerDown); + excalidrawAPI.onPointerUp(onPointerUp); + + mouse.downAt(100); + expect(onPointerDown).toHaveBeenCalledTimes(1); + expect(onPointerUp).not.toHaveBeenCalled(); + mouse.up(); + expect(onPointerDown).toHaveBeenCalledTimes(1); + expect(onPointerUp).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/excalidraw/tests/queries/dom.ts b/packages/excalidraw/tests/queries/dom.ts new file mode 100644 index 0000000..e1515c8 --- /dev/null +++ b/packages/excalidraw/tests/queries/dom.ts @@ -0,0 +1,19 @@ +import { waitFor } from "@testing-library/dom"; +import { fireEvent } from "@testing-library/react"; + +export const getTextEditor = async (selector: string, waitForEditor = true) => { + const query = () => document.querySelector(selector) as HTMLTextAreaElement; + if (waitForEditor) { + await waitFor(() => expect(query()).not.toBe(null)); + return query(); + } + return query(); +}; + +export const updateTextEditor = ( + editor: HTMLTextAreaElement | HTMLInputElement, + value: string, +) => { + fireEvent.change(editor, { target: { value } }); + fireEvent.input(editor); +}; diff --git a/packages/excalidraw/tests/queries/toolQueries.ts b/packages/excalidraw/tests/queries/toolQueries.ts new file mode 100644 index 0000000..df0afa1 --- /dev/null +++ b/packages/excalidraw/tests/queries/toolQueries.ts @@ -0,0 +1,25 @@ +import { queries, buildQueries } from "@testing-library/react"; +import type { ToolType } from "../../types"; +import { TOOL_TYPE } from "../../constants"; + +const _getAllByToolName = (container: HTMLElement, tool: ToolType | "lock") => { + const toolTitle = tool === "lock" ? "lock" : TOOL_TYPE[tool]; + return queries.getAllByTestId(container, `toolbar-${toolTitle}`); +}; + +const getMultipleError = (_container: any, tool: any) => + `Found multiple elements with tool name: ${tool}`; +const getMissingError = (_container: any, tool: any) => + `Unable to find an element with tool name: ${tool}`; + +export const [ + queryByToolName, + getAllByToolName, + getByToolName, + findAllByToolName, + findByToolName, +] = buildQueries<(ToolType | "lock")[]>( + _getAllByToolName, + getMultipleError, + getMissingError, +); diff --git a/packages/excalidraw/tests/regressionTests.test.tsx b/packages/excalidraw/tests/regressionTests.test.tsx new file mode 100644 index 0000000..a3e45bc --- /dev/null +++ b/packages/excalidraw/tests/regressionTests.test.tsx @@ -0,0 +1,1183 @@ +import React from "react"; +import type { ExcalidrawElement } from "../element/types"; +import { CODES, KEYS } from "../keys"; +import { Excalidraw } from "../index"; +import { reseed } from "../random"; +import * as StaticScene from "../renderer/staticScene"; +import { setDateTimeForTests } from "../utils"; +import { API } from "./helpers/api"; +import { Keyboard, Pointer, UI } from "./helpers/ui"; +import { + assertSelectedElements, + fireEvent, + render, + screen, + togglePopover, + unmountComponent, +} from "./test-utils"; +import { FONT_FAMILY } from "../constants"; +import { vi } from "vitest"; + +const { h } = window; + +const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene"); + +const mouse = new Pointer("mouse"); +const finger1 = new Pointer("touch", 1); +const finger2 = new Pointer("touch", 2); + +/** + * This is always called at the end of your test, so usually you don't need to call it. + * However, if you have a long test, you might want to call it during the test so it's easier + * to debug where a test failure came from. + */ +const checkpoint = (name: string) => { + expect(renderStaticScene.mock.calls.length).toMatchSnapshot( + `[${name}] number of renders`, + ); + expect(h.state).toMatchSnapshot(`[${name}] appState`); + expect(h.history).toMatchSnapshot(`[${name}] history`); + expect(h.elements.length).toMatchSnapshot(`[${name}] number of elements`); + h.elements.forEach((element, i) => + expect(element).toMatchSnapshot(`[${name}] element ${i}`), + ); +}; +beforeEach(async () => { + unmountComponent(); + + localStorage.clear(); + renderStaticScene.mockClear(); + reseed(7); + setDateTimeForTests("201933152653"); + + mouse.reset(); + finger1.reset(); + finger2.reset(); + + await render(<Excalidraw handleKeyboardGlobally={true} />); + API.setAppState({ height: 768, width: 1024 }); +}); + +afterEach(() => { + checkpoint("end of test"); +}); + +describe("regression tests", () => { + it("draw every type of shape", () => { + UI.clickTool("rectangle"); + mouse.down(10, -10); + mouse.up(20, 10); + + UI.clickTool("diamond"); + mouse.down(10, -10); + mouse.up(20, 10); + + UI.clickTool("ellipse"); + mouse.down(10, -10); + mouse.up(20, 10); + + UI.clickTool("arrow"); + mouse.down(40, -10); + mouse.up(50, 10); + + UI.clickTool("line"); + mouse.down(40, -10); + mouse.up(50, 10); + + UI.clickTool("arrow"); + mouse.click(40, -10); + mouse.click(50, 10); + mouse.click(30, 10); + Keyboard.keyPress(KEYS.ENTER); + + UI.clickTool("line"); + mouse.click(40, -20); + mouse.click(50, 10); + mouse.click(30, 10); + Keyboard.keyPress(KEYS.ENTER); + + UI.clickTool("freedraw"); + mouse.down(40, -20); + mouse.up(50, 10); + + expect(h.elements.map((element) => element.type)).toEqual([ + "rectangle", + "diamond", + "ellipse", + "arrow", + "line", + "arrow", + "line", + "freedraw", + ]); + }); + + it("click to select a shape", () => { + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(10, 10); + + const firstRectPos = mouse.getPosition(); + + UI.clickTool("rectangle"); + mouse.down(10, -10); + mouse.up(10, 10); + + const prevSelectedId = API.getSelectedElement().id; + mouse.restorePosition(...firstRectPos); + mouse.click(); + + expect(API.getSelectedElement().id).not.toEqual(prevSelectedId); + }); + + for (const [keys, shape, shouldSelect] of [ + [`2${KEYS.R}`, "rectangle", true], + [`3${KEYS.D}`, "diamond", true], + [`4${KEYS.O}`, "ellipse", true], + [`5${KEYS.A}`, "arrow", true], + [`6${KEYS.L}`, "line", true], + [`7${KEYS.P}`, "freedraw", false], + ] as [string, ExcalidrawElement["type"], boolean][]) { + for (const key of keys) { + it(`key ${key} selects ${shape} tool`, () => { + Keyboard.keyPress(key); + + expect(h.state.activeTool.type).toBe(shape); + + mouse.down(10, 10); + mouse.up(10, 10); + + if (shouldSelect) { + expect(API.getSelectedElement().type).toBe(shape); + } + }); + } + } + it("change the properties of a shape", () => { + UI.clickTool("rectangle"); + + mouse.down(10, 10); + mouse.up(10, 10); + togglePopover("Background"); + UI.clickOnTestId("color-yellow"); + UI.clickOnTestId("color-red"); + + togglePopover("Stroke"); + UI.clickOnTestId("color-blue"); + expect(API.getSelectedElement().backgroundColor).toBe("#ffc9c9"); + expect(API.getSelectedElement().strokeColor).toBe("#1971c2"); + }); + + it("click on an element and drag it", () => { + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(10, 10); + + const { x: prevX, y: prevY } = API.getSelectedElement(); + mouse.down(-8, -8); + mouse.up(10, 10); + + const { x: nextX, y: nextY } = API.getSelectedElement(); + expect(nextX).toBeGreaterThan(prevX); + expect(nextY).toBeGreaterThan(prevY); + + checkpoint("dragged"); + + mouse.down(); + mouse.up(-10, -10); + + const { x, y } = API.getSelectedElement(); + expect(x).toBe(prevX); + expect(y).toBe(prevY); + }); + + it("alt-drag duplicates an element", () => { + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(10, 10); + + expect( + h.elements.filter((element) => element.type === "rectangle").length, + ).toBe(1); + Keyboard.withModifierKeys({ alt: true }, () => { + mouse.down(-8, -8); + mouse.up(10, 10); + }); + + expect( + h.elements.filter((element) => element.type === "rectangle").length, + ).toBe(2); + }); + + it("click-drag to select a group", () => { + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(10, 10); + + UI.clickTool("rectangle"); + mouse.down(10, -10); + mouse.up(10, 10); + + const finalPosition = mouse.getPosition(); + + UI.clickTool("rectangle"); + mouse.down(10, -10); + mouse.up(10, 10); + + mouse.restorePosition(0, 0); + mouse.down(); + mouse.restorePosition(...finalPosition); + mouse.up(5, 5); + + expect( + h.elements.filter((element) => h.state.selectedElementIds[element.id]) + .length, + ).toBe(2); + }); + + it("shift-click to multiselect, then drag", () => { + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(10, 10); + + UI.clickTool("rectangle"); + mouse.down(10, -10); + mouse.up(10, 10); + + const prevRectsXY = h.elements + .filter((element) => element.type === "rectangle") + .map((element) => ({ x: element.x, y: element.y })); + + mouse.reset(); + mouse.click(10, 10); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.click(20, 0); + }); + + mouse.down(); + mouse.up(10, 10); + + h.elements + .filter((element) => element.type === "rectangle") + .forEach((element, i) => { + expect(element.x).toBeGreaterThan(prevRectsXY[i].x); + expect(element.y).toBeGreaterThan(prevRectsXY[i].y); + }); + }); + + it("pinch-to-zoom works", () => { + expect(h.state.zoom.value).toBe(1); + finger1.down(50, 50); + finger2.down(60, 50); + finger1.move(-10, 0); + expect(h.state.zoom.value).toBeGreaterThan(1); + const zoomed = h.state.zoom.value; + finger1.move(5, 0); + finger2.move(-5, 0); + expect(h.state.zoom.value).toBeLessThan(zoomed); + }); + + it("two-finger scroll works", () => { + // scroll horizontally vertically + + const startScrollY = h.state.scrollY; + + finger1.downAt(0, 0); + finger2.downAt(10, 0); + + finger1.clientY -= 10; + finger2.clientY -= 10; + + finger1.moveTo(); + finger2.moveTo(); + + finger1.upAt(); + finger2.upAt(); + expect(h.state.scrollY).toBeLessThan(startScrollY); + + // scroll horizontally + + const startScrollX = h.state.scrollX; + + finger1.downAt(); + finger2.downAt(); + + finger1.clientX += 10; + finger2.clientX += 10; + + finger1.moveTo(); + finger2.moveTo(); + + finger1.upAt(); + finger2.upAt(); + + expect(h.state.scrollX).toBeGreaterThan(startScrollX); + }); + + it("spacebar + drag scrolls the canvas", () => { + const { scrollX: startScrollX, scrollY: startScrollY } = h.state; + Keyboard.keyDown(KEYS.SPACE); + mouse.down(50, 50); + mouse.up(60, 60); + Keyboard.keyUp(KEYS.SPACE); + const { scrollX, scrollY } = h.state; + expect(scrollX).not.toEqual(startScrollX); + expect(scrollY).not.toEqual(startScrollY); + }); + + it("arrow keys", () => { + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(10, 10); + Keyboard.keyPress(KEYS.ARROW_LEFT); + Keyboard.keyPress(KEYS.ARROW_LEFT); + Keyboard.keyPress(KEYS.ARROW_RIGHT); + Keyboard.keyPress(KEYS.ARROW_UP); + Keyboard.keyPress(KEYS.ARROW_UP); + Keyboard.keyPress(KEYS.ARROW_DOWN); + expect(h.elements[0].x).toBe(9); + expect(h.elements[0].y).toBe(9); + }); + + it("undo/redo drawing an element", () => { + UI.clickTool("rectangle"); + mouse.down(10, -10); + mouse.up(20, 10); + + UI.clickTool("rectangle"); + mouse.down(10, 0); + mouse.up(30, 20); + + UI.clickTool("arrow"); + mouse.click(60, -10); + mouse.click(60, 10); + mouse.click(40, 10); + Keyboard.keyPress(KEYS.ENTER); + + expect(h.elements.filter((element) => !element.isDeleted).length).toBe(3); + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.Z); + Keyboard.keyPress(KEYS.Z); + Keyboard.keyPress(KEYS.Z); + }); + expect(h.elements.filter((element) => !element.isDeleted).length).toBe(2); + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.Z); + }); + expect(h.elements.filter((element) => !element.isDeleted).length).toBe(1); + Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => { + Keyboard.keyPress(KEYS.Z); + }); + expect(h.elements.filter((element) => !element.isDeleted).length).toBe(2); + }); + + it("noop interaction after undo shouldn't create history entry", () => { + expect(API.getUndoStack().length).toBe(0); + + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(10, 10); + + const firstElementEndPoint = mouse.getPosition(); + + UI.clickTool("rectangle"); + mouse.down(10, -10); + mouse.up(10, 10); + + const secondElementEndPoint = mouse.getPosition(); + + expect(API.getUndoStack().length).toBe(2); + + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.Z); + }); + + expect(API.getUndoStack().length).toBe(1); + + // clicking an element shouldn't add to history + mouse.restorePosition(...firstElementEndPoint); + mouse.click(); + expect(API.getUndoStack().length).toBe(1); + + Keyboard.withModifierKeys({ shift: true, ctrl: true }, () => { + Keyboard.keyPress(KEYS.Z); + }); + + expect(API.getUndoStack().length).toBe(2); + + // clicking an element should add to history + mouse.click(); + expect(API.getUndoStack().length).toBe(3); + + const firstSelectedElementId = API.getSelectedElement().id; + + // same for clicking the element just redo-ed + mouse.restorePosition(...secondElementEndPoint); + mouse.click(); + expect(API.getUndoStack().length).toBe(4); + + expect(API.getSelectedElement().id).not.toEqual(firstSelectedElementId); + }); + + it("zoom hotkeys", () => { + expect(h.state.zoom.value).toBe(1); + fireEvent.keyDown(document, { + code: CODES.EQUAL, + ctrlKey: true, + }); + fireEvent.keyUp(document, { + code: CODES.EQUAL, + ctrlKey: true, + }); + expect(h.state.zoom.value).toBeGreaterThan(1); + fireEvent.keyDown(document, { + code: CODES.MINUS, + ctrlKey: true, + }); + fireEvent.keyUp(document, { + code: CODES.MINUS, + ctrlKey: true, + }); + expect(h.state.zoom.value).toBe(1); + }); + + it("make a group and duplicate it", () => { + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(10, 10); + + UI.clickTool("rectangle"); + mouse.down(10, -10); + mouse.up(10, 10); + + UI.clickTool("rectangle"); + mouse.down(10, -10); + mouse.up(10, 10); + const end = mouse.getPosition(); + + mouse.reset(); + mouse.down(); + mouse.restorePosition(...end); + mouse.up(); + + expect(h.elements.length).toBe(3); + for (const element of h.elements) { + expect(element.groupIds.length).toBe(0); + expect(h.state.selectedElementIds[element.id]).toBe(true); + } + + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.G); + }); + + for (const element of h.elements) { + expect(element.groupIds.length).toBe(1); + } + + Keyboard.withModifierKeys({ alt: true }, () => { + mouse.restorePosition(...end); + mouse.down(); + mouse.up(10, 10); + }); + + expect(h.elements.length).toBe(6); + const groups = new Set(); + for (const element of h.elements) { + for (const groupId of element.groupIds) { + groups.add(groupId); + } + } + + expect(groups.size).toBe(2); + }); + + it("should group elements and ungroup them", () => { + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(10, 10); + + UI.clickTool("rectangle"); + mouse.down(10, -10); + mouse.up(10, 10); + + UI.clickTool("rectangle"); + mouse.down(10, -10); + mouse.up(10, 10); + const end = mouse.getPosition(); + + mouse.reset(); + mouse.down(); + mouse.restorePosition(...end); + mouse.up(); + + for (const element of h.elements) { + expect(element.groupIds.length).toBe(0); + } + + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.G); + }); + + for (const element of h.elements) { + expect(element.groupIds.length).toBe(1); + } + + mouse.moveTo(-10, -10); // the NW resizing handle is at [0, 0], so moving further + mouse.down(); + mouse.restorePosition(...end); + mouse.up(); + + Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => { + Keyboard.keyPress(KEYS.G); + }); + + for (const element of h.elements) { + expect(element.groupIds.length).toBe(0); + } + }); + + it("double click to edit a group", () => { + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(10, 10); + + UI.clickTool("rectangle"); + mouse.down(10, -10); + mouse.up(10, 10); + + UI.clickTool("rectangle"); + mouse.down(10, -10); + mouse.up(10, 10); + + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.A); + Keyboard.keyPress(KEYS.G); + }); + + expect(API.getSelectedElements().length).toBe(3); + expect(h.state.editingGroupId).toBe(null); + mouse.doubleClick(); + expect(API.getSelectedElements().length).toBe(1); + expect(h.state.editingGroupId).not.toBe(null); + }); + + it("adjusts z order when grouping", () => { + const positions: number[][] = []; + + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(10, 10); + positions.push(mouse.getPosition()); + + UI.clickTool("rectangle"); + mouse.down(10, -10); + mouse.up(10, 10); + positions.push(mouse.getPosition()); + + UI.clickTool("rectangle"); + mouse.down(10, -10); + mouse.up(10, 10); + positions.push(mouse.getPosition()); + + const ids = h.elements.map((element) => element.id); + + mouse.restorePosition(...positions[0]); + mouse.click(); + mouse.restorePosition(...positions[2]); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.click(); + }); + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.G); + }); + + expect(h.elements.map((element) => element.id)).toEqual([ + ids[1], + ids[0], + ids[2], + ]); + }); + + it("supports nested groups", () => { + const rectA = UI.createElement("rectangle", { position: 0, size: 50 }); + const rectB = UI.createElement("rectangle", { position: 100, size: 50 }); + const rectC = UI.createElement("rectangle", { position: 200, size: 50 }); + + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.A); + Keyboard.keyPress(KEYS.G); + }); + + mouse.doubleClickOn(rectC); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.clickOn(rectA); + }); + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.G); + }); + + expect(rectC.groupIds.length).toBe(2); + expect(rectA.groupIds).toEqual(rectC.groupIds); + expect(rectB.groupIds).toEqual(rectA.groupIds.slice(1)); + + mouse.click(0, 100); + expect(API.getSelectedElements().length).toBe(0); + + mouse.clickOn(rectA); + expect(API.getSelectedElements().length).toBe(3); + expect(h.state.editingGroupId).toBe(null); + + mouse.doubleClickOn(rectA); + expect(API.getSelectedElements().length).toBe(2); + expect(h.state.editingGroupId).toBe(rectA.groupIds[1]); + + mouse.doubleClickOn(rectA); + expect(API.getSelectedElements().length).toBe(1); + expect(h.state.editingGroupId).toBe(rectA.groupIds[0]); + + // click outside current (sub)group + mouse.clickOn(rectB); + expect(API.getSelectedElements().length).toBe(3); + mouse.doubleClickOn(rectB); + expect(API.getSelectedElements().length).toBe(1); + }); + + it("updates fontSize & fontFamily appState", () => { + UI.clickTool("text"); + expect(h.state.currentItemFontFamily).toEqual(FONT_FAMILY.Excalifont); + fireEvent.click(screen.getByTitle(/code/i)); + expect(h.state.currentItemFontFamily).toEqual(FONT_FAMILY["Comic Shanns"]); + }); + + it("deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element", () => { + UI.clickTool("ellipse"); + mouse.down(); + mouse.up(100, 100); + + expect(API.getSelectedElements().length).toBe(1); + + // hits bounding box without hitting element + mouse.down(98, 98); + mouse.up(); + expect(API.getSelectedElements().length).toBe(0); + }); + + it("switches selected element on pointer down", () => { + UI.clickTool("rectangle"); + mouse.down(); + mouse.up(10, 10); + + UI.clickTool("ellipse"); + mouse.down(10, 10); + mouse.up(10, 10); + + expect(API.getSelectedElement().type).toBe("ellipse"); + + // pointer down on rectangle + mouse.reset(); + mouse.down(); + + expect(API.getSelectedElement().type).toBe("rectangle"); + }); + + it("can drag element that covers another element, while another elem is selected", () => { + UI.clickTool("rectangle"); + mouse.down(100, 100); + mouse.up(200, 200); + + UI.clickTool("rectangle"); + mouse.reset(); + mouse.down(100, 100); + mouse.up(200, 200); + + UI.clickTool("ellipse"); + mouse.reset(); + mouse.down(300, 300); + mouse.up(350, 350); + + expect(API.getSelectedElement().type).toBe("ellipse"); + + // pointer down on rectangle + mouse.reset(); + mouse.down(100, 100); + mouse.up(200, 200); + + expect(API.getSelectedElement().type).toBe("rectangle"); + }); + + it("deselects selected element on pointer down when pointer doesn't hit any element", () => { + UI.clickTool("rectangle"); + mouse.down(); + mouse.up(10, 10); + + expect(API.getSelectedElements().length).toBe(1); + + // pointer down on space without elements + mouse.down(100, 100); + + expect(API.getSelectedElements().length).toBe(0); + }); + + it("Drags selected element when hitting only bounding box and keeps element selected", () => { + UI.clickTool("ellipse"); + mouse.down(); + mouse.up(10, 10); + + const { x: prevX, y: prevY } = API.getSelectedElement(); + API.clearSelection(); + // drag element from point on bounding box that doesn't hit element + mouse.reset(); + mouse.down(8, 8); + mouse.up(25, 25); + + expect(API.getSelectedElement().x).toEqual(prevX + 25); + expect(API.getSelectedElement().y).toEqual(prevY + 25); + }); + + it( + "given selected element A with lower z-index than unselected element B and given B is partially over A " + + "when clicking intersection between A and B " + + "B should be selected on pointer up", + () => { + // set background color since default is transparent + // and transparent elements can't be selected by clicking inside of them + const rect1 = API.createElement({ + type: "rectangle", + backgroundColor: "red", + x: 0, + y: 0, + width: 1000, + height: 1000, + }); + const rect2 = API.createElement({ + type: "rectangle", + backgroundColor: "red", + x: 500, + y: 500, + width: 500, + height: 500, + }); + API.setElements([rect1, rect2]); + + mouse.select(rect1); + + // pointerdown on rect2 covering rect1 while rect1 is selected should + // retain rect1 selection + mouse.down(900, 900); + expect(API.getSelectedElement().id).toBe(rect1.id); + + // pointerup should select rect2 + mouse.up(); + expect(API.getSelectedElement().id).toBe(rect2.id); + }, + ); + + it( + "given selected element A with lower z-index than unselected element B and given B is partially over A " + + "when dragging on intersection between A and B " + + "A should be dragged and keep being selected", + () => { + const rect1 = API.createElement({ + type: "rectangle", + backgroundColor: "red", + x: 0, + y: 0, + width: 1000, + height: 1000, + }); + const rect2 = API.createElement({ + type: "rectangle", + backgroundColor: "red", + x: 500, + y: 500, + width: 500, + height: 500, + }); + API.setElements([rect1, rect2]); + + mouse.select(rect1); + + expect(API.getSelectedElement().id).toBe(rect1.id); + + const { x: prevX, y: prevY } = API.getSelectedElement(); + + // pointer down on intersection between ellipse and rectangle + mouse.down(900, 900); + mouse.up(100, 100); + + expect(API.getSelectedElement().id).toBe(rect1.id); + expect(API.getSelectedElement().x).toEqual(prevX + 100); + expect(API.getSelectedElement().y).toEqual(prevY + 100); + }, + ); + + it("deselects group of selected elements on pointer down when pointer doesn't hit any element", () => { + UI.clickTool("rectangle"); + mouse.down(); + mouse.up(10, 10); + + UI.clickTool("ellipse"); + mouse.down(100, 100); + mouse.up(10, 10); + + // Selects first element without deselecting the second element + // Second element is already selected because creating it was our last action + mouse.reset(); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.click(5, 5); + }); + + expect(API.getSelectedElements().length).toBe(2); + + // pointer down on space without elements + mouse.reset(); + mouse.down(500, 500); + + expect(API.getSelectedElements().length).toBe(0); + }); + + it("switches from group of selected elements to another element on pointer down", () => { + UI.clickTool("rectangle"); + mouse.down(); + mouse.up(10, 10); + + UI.clickTool("ellipse"); + mouse.down(100, 100); + mouse.up(100, 100); + + UI.clickTool("diamond"); + mouse.down(100, 100); + mouse.up(100, 100); + + // Selects ellipse without deselecting the diamond + // Diamond is already selected because creating it was our last action + mouse.reset(); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.click(110, 160); + }); + + expect(API.getSelectedElements().length).toBe(2); + + // select rectangle + mouse.reset(); + mouse.down(); + + expect(API.getSelectedElement().type).toBe("rectangle"); + }); + + it("deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element", () => { + UI.clickTool("rectangle"); + mouse.down(); + mouse.up(10, 10); + + UI.clickTool("ellipse"); + mouse.down(100, 100); + mouse.up(10, 10); + + // Selects first element without deselecting the second element + // Second element is already selected because creating it was our last action + mouse.reset(); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.click(5, 5); + }); + + // pointer down on common bounding box without hitting any of the elements + mouse.reset(); + mouse.down(50, 50); + expect(API.getSelectedElements().length).toBe(2); + + mouse.up(); + expect(API.getSelectedElements().length).toBe(0); + }); + + it("drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging", () => { + UI.clickTool("rectangle"); + mouse.down(); + mouse.up(10, 10); + + UI.clickTool("ellipse"); + mouse.down(100, 100); + mouse.up(10, 10); + + // Selects first element without deselecting the second element + // Second element is already selected because creating it was our last action + mouse.reset(); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.click(5, 5); + }); + + expect(API.getSelectedElements().length).toBe(2); + + const { x: firstElementPrevX, y: firstElementPrevY } = + API.getSelectedElements()[0]; + const { x: secondElementPrevX, y: secondElementPrevY } = + API.getSelectedElements()[1]; + + // drag elements from point on common bounding box that doesn't hit any of the elements + mouse.reset(); + mouse.down(50, 50); + mouse.up(25, 25); + + expect(API.getSelectedElements()[0].x).toEqual(firstElementPrevX + 25); + expect(API.getSelectedElements()[0].y).toEqual(firstElementPrevY + 25); + + expect(API.getSelectedElements()[1].x).toEqual(secondElementPrevX + 25); + expect(API.getSelectedElements()[1].y).toEqual(secondElementPrevY + 25); + + expect(API.getSelectedElements().length).toBe(2); + }); + + it( + "given a group of selected elements with an element that is not selected inside the group common bounding box " + + "when element that is not selected is clicked " + + "should switch selection to not selected element on pointer up", + () => { + UI.clickTool("rectangle"); + mouse.down(); + mouse.up(10, 10); + + UI.clickTool("ellipse"); + mouse.down(100, 100); + mouse.up(100, 100); + + UI.clickTool("diamond"); + mouse.down(100, 100); + mouse.up(100, 100); + + // Selects rectangle without deselecting the diamond + // Diamond is already selected because creating it was our last action + mouse.reset(); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.click(); + }); + + // pointer down on ellipse + mouse.down(110, 160); + expect(API.getSelectedElements().length).toBe(2); + + mouse.up(); + expect(API.getSelectedElement().type).toBe("ellipse"); + }, + ); + + it( + "given a selected element A and a not selected element B with higher z-index than A " + + "and given B partially overlaps A " + + "when there's a shift-click on the overlapped section B is added to the selection", + () => { + UI.clickTool("rectangle"); + // change background color since default is transparent + // and transparent elements can't be selected by clicking inside of them + togglePopover("Background"); + UI.clickOnTestId("color-red"); + mouse.down(); + mouse.up(1000, 1000); + + // draw ellipse partially over rectangle. + // since ellipse was created after rectangle it has an higher z-index. + // we don't need to change background color again since change above + // affects next drawn elements. + UI.clickTool("ellipse"); + mouse.reset(); + mouse.down(500, 500); + mouse.up(1000, 1000); + + // select rectangle + mouse.reset(); + mouse.click(); + + // click on intersection between ellipse and rectangle + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.click(900, 900); + }); + + expect(API.getSelectedElements().length).toBe(2); + }, + ); + + it("shift click on selected element should deselect it on pointer up", () => { + UI.clickTool("rectangle"); + mouse.down(); + mouse.up(10, 10); + + // Rectangle is already selected since creating + // it was our last action + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.down(-8, -8); + }); + expect(API.getSelectedElements().length).toBe(1); + + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.up(); + }); + expect(API.getSelectedElements().length).toBe(0); + }); + + it("single-clicking on a subgroup of a selected group should not alter selection", () => { + const rect1 = UI.createElement("rectangle", { + x: 10, + }); + const rect2 = UI.createElement("rectangle", { + x: 50, + }); + UI.group([rect1, rect2]); + + const rect3 = UI.createElement("rectangle", { + x: 10, + y: 50, + }); + const rect4 = UI.createElement("rectangle", { + x: 50, + y: 50, + }); + UI.group([rect3, rect4]); + + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.A); + Keyboard.keyPress(KEYS.G); + }); + + const selectedGroupIds_prev = h.state.selectedGroupIds; + const selectedElements_prev = API.getSelectedElements(); + mouse.clickOn(rect3); + expect(h.state.selectedGroupIds).toEqual(selectedGroupIds_prev); + expect(API.getSelectedElements()).toEqual(selectedElements_prev); + }); + + it("deleting last but one element in editing group should unselect the group", () => { + const rect1 = UI.createElement("rectangle", { x: 10 }); + const rect2 = UI.createElement("rectangle", { x: 50 }); + + UI.group([rect1, rect2]); + + mouse.doubleClickOn(rect1); + Keyboard.keyDown(KEYS.DELETE); + + // Clicking on the deleted element, hence in the empty space + mouse.clickOn(rect1); + + expect(h.state.selectedGroupIds).toEqual({}); + expect(API.getSelectedElements()).toEqual([]); + + // Clicking back in and expecting no group selection + mouse.clickOn(rect2); + + expect(h.state.selectedGroupIds).toEqual({ [rect2.groupIds[0]]: false }); + expect(API.getSelectedElements()).toEqual([rect2.get()]); + }); + + it("Cmd/Ctrl-click exclusively select element under pointer", () => { + const rect1 = UI.createElement("rectangle", { x: 0 }); + const rect2 = UI.createElement("rectangle", { x: 30 }); + + UI.group([rect1, rect2]); + assertSelectedElements(rect1, rect2); + + Keyboard.withModifierKeys({ ctrl: true }, () => { + mouse.clickOn(rect1); + }); + assertSelectedElements(rect1); + + API.clearSelection(); + Keyboard.withModifierKeys({ ctrl: true }, () => { + mouse.clickOn(rect1); + }); + assertSelectedElements(rect1); + + const rect3 = UI.createElement("rectangle", { x: 60 }); + UI.group([rect1, rect3]); + assertSelectedElements(rect1, rect2, rect3); + + mouse.reset(); + Keyboard.withModifierKeys({ ctrl: true }, () => { + mouse.click(10, 5); + }); + assertSelectedElements(rect1); + + API.clearSelection(); + Keyboard.withModifierKeys({ ctrl: true }, () => { + mouse.clickOn(rect3); + }); + assertSelectedElements(rect3); + }); +}); + +it( + "given element A and group of elements B and given both are selected " + + "when user clicks on B, on pointer up " + + "only elements from B should be selected", + () => { + const rect1 = UI.createElement("rectangle", { y: 0 }); + const rect2 = UI.createElement("rectangle", { y: 30 }); + const rect3 = UI.createElement("rectangle", { y: 60 }); + + UI.group([rect1, rect3]); + + expect(API.getSelectedElements().length).toBe(2); + expect(Object.keys(h.state.selectedGroupIds).length).toBe(1); + + // Select second rectangle without deselecting group + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.clickOn(rect2); + }); + expect(API.getSelectedElements().length).toBe(3); + + // clicking on first rectangle that is part of the group should select + // that group (exclusively) + mouse.clickOn(rect1); + expect(API.getSelectedElements().length).toBe(2); + expect(Object.keys(h.state.selectedGroupIds).length).toBe(1); + }, +); + +it( + "given element A and group of elements B and given both are selected " + + "when user shift-clicks on B, on pointer up " + + "only element A should be selected", + () => { + UI.clickTool("rectangle"); + mouse.down(); + mouse.up(100, 100); + + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(100, 100); + + UI.clickTool("rectangle"); + mouse.down(10, 10); + mouse.up(100, 100); + + // Select first rectangle while keeping third one selected. + // Third rectangle is selected because it was the last element to be created. + mouse.reset(); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.click(); + }); + + // Create group with first and third rectangle + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.G); + }); + + expect(API.getSelectedElements().length).toBe(2); + const selectedGroupIds = Object.keys(h.state.selectedGroupIds); + expect(selectedGroupIds.length).toBe(1); + + // Select second rectangle without deselecting group + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.click(110, 110); + }); + expect(API.getSelectedElements().length).toBe(3); + + // Pointer down o first rectangle that is part of the group + mouse.reset(); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.down(); + }); + expect(API.getSelectedElements().length).toBe(3); + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.up(); + }); + expect(API.getSelectedElements().length).toBe(1); + }, +); diff --git a/packages/excalidraw/tests/resize.test.tsx b/packages/excalidraw/tests/resize.test.tsx new file mode 100644 index 0000000..3ebd8cf --- /dev/null +++ b/packages/excalidraw/tests/resize.test.tsx @@ -0,0 +1,1344 @@ +import React from "react"; +import { render, unmountComponent } from "./test-utils"; +import { reseed } from "../random"; +import { UI, Keyboard, Pointer } from "./helpers/ui"; +import type { + ExcalidrawElbowArrowElement, + ExcalidrawFreeDrawElement, + ExcalidrawLinearElement, +} from "../element/types"; +import type { Bounds } from "../element/bounds"; +import { getElementPointsCoords } from "../element/bounds"; +import { Excalidraw } from "../index"; +import { API } from "./helpers/api"; +import { KEYS } from "../keys"; +import { isLinearElement } from "../element/typeChecks"; +import { LinearElementEditor } from "../element/linearElementEditor"; +import { arrayToMap } from "../utils"; +import type { LocalPoint } from "@excalidraw/math"; +import { pointFrom } from "@excalidraw/math"; +import { resizeSingleElement } from "../element/resizeElements"; +import { getSizeFromPoints } from "../points"; + +unmountComponent(); + +const { h } = window; +const mouse = new Pointer("mouse"); + +const getBoundsFromPoints = ( + element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement, +): Bounds => { + if (isLinearElement(element)) { + return getElementPointsCoords(element, element.points); + } + + const { x, y, points } = element; + const pointsX = points.map(([x]) => x); + const pointsY = points.map(([, y]) => y); + + return [ + Math.min(...pointsX) + x, + Math.min(...pointsY) + y, + Math.max(...pointsX) + x, + Math.max(...pointsY) + y, + ]; +}; + +beforeEach(async () => { + localStorage.clear(); + reseed(7); + mouse.reset(); + + await render(<Excalidraw handleKeyboardGlobally={true} />); + h.state.width = 1000; + h.state.height = 1000; + + // The bounds of hand-drawn linear elements may change after flipping, so + // removing this style for testing + UI.clickTool("arrow"); + UI.clickByTitle("Architect"); + UI.clickTool("selection"); +}); + +describe("generic element", () => { + // = rectangle/diamond/ellipse + + describe("resizes", () => { + it.each` + handle | move | size | xy + ${"n"} | ${[10, -27]} | ${[200, 127]} | ${[0, -27]} + ${"e"} | ${[67, -45]} | ${[267, 100]} | ${[0, 0]} + ${"s"} | ${[-50, -39]} | ${[200, 61]} | ${[0, 0]} + ${"w"} | ${[20, 90]} | ${[180, 100]} | ${[20, 0]} + ${"ne"} | ${[5, -33]} | ${[205, 133]} | ${[0, -33]} + ${"se"} | ${[-30, -81]} | ${[170, 19]} | ${[0, 0]} + ${"sw"} | ${[37, 25]} | ${[163, 125]} | ${[37, 0]} + ${"nw"} | ${[-34, 42]} | ${[234, 58]} | ${[-34, 42]} + `( + "with handle $handle", + async ({ handle, move, size: [width, height], xy: [x, y] }) => { + const rectangle = UI.createElement("rectangle", { + width: 200, + height: 100, + }); + UI.resize(rectangle, handle, move); + + expect(rectangle.x).toBeCloseTo(x); + expect(rectangle.y).toBeCloseTo(y); + expect(rectangle.width).toBeCloseTo(width); + expect(rectangle.height).toBeCloseTo(height); + expect(rectangle.angle).toBeCloseTo(0); + }, + ); + }); + + describe("flips while resizing", () => { + it.each` + handle | move | size | xy + ${"n"} | ${[15, 139]} | ${[200, 39]} | ${[0, 100]} + ${"e"} | ${[-245, 67]} | ${[45, 100]} | ${[-45, 0]} + ${"s"} | ${[-26, -210]} | ${[200, 110]} | ${[0, -110]} + ${"w"} | ${[241, 0]} | ${[41, 100]} | ${[200, 0]} + ${"ne"} | ${[-250, 125]} | ${[50, 25]} | ${[-50, 100]} + ${"se"} | ${[-283, -58]} | ${[83, 42]} | ${[-83, 0]} + ${"sw"} | ${[40, -123]} | ${[160, 23]} | ${[40, -23]} + ${"nw"} | ${[270, 133]} | ${[70, 33]} | ${[200, 100]} + `( + "with handle $handle", + async ({ handle, move, size: [width, height], xy: [x, y] }) => { + const rectangle = UI.createElement("rectangle", { + width: 200, + height: 100, + }); + UI.resize(rectangle, handle, move); + + expect(rectangle.x).toBeCloseTo(x); + expect(rectangle.y).toBeCloseTo(y); + expect(rectangle.width).toBeCloseTo(width); + expect(rectangle.height).toBeCloseTo(height); + expect(rectangle.angle).toBeCloseTo(0); + }, + ); + }); + + it("resizes with locked aspect ratio", async () => { + const rectangle = UI.createElement("rectangle", { + width: 200, + height: 100, + }); + UI.resize(rectangle, "se", [100, 10], { shift: true }); + + expect(rectangle.x).toBeCloseTo(0); + expect(rectangle.y).toBeCloseTo(0); + expect(rectangle.width).toBeCloseTo(300); + expect(rectangle.height).toBeCloseTo(150); + expect(rectangle.angle).toBeCloseTo(0); + + UI.resize(rectangle, "n", [30, 50], { shift: true }); + + expect(rectangle.x).toBeCloseTo(50); + expect(rectangle.y).toBeCloseTo(50); + expect(rectangle.width).toBeCloseTo(200); + expect(rectangle.height).toBeCloseTo(100); + expect(rectangle.angle).toBeCloseTo(0); + }); + + it("resizes from center", async () => { + const rectangle = UI.createElement("rectangle", { + width: 200, + height: 100, + }); + UI.resize(rectangle, "nw", [20, 10], { alt: true }); + + expect(rectangle.x).toBeCloseTo(20); + expect(rectangle.y).toBeCloseTo(10); + expect(rectangle.width).toBeCloseTo(160); + expect(rectangle.height).toBeCloseTo(80); + expect(rectangle.angle).toBeCloseTo(0); + + UI.resize(rectangle, "e", [15, 43], { alt: true }); + + expect(rectangle.x).toBeCloseTo(5); + expect(rectangle.y).toBeCloseTo(10); + expect(rectangle.width).toBeCloseTo(190); + expect(rectangle.height).toBeCloseTo(80); + expect(rectangle.angle).toBeCloseTo(0); + }); + + it("resizes with bound arrow", async () => { + const rectangle = UI.createElement("rectangle", { + width: 200, + height: 100, + }); + const arrow = UI.createElement("arrow", { + x: -30, + y: 50, + width: 28, + height: 5, + }); + + expect(arrow.endBinding?.elementId).toEqual(rectangle.id); + + UI.resize(rectangle, "e", [40, 0]); + + expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(30, 0); + + UI.resize(rectangle, "w", [50, 0]); + + expect(arrow.endBinding?.elementId).toEqual(rectangle.id); + expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(80, 0); + }); + + it("resizes with a label", async () => { + const rectangle = UI.createElement("rectangle", { + width: 200, + height: 100, + }); + const label = await UI.editText(rectangle, "Hello world"); + UI.resize(rectangle, "se", [50, 50]); + + expect(label.x + label.width / 2).toBeCloseTo( + rectangle.x + rectangle.width / 2, + ); + expect(label.y + label.height / 2).toBeCloseTo( + rectangle.y + rectangle.height / 2, + ); + expect(label.angle).toBeCloseTo(rectangle.angle); + expect(label.fontSize).toEqual(20); + + UI.resize(rectangle, "w", [190, 0]); + + expect(label.x + label.width / 2).toBeCloseTo( + rectangle.x + rectangle.width / 2, + ); + expect(label.y + label.height / 2).toBeCloseTo( + rectangle.y + rectangle.height / 2, + ); + expect(label.angle).toBeCloseTo(rectangle.angle); + expect(label.fontSize).toEqual(20); + }); +}); + +describe.each(["line", "freedraw"] as const)("%s element", (type) => { + const points: Record<typeof type, LocalPoint[]> = { + line: [ + pointFrom(0, 0), + pointFrom(60, -20), + pointFrom(20, 40), + pointFrom(-40, 0), + ], + freedraw: [ + pointFrom(0, 0), + pointFrom(-2.474600807561444, 41.021700699972), + pointFrom(3.6627956000014024, 47.84174560617245), + pointFrom(40.495224145598115, 47.15909710753482), + ], + }; + + it("resizes", async () => { + const element = UI.createElement("freedraw", { points: points.freedraw }); + const bounds = getBoundsFromPoints(element); + + UI.resize(element, "ne", [30, -60]); + const newBounds = getBoundsFromPoints(element); + + expect(newBounds[0]).toBeCloseTo(bounds[0]); + expect(newBounds[1]).toBeCloseTo(bounds[1] - 60); + expect(newBounds[2]).toBeCloseTo(bounds[2] + 30); + expect(newBounds[3]).toBeCloseTo(bounds[3]); + expect(element.angle).toBeCloseTo(0); + }); + + it("flips while resizing", async () => { + const element = UI.createElement("freedraw", { points: points.freedraw }); + const bounds = getBoundsFromPoints(element); + + UI.resize(element, "sw", [140, -80]); + const newBounds = getBoundsFromPoints(element); + + expect(newBounds[0]).toBeCloseTo(bounds[2]); + expect(newBounds[1]).toBeCloseTo(bounds[3] - 80); + expect(newBounds[2]).toBeCloseTo(bounds[0] + 140); + expect(newBounds[3]).toBeCloseTo(bounds[1]); + expect(element.angle).toBeCloseTo(0); + }); + + it("resizes with locked aspect ratio", async () => { + const element = UI.createElement("freedraw", { points: points.freedraw }); + const bounds = getBoundsFromPoints(element); + + UI.resize(element, "ne", [30, -60], { shift: true }); + const newBounds = getBoundsFromPoints(element); + const scale = 1 + 60 / (bounds[3] - bounds[1]); + + expect(newBounds[0]).toBeCloseTo(bounds[0]); + expect(newBounds[1]).toBeCloseTo(bounds[1] - 60); + expect(newBounds[2]).toBeCloseTo( + bounds[0] + (bounds[2] - bounds[0]) * scale, + ); + expect(newBounds[3]).toBeCloseTo(bounds[3]); + expect(element.angle).toBeCloseTo(0); + }); + + it("resizes from center", async () => { + const element = UI.createElement("freedraw", { points: points.freedraw }); + const bounds = getBoundsFromPoints(element); + + UI.resize(element, "nw", [-20, -30], { alt: true }); + const newBounds = getBoundsFromPoints(element); + + expect(newBounds[0]).toBeCloseTo(bounds[0] - 20); + expect(newBounds[1]).toBeCloseTo(bounds[1] - 30); + expect(newBounds[2]).toBeCloseTo(bounds[2] + 20); + expect(newBounds[3]).toBeCloseTo(bounds[3] + 30); + expect(element.angle).toBeCloseTo(0); + }); +}); + +describe("line element", () => { + const points: LocalPoint[] = [ + pointFrom(0, 0), + pointFrom(60, -20), + pointFrom(20, 40), + pointFrom(-40, 0), + ]; + + it("resizes", async () => { + UI.createElement("line", { points }); + + const element = h.elements[0] as ExcalidrawLinearElement; + + const { + x: prevX, + y: prevY, + width: prevWidth, + height: prevHeight, + } = element; + + const nextWidth = prevWidth + 30; + const nextHeight = prevHeight + 30; + + resizeSingleElement( + nextWidth, + nextHeight, + element, + element, + h.app.scene.getNonDeletedElementsMap(), + h.app.scene.getNonDeletedElementsMap(), + "ne", + ); + + expect(element.x).not.toBe(prevX); + expect(element.y).not.toBe(prevY); + + expect(element.width).toBe(nextWidth); + expect(element.height).toBe(nextHeight); + + expect(element.points[0]).toEqual([0, 0]); + + const { width, height } = getSizeFromPoints(element.points); + expect(width).toBe(element.width); + expect(height).toBe(element.height); + }); + + it("flips while resizing", async () => { + UI.createElement("line", { points }); + const element = h.elements[0] as ExcalidrawLinearElement; + + const { + width: prevWidth, + height: prevHeight, + points: prevPoints, + } = element; + + const nextWidth = prevWidth * -1; + const nextHeight = prevHeight * -1; + + resizeSingleElement( + nextWidth, + nextHeight, + element, + element, + h.app.scene.getNonDeletedElementsMap(), + h.app.scene.getNonDeletedElementsMap(), + "se", + ); + + expect(element.width).toBe(prevWidth); + expect(element.height).toBe(prevHeight); + + element.points.forEach((point, idx) => { + expect(point[0]).toBeCloseTo(prevPoints[idx][0] * -1); + expect(point[1]).toBeCloseTo(prevPoints[idx][1] * -1); + }); + }); + + it("resizes with locked aspect ratio", async () => { + UI.createElement("line", { points }); + const element = h.elements[0] as ExcalidrawLinearElement; + + const { width: prevWidth, height: prevHeight } = element; + + UI.resize(element, "ne", [30, -60], { shift: true }); + + const scaleHeight = element.width / prevWidth; + const scaleWidth = element.height / prevHeight; + + expect(scaleHeight).toBeCloseTo(scaleWidth); + }); + + it("resizes from center", async () => { + UI.createElement("line", { + points: [ + pointFrom(0, 0), + pointFrom(338.05644048727373, -180.4761618151104), + pointFrom(338.05644048727373, 180.4761618151104), + pointFrom(-338.05644048727373, 180.4761618151104), + pointFrom(-338.05644048727373, -180.4761618151104), + ], + }); + const element = h.elements[0] as ExcalidrawLinearElement; + + const { + x: prevX, + y: prevY, + width: prevWidth, + height: prevHeight, + } = element; + + const prevSmallestX = Math.min(...element.points.map((p) => p[0])); + const prevBiggestX = Math.max(...element.points.map((p) => p[0])); + + resizeSingleElement( + prevWidth + 20, + prevHeight, + element, + element, + h.app.scene.getNonDeletedElementsMap(), + h.app.scene.getNonDeletedElementsMap(), + "e", + { + shouldResizeFromCenter: true, + }, + ); + + expect(element.width).toBeCloseTo(prevWidth + 20); + expect(element.height).toBeCloseTo(prevHeight); + + expect(element.x).toBeCloseTo(prevX); + expect(element.y).toBeCloseTo(prevY); + + const smallestX = Math.min(...element.points.map((p) => p[0])); + const biggestX = Math.max(...element.points.map((p) => p[0])); + + expect(prevSmallestX - smallestX).toBeCloseTo(10); + expect(biggestX - prevBiggestX).toBeCloseTo(10); + }); +}); + +describe("arrow element", () => { + it("resizes with a label", async () => { + const arrow = UI.createElement("arrow", { + points: [ + pointFrom(0, 0), + pointFrom(40, 140), + pointFrom(80, 60), // label's anchor + pointFrom(180, 20), + pointFrom(200, 120), + ], + }); + const label = await UI.editText(arrow, "Hello"); + const elementsMap = arrayToMap(h.elements); + UI.resize(arrow, "se", [50, 30]); + let labelPos = LinearElementEditor.getBoundTextElementPosition( + arrow, + label, + elementsMap, + ); + + expect(labelPos.x + label.width / 2).toBeCloseTo( + arrow.x + arrow.points[2][0], + ); + expect(labelPos.y + label.height / 2).toBeCloseTo( + arrow.y + arrow.points[2][1], + ); + expect(label.angle).toBeCloseTo(0); + expect(label.fontSize).toEqual(20); + + UI.resize(arrow, "w", [20, 0]); + labelPos = LinearElementEditor.getBoundTextElementPosition( + arrow, + label, + elementsMap, + ); + + expect(labelPos.x + label.width / 2).toBeCloseTo( + arrow.x + arrow.points[2][0], + ); + expect(labelPos.y + label.height / 2).toBeCloseTo( + arrow.y + arrow.points[2][1], + ); + expect(label.angle).toBeCloseTo(0); + expect(label.fontSize).toEqual(20); + }); + + it("flips the fixed point binding on negative resize for single bindable", () => { + const rectangle = UI.createElement("rectangle", { + x: -100, + y: -75, + width: 95, + height: 100, + }); + UI.clickTool("arrow"); + UI.clickOnTestId("elbow-arrow"); + mouse.reset(); + mouse.moveTo(-5, 0); + mouse.click(); + mouse.moveTo(120, 200); + mouse.click(); + + const arrow = h.scene.getSelectedElements( + h.state, + )[0] as ExcalidrawElbowArrowElement; + + expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1); + expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75); + + UI.resize(rectangle, "se", [-200, -150]); + + expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1); + expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75); + }); + + it("flips the fixed point binding on negative resize for group selection", () => { + const rectangle = UI.createElement("rectangle", { + x: -100, + y: -75, + width: 95, + height: 100, + }); + UI.clickTool("arrow"); + UI.clickOnTestId("elbow-arrow"); + mouse.reset(); + mouse.moveTo(-5, 0); + mouse.click(); + mouse.moveTo(120, 200); + mouse.click(); + + const arrow = h.scene.getSelectedElements( + h.state, + )[0] as ExcalidrawElbowArrowElement; + + expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1); + expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75); + + UI.resize([rectangle, arrow], "nw", [300, 350]); + expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(0); + expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.25); + }); +}); + +describe("text element", () => { + it("resizes", async () => { + const text = UI.createElement("text"); + await UI.editText(text, "hello\nworld"); + const { width, height, fontSize } = text; + const scale = 40 / height + 1; + UI.resize(text, "se", [30, 40]); + + expect(text.x).toBeCloseTo(0); + expect(text.y).toBeCloseTo(0); + expect(text.width).toBeCloseTo(width * scale); + expect(text.height).toBeCloseTo(height * scale); + expect(text.angle).toBeCloseTo(0); + expect(text.fontSize).toBeCloseTo(fontSize * scale); + }); + + // TODO enable this test after adding single text element flipping + it.skip("flips while resizing", async () => { + const text = UI.createElement("text"); + await UI.editText(text, "hello\nworld"); + const { width, height, fontSize } = text; + const scale = 100 / width - 1; + UI.resize(text, "nw", [100, 80]); + + expect(text.x).toBeCloseTo(width); + expect(text.y).toBeCloseTo(height); + expect(text.width).toBeCloseTo(width * scale); + expect(text.height).toBeCloseTo(height * scale); + expect(text.angle).toBeCloseTo(0); + expect(text.fontSize).toBeCloseTo(fontSize * scale); + }); + + // TODO enable this test after fixing text resizing from center + it.skip("resizes from center", async () => { + const text = UI.createElement("text"); + await UI.editText(text, "hello\nworld"); + const { x, y, width, height, fontSize } = text; + const scale = 80 / height + 1; + UI.resize(text, "nw", [-25, -40], { alt: true }); + + expect(text.x).toBeCloseTo(x - ((scale - 1) * width) / 2); + expect(text.y).toBeCloseTo(y - 40); + expect(text.width).toBeCloseTo(width * scale); + expect(text.height).toBeCloseTo(height * scale); + expect(text.angle).toBeCloseTo(0); + expect(text.fontSize).toBeCloseTo(fontSize * scale); + }); + + it("resizes with bound arrow", async () => { + const text = UI.createElement("text"); + await UI.editText(text, "hello\nworld"); + const boundArrow = UI.createElement("arrow", { + x: -30, + y: 25, + width: 28, + height: 5, + }); + + expect(boundArrow.endBinding?.elementId).toEqual(text.id); + + UI.resize(text, "ne", [40, 0]); + + expect(boundArrow.width + boundArrow.endBinding!.gap).toBeCloseTo(30); + + const textWidth = text.width; + const scale = 20 / text.height; + UI.resize(text, "nw", [50, 20]); + + expect(boundArrow.endBinding?.elementId).toEqual(text.id); + expect(boundArrow.width + boundArrow.endBinding!.gap).toBeCloseTo( + 30 + textWidth * scale, + ); + }); + + it("updates font size via keyboard", async () => { + const text = UI.createElement("text"); + await UI.editText(text, "abc"); + const { fontSize } = text; + mouse.select(text); + + Keyboard.withModifierKeys({ shift: true, ctrl: true }, () => { + Keyboard.keyDown(KEYS.CHEVRON_RIGHT); + expect(text.fontSize).toBe(fontSize * 1.1); + + Keyboard.keyDown(KEYS.CHEVRON_LEFT); + expect(text.fontSize).toBe(fontSize); + }); + }); + + // text can be resized from sides + it("can be resized from e", async () => { + const text = UI.createElement("text"); + await UI.editText(text, "Excalidraw\nEditor"); + + const width = text.width; + const height = text.height; + + UI.resize(text, "e", [30, 0]); + expect(text.width).toBe(width + 30); + expect(text.height).toBe(height); + + UI.resize(text, "e", [-30, 0]); + expect(text.width).toBe(width); + expect(text.height).toBe(height); + }); + + it("can be resized from w", async () => { + const text = UI.createElement("text"); + await UI.editText(text, "Excalidraw\nEditor"); + + const width = text.width; + const height = text.height; + + UI.resize(text, "w", [-50, 0]); + expect(text.width).toBe(width + 50); + expect(text.height).toBe(height); + + UI.resize(text, "w", [50, 0]); + expect(text.width).toBe(width); + expect(text.height).toBe(height); + }); + + it("wraps when width is narrower than texts inside", async () => { + const text = UI.createElement("text"); + await UI.editText(text, "Excalidraw\nEditor"); + + const prevWidth = text.width; + const prevHeight = text.height; + const prevText = text.text; + + UI.resize(text, "w", [50, 0]); + expect(text.width).toBe(prevWidth - 50); + expect(text.height).toBeGreaterThan(prevHeight); + expect(text.text).not.toEqual(prevText); + expect(text.autoResize).toBe(false); + + UI.resize(text, "w", [-50, 0]); + expect(text.width).toBe(prevWidth); + expect(text.height).toEqual(prevHeight); + expect(text.text).toEqual(prevText); + expect(text.autoResize).toBe(false); + + UI.resize(text, "e", [-20, 0]); + expect(text.width).toBe(prevWidth - 20); + expect(text.height).toBeGreaterThan(prevHeight); + expect(text.text).not.toEqual(prevText); + expect(text.autoResize).toBe(false); + + UI.resize(text, "e", [20, 0]); + expect(text.width).toBe(prevWidth); + expect(text.height).toEqual(prevHeight); + expect(text.text).toEqual(prevText); + expect(text.autoResize).toBe(false); + }); + + it("keeps properties when wrapped", async () => { + const text = UI.createElement("text"); + await UI.editText(text, "Excalidraw\nEditor"); + + const alignment = text.textAlign; + const fontSize = text.fontSize; + const fontFamily = text.fontFamily; + + UI.resize(text, "e", [-60, 0]); + expect(text.textAlign).toBe(alignment); + expect(text.fontSize).toBe(fontSize); + expect(text.fontFamily).toBe(fontFamily); + expect(text.autoResize).toBe(false); + + UI.resize(text, "e", [60, 0]); + expect(text.textAlign).toBe(alignment); + expect(text.fontSize).toBe(fontSize); + expect(text.fontFamily).toBe(fontFamily); + expect(text.autoResize).toBe(false); + }); + + it("has a minimum width when wrapped", async () => { + const text = UI.createElement("text"); + await UI.editText(text, "Excalidraw\nEditor"); + + const width = text.width; + + UI.resize(text, "e", [-width, 0]); + expect(text.width).not.toEqual(0); + UI.resize(text, "e", [width - text.width, 0]); + expect(text.width).toEqual(width); + expect(text.autoResize).toBe(false); + + UI.resize(text, "w", [width, 0]); + expect(text.width).not.toEqual(0); + UI.resize(text, "w", [text.width - width, 0]); + expect(text.width).toEqual(width); + expect(text.autoResize).toBe(false); + }); +}); + +describe("image element", () => { + it("resizes", async () => { + const image = API.createElement({ type: "image", width: 100, height: 100 }); + API.setElements([image]); + UI.resize(image, "ne", [-20, -30]); + + expect(image.x).toBeCloseTo(0); + expect(image.y).toBeCloseTo(-30); + expect(image.width).toBeCloseTo(130); + expect(image.height).toBeCloseTo(130); + expect(image.angle).toBeCloseTo(0); + expect(image.scale).toEqual([1, 1]); + }); + + it("flips while resizing", async () => { + const image = API.createElement({ type: "image", width: 100, height: 100 }); + API.setElements([image]); + UI.resize(image, "sw", [150, -150]); + + expect(image.x).toBeCloseTo(100); + expect(image.y).toBeCloseTo(-50); + expect(image.width).toBeCloseTo(50); + expect(image.height).toBeCloseTo(50); + expect(image.angle).toBeCloseTo(0); + expect(image.scale).toEqual([-1, -1]); + }); + + it("resizes with locked/unlocked aspect ratio", async () => { + const image = API.createElement({ type: "image", width: 100, height: 100 }); + API.setElements([image]); + UI.resize(image, "ne", [30, -20]); + + expect(image.x).toBeCloseTo(0); + expect(image.y).toBeCloseTo(-30); + expect(image.width).toBeCloseTo(130); + expect(image.height).toBeCloseTo(130); + + UI.resize(image, "ne", [-30, 50], { shift: true }); + + expect(image.x).toBeCloseTo(0); + expect(image.y).toBeCloseTo(20); + expect(image.width).toBeCloseTo(100); + expect(image.height).toBeCloseTo(80); + }); + + it("resizes from center", async () => { + const image = API.createElement({ type: "image", width: 100, height: 100 }); + API.setElements([image]); + UI.resize(image, "nw", [25, 15], { alt: true }); + + expect(image.x).toBeCloseTo(15); + expect(image.y).toBeCloseTo(15); + expect(image.width).toBeCloseTo(70); + expect(image.height).toBeCloseTo(70); + expect(image.angle).toBeCloseTo(0); + expect(image.scale).toEqual([1, 1]); + }); + + it("resizes with bound arrow", async () => { + const image = API.createElement({ + type: "image", + width: 100, + height: 100, + }); + API.setElements([image]); + const arrow = UI.createElement("arrow", { + x: -30, + y: 50, + width: 28, + height: 5, + }); + + expect(arrow.endBinding?.elementId).toEqual(image.id); + + UI.resize(image, "ne", [40, 0]); + + expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(31, 0); + + const imageWidth = image.width; + const scale = 20 / image.height; + UI.resize(image, "nw", [50, 20]); + + expect(arrow.endBinding?.elementId).toEqual(image.id); + expect(Math.floor(arrow.width + arrow.endBinding!.gap)).toBeCloseTo( + 30 + imageWidth * scale, + 0, + ); + }); +}); + +describe("multiple selection", () => { + it("resizes with generic elements", async () => { + const rectangle = UI.createElement("rectangle", { + position: 0, + width: 100, + height: 80, + }); + const rectLabel = await UI.editText(rectangle, "hello\nworld"); + const diamond = UI.createElement("diamond", { + x: 140, + y: 40, + size: 80, + }); + const ellipse = UI.createElement("ellipse", { + x: 40, + y: 100, + width: 80, + height: 60, + }); + + const selectionWidth = 220; + const selectionHeight = 160; + const move = [50, 30] as [number, number]; + const scale = Math.max( + 1 + move[0] / selectionWidth, + 1 + move[1] / selectionHeight, + ); + + UI.resize([rectangle, diamond, ellipse], "se", move, { + shift: true, + }); + + expect(rectangle.x).toBeCloseTo(0); + expect(rectangle.y).toBeCloseTo(0); + expect(rectangle.width).toBeCloseTo(100 * scale); + expect(rectangle.height).toBeCloseTo(80 * scale); + expect(rectangle.angle).toEqual(0); + + expect(rectLabel.type).toEqual("text"); + expect(rectLabel.containerId).toEqual(rectangle.id); + expect(rectLabel.x + rectLabel.width / 2).toBeCloseTo( + rectangle.x + rectangle.width / 2, + ); + expect(rectLabel.y + rectLabel.height / 2).toBeCloseTo( + rectangle.y + rectangle.height / 2, + ); + expect(rectLabel.angle).toEqual(0); + expect(rectLabel.fontSize).toBeCloseTo(20 * scale, -1); + + expect(diamond.x).toBeCloseTo(140 * scale); + expect(diamond.y).toBeCloseTo(40 * scale); + expect(diamond.width).toBeCloseTo(80 * scale); + expect(diamond.height).toBeCloseTo(80 * scale); + expect(diamond.angle).toEqual(0); + + expect(ellipse.x).toBeCloseTo(40 * scale); + expect(ellipse.y).toBeCloseTo(100 * scale); + expect(ellipse.width).toBeCloseTo(80 * scale); + expect(ellipse.height).toBeCloseTo(60 * scale); + expect(ellipse.angle).toEqual(0); + }); + + it("resizes with linear elements > 2 points", async () => { + UI.clickTool("line"); + UI.clickByTitle("Sharp"); + + const line = UI.createElement("line", { + x: 60, + y: 40, + points: [ + pointFrom(0, 0), + pointFrom(-40, 40), + pointFrom(-60, 0), + pointFrom(0, -40), + pointFrom(40, 20), + pointFrom(0, 40), + ], + }); + const freedraw = UI.createElement("freedraw", { + x: 63.56072661326618, + y: 100, + points: [ + pointFrom(0, 0), + pointFrom(-43.56072661326618, 18.15048126846341), + pointFrom(-43.56072661326618, 29.041198460587566), + pointFrom(-38.115368017204105, 42.652452795512204), + pointFrom(-19.964886748740696, 66.24829266003775), + pointFrom(19.056612930986716, 77.1390098521619), + ], + }); + + const selectionWidth = 100; + const selectionHeight = 177.1390098521619; + const move = [-25, -25] as [number, number]; + const scale = Math.max( + 1 + move[0] / selectionWidth, + 1 + move[1] / selectionHeight, + ); + + UI.resize([line, freedraw], "se", move, { + shift: true, + }); + + expect(line.x).toBeCloseTo(60 * scale); + expect(line.y).toBeCloseTo(40 * scale); + expect(line.width).toBeCloseTo(100 * scale); + expect(line.height).toBeCloseTo(80 * scale); + expect(line.angle).toEqual(0); + + expect(freedraw.x).toBeCloseTo(63.56072661326618 * scale); + expect(freedraw.y).toBeCloseTo(100 * scale); + expect(freedraw.width).toBeCloseTo(62.6173395442529 * scale); + expect(freedraw.height).toBeCloseTo(77.1390098521619 * scale); + expect(freedraw.angle).toEqual(0); + }); + + it("resizes with 2-point lines", async () => { + const horizLine = UI.createElement("line", { + position: 0, + width: 120, + height: 0, + }); + const vertLine = UI.createElement("line", { + x: 0, + y: 20, + width: 0, + height: 80, + }); + const diagLine = UI.createElement("line", { + position: 40, + size: 60, + }); + + const selectionWidth = 120; + const selectionHeight = 100; + const move = [40, 40] as [number, number]; + const scale = Math.max( + 1 - move[0] / selectionWidth, + 1 - move[1] / selectionHeight, + ); + + UI.resize([horizLine, vertLine, diagLine], "nw", move, { + shift: true, + }); + + expect(horizLine.x).toBeCloseTo(selectionWidth * (1 - scale)); + expect(horizLine.y).toBeCloseTo(selectionHeight * (1 - scale)); + expect(horizLine.width).toBeCloseTo(120 * scale); + expect(horizLine.height).toBeCloseTo(0); + expect(horizLine.angle).toEqual(0); + + expect(vertLine.x).toBeCloseTo(selectionWidth * (1 - scale)); + expect(vertLine.y).toBeCloseTo((selectionHeight - 20) * (1 - scale) + 20); + expect(vertLine.width).toBeCloseTo(0); + expect(vertLine.height).toBeCloseTo(80 * scale); + expect(vertLine.angle).toEqual(0); + + expect(diagLine.x).toBeCloseTo((selectionWidth - 40) * (1 - scale) + 40); + expect(diagLine.y).toBeCloseTo((selectionHeight - 40) * (1 - scale) + 40); + expect(diagLine.width).toBeCloseTo(60 * scale); + expect(diagLine.height).toBeCloseTo(60 * scale); + expect(diagLine.angle).toEqual(0); + }); + + it("resizes with bound arrows", async () => { + const rectangle = UI.createElement("rectangle", { + position: 0, + size: 100, + }); + const leftBoundArrow = UI.createElement("arrow", { + x: -110, + y: 50, + width: 100, + height: 0, + }); + + const rightBoundArrow = UI.createElement("arrow", { + x: 210, + y: 50, + width: -100, + height: 0, + }); + + const selectionWidth = 210; + const selectionHeight = 100; + const move = [40, 40] as [number, number]; + const scale = Math.max( + 1 - move[0] / selectionWidth, + 1 - move[1] / selectionHeight, + ); + const leftArrowBinding = { ...leftBoundArrow.endBinding }; + const rightArrowBinding = { ...rightBoundArrow.endBinding }; + delete rightArrowBinding.gap; + + UI.resize([rectangle, rightBoundArrow], "nw", move, { + shift: true, + }); + + expect(leftBoundArrow.x).toBeCloseTo(-110); + expect(leftBoundArrow.y).toBeCloseTo(50); + expect(leftBoundArrow.width).toBeCloseTo(143, 0); + expect(leftBoundArrow.height).toBeCloseTo(7, 0); + expect(leftBoundArrow.angle).toEqual(0); + expect(leftBoundArrow.startBinding).toBeNull(); + expect(leftBoundArrow.endBinding?.gap).toBeCloseTo(10); + expect(leftBoundArrow.endBinding?.elementId).toBe( + leftArrowBinding.elementId, + ); + expect(leftBoundArrow.endBinding?.focus).toBe(leftArrowBinding.focus); + + expect(rightBoundArrow.x).toBeCloseTo(210); + expect(rightBoundArrow.y).toBeCloseTo( + (selectionHeight - 50) * (1 - scale) + 50, + ); + expect(rightBoundArrow.width).toBeCloseTo(100 * scale); + expect(rightBoundArrow.height).toBeCloseTo(0); + expect(rightBoundArrow.angle).toEqual(0); + expect(rightBoundArrow.startBinding).toBeNull(); + expect(rightBoundArrow.endBinding?.gap).toBeCloseTo(8.0952); + expect(rightBoundArrow.endBinding?.elementId).toBe( + rightArrowBinding.elementId, + ); + expect(rightBoundArrow.endBinding?.focus).toBeCloseTo( + rightArrowBinding.focus!, + ); + }); + + it("resizes with labeled arrows", async () => { + const topArrow = UI.createElement("arrow", { + x: 0, + y: 20, + width: 220, + height: 0, + }); + const topArrowLabel = await UI.editText(topArrow.get(), "lorem ipsum"); + + UI.clickTool("text"); + UI.clickByTitle("Large"); + const bottomArrow = UI.createElement("arrow", { + x: 0, + y: 80, + width: 220, + height: 0, + }); + const bottomArrowLabel = await UI.editText( + bottomArrow.get(), + "dolor\nsit amet", + ); + + const selectionWidth = 220; + const selectionTop = 20 - topArrowLabel.height / 2; + const move = [80, 0] as [number, number]; + const scale = move[0] / selectionWidth + 1; + const elementsMap = arrayToMap(h.elements); + UI.resize([topArrow.get(), bottomArrow.get()], "se", move, { + shift: true, + }); + const topArrowLabelPos = LinearElementEditor.getBoundTextElementPosition( + topArrow, + topArrowLabel, + elementsMap, + ); + const bottomArrowLabelPos = LinearElementEditor.getBoundTextElementPosition( + bottomArrow, + bottomArrowLabel, + elementsMap, + ); + + expect(topArrow.x).toBeCloseTo(0); + expect(topArrow.y).toBeCloseTo(selectionTop + (20 - selectionTop) * scale); + expect(topArrow.width).toBeCloseTo(300); + expect(topArrow.points).toEqual([ + [0, 0], + [300, 0], + ]); + + expect(topArrowLabelPos.x + topArrowLabel.width / 2).toBeCloseTo( + topArrow.width / 2, + ); + expect(topArrowLabelPos.y + topArrowLabel.height / 2).toBeCloseTo( + topArrow.y, + ); + expect(topArrowLabel.fontSize).toBeCloseTo(20 * scale); + + expect(bottomArrow.x).toBeCloseTo(0); + expect(bottomArrow.y).toBeCloseTo( + selectionTop + (80 - selectionTop) * scale, + ); + expect(bottomArrow.width).toBeCloseTo(300); + expect(topArrow.points).toEqual([ + [0, 0], + [300, 0], + ]); + + expect(bottomArrowLabelPos.x + bottomArrowLabel.width / 2).toBeCloseTo( + bottomArrow.width / 2, + ); + expect(bottomArrowLabelPos.y + bottomArrowLabel.height / 2).toBeCloseTo( + bottomArrow.y, + ); + expect(bottomArrowLabel.fontSize).toBeCloseTo(28 * scale); + }); + + it("resizes with text elements", async () => { + const topText = UI.createElement("text", { position: 0 }); + await UI.editText(topText, "lorem ipsum"); + + UI.clickTool("text"); + UI.clickByTitle("Large"); + const bottomText = UI.createElement("text", { position: 40 }); + await UI.editText(bottomText, "dolor\nsit amet"); + + const selectionWidth = 40 + bottomText.width; + const selectionHeight = 40 + bottomText.height; + const move = [30, -40] as [number, number]; + const scale = Math.max( + 1 + move[0] / selectionWidth, + 1 - move[1] / selectionHeight, + ); + + UI.resize([topText, bottomText], "ne", move, { shift: true }); + + expect(topText.x).toBeCloseTo(0); + expect(topText.y).toBeCloseTo(-selectionHeight * (scale - 1)); + expect(topText.fontSize).toBeCloseTo(20 * scale); + expect(topText.angle).toEqual(0); + + expect(bottomText.x).toBeCloseTo(40 * scale); + expect(bottomText.y).toBeCloseTo(40 - (selectionHeight - 40) * (scale - 1)); + expect(bottomText.fontSize).toBeCloseTo(28 * scale); + expect(bottomText.angle).toEqual(0); + }); + + it("resizes with images (proportional)", () => { + const topImage = API.createElement({ + type: "image", + x: 0, + y: 0, + width: 200, + height: 100, + }); + const bottomImage = API.createElement({ + type: "image", + x: 30, + y: 150, + width: 120, + height: 80, + }); + API.setElements([topImage, bottomImage]); + + const selectionWidth = 200; + const selectionHeight = 230; + const move = [-50, -50] as [number, number]; + const scale = Math.max( + 1 + move[0] / selectionWidth, + 1 + move[1] / selectionHeight, + ); + + UI.resize([topImage, bottomImage], "se", move); + + expect(topImage.x).toBeCloseTo(0); + expect(topImage.y).toBeCloseTo(0); + expect(topImage.width).toBeCloseTo(200 * scale); + expect(topImage.height).toBeCloseTo(100 * scale); + expect(topImage.angle).toEqual(0); + expect(topImage.scale).toEqual([1, 1]); + + expect(bottomImage.x).toBeCloseTo(30 * scale); + expect(bottomImage.y).toBeCloseTo(150 * scale); + expect(bottomImage.width).toBeCloseTo(120 * scale); + expect(bottomImage.height).toBeCloseTo(80 * scale); + expect(bottomImage.angle).toEqual(0); + expect(bottomImage.scale).toEqual([1, 1]); + }); + + it("resizes from center", () => { + const rectangle = UI.createElement("rectangle", { + x: -200, + y: -140, + width: 120, + height: 100, + }); + const ellipse = UI.createElement("ellipse", { + position: 60, + width: 140, + height: 80, + }); + + const selectionWidth = 400; + const selectionHeight = 280; + const move = [-80, -80] as [number, number]; + const scale = Math.max( + 1 + (2 * move[0]) / selectionWidth, + 1 + (2 * move[1]) / selectionHeight, + ); + + UI.resize([rectangle, ellipse], "se", move, { shift: true, alt: true }); + + expect(rectangle.x).toBeCloseTo(-200 * scale); + expect(rectangle.y).toBeCloseTo(-140 * scale); + expect(rectangle.width).toBeCloseTo(120 * scale); + expect(rectangle.height).toBeCloseTo(100 * scale); + expect(rectangle.angle).toEqual(0); + + expect(ellipse.x).toBeCloseTo(60 * scale); + expect(ellipse.y).toBeCloseTo(60 * scale); + expect(ellipse.width).toBeCloseTo(140 * scale); + expect(ellipse.height).toBeCloseTo(80 * scale); + expect(ellipse.angle).toEqual(0); + }); + + it("flips while resizing", async () => { + const image = API.createElement({ + type: "image", + x: 60, + y: 100, + width: 100, + height: 100, + angle: (Math.PI * 7) / 6, + }); + API.setElements([image]); + + const line = UI.createElement("line", { + x: 60, + y: 0, + points: [ + pointFrom(0, 0), + pointFrom(-40, 40), + pointFrom(-20, 60), + pointFrom(20, 20), + pointFrom(40, 40), + pointFrom(-20, 100), + pointFrom(-60, 60), + ], + }); + + const rectangle = UI.createElement("rectangle", { + x: 180, + y: 60, + width: 160, + height: 80, + angle: Math.PI / 6, + }); + const rectLabel = await UI.editText(rectangle, "hello\nworld"); + + const boundArrow = UI.createElement("arrow", { + x: 380, + y: 240, + width: -60, + height: -80, + }); + const arrowLabel = await UI.editText(boundArrow, "test"); + + const selectionWidth = 380; + const move = [-800, 0] as [number, number]; + const scaleX = move[0] / selectionWidth + 1; + const scaleY = -scaleX; + const lineOrigBounds = getBoundsFromPoints(line); + const elementsMap = arrayToMap(h.elements); + UI.resize([line, image, rectangle, boundArrow], "se", move, { + shift: true, + }); + const lineNewBounds = getBoundsFromPoints(line); + const arrowLabelPos = LinearElementEditor.getBoundTextElementPosition( + boundArrow, + arrowLabel, + elementsMap, + ); + + expect(line.x).toBeCloseTo(60 * scaleX); + expect(line.y).toBeCloseTo(0); + expect(lineNewBounds[0]).toBeCloseTo( + (lineOrigBounds[2] - lineOrigBounds[0]) * scaleX, + ); + expect(lineNewBounds[1]).toBeCloseTo(0); + expect(lineNewBounds[3]).toBeCloseTo( + (lineOrigBounds[3] - lineOrigBounds[1]) * scaleY, + ); + expect(lineNewBounds[2]).toBeCloseTo(0); + expect(line.angle).toEqual(0); + + expect(image.x).toBeCloseTo((60 + 100) * scaleX); + expect(image.y).toBeCloseTo(100 * scaleY); + expect(image.width).toBeCloseTo(100 * -scaleX); + expect(image.height).toBeCloseTo(100 * scaleY); + expect(image.angle).toBeCloseTo((Math.PI * 5) / 6); + expect(image.scale).toEqual([-1, 1]); + + expect(rectangle.x).toBeCloseTo((180 + 160) * scaleX); + expect(rectangle.y).toBeCloseTo(60 * scaleY); + expect(rectangle.width).toBeCloseTo(160 * -scaleX); + expect(rectangle.height).toBeCloseTo(80 * scaleY); + expect(rectangle.angle).toEqual((Math.PI * 11) / 6); + + expect(rectLabel.x + rectLabel.width / 2).toBeCloseTo( + rectangle.x + rectangle.width / 2, + ); + expect(rectLabel.y + rectLabel.height / 2).toBeCloseTo( + rectangle.y + rectangle.height / 2, + ); + expect(rectLabel.angle).toBeCloseTo(rectangle.angle); + expect(rectLabel.fontSize).toBeCloseTo(20 * scaleY); + + expect(boundArrow.x).toBeCloseTo(380 * scaleX); + expect(boundArrow.y).toBeCloseTo(240 * scaleY); + expect(boundArrow.points[1][0]).toBeCloseTo(-60 * scaleX); + expect(boundArrow.points[1][1]).toBeCloseTo(-80 * scaleY); + + expect(arrowLabelPos.x + arrowLabel.width / 2).toBeCloseTo( + boundArrow.x + boundArrow.points[1][0] / 2, + ); + expect(arrowLabelPos.y + arrowLabel.height / 2).toBeCloseTo( + boundArrow.y + boundArrow.points[1][1] / 2, + ); + expect(arrowLabel.angle).toEqual(0); + expect(arrowLabel.fontSize).toBeCloseTo(20 * scaleY); + }); +}); diff --git a/packages/excalidraw/tests/rotate.test.tsx b/packages/excalidraw/tests/rotate.test.tsx new file mode 100644 index 0000000..367313e --- /dev/null +++ b/packages/excalidraw/tests/rotate.test.tsx @@ -0,0 +1,81 @@ +import React from "react"; +import { render, unmountComponent } from "./test-utils"; +import { reseed } from "../random"; +import { UI } from "./helpers/ui"; +import { Excalidraw } from "../index"; +import { expect } from "vitest"; + +unmountComponent(); + +beforeEach(() => { + localStorage.clear(); + reseed(7); +}); + +test("unselected bound arrow updates when rotating its target element", async () => { + await render(<Excalidraw />); + const rectangle = UI.createElement("rectangle", { + width: 200, + height: 100, + }); + const arrow = UI.createElement("arrow", { + x: -80, + y: 50, + width: 70, + height: 0, + }); + + expect(arrow.endBinding?.elementId).toEqual(rectangle.id); + + UI.rotate(rectangle, [60, 36], { shift: true }); + + expect(arrow.endBinding?.elementId).toEqual(rectangle.id); + expect(arrow.x).toBeCloseTo(-80); + expect(arrow.y).toBeCloseTo(50); + expect(arrow.width).toBeCloseTo(116.7, 1); + expect(arrow.height).toBeCloseTo(0); +}); + +test("unselected bound arrows update when rotating their target elements", async () => { + await render(<Excalidraw />); + const ellipse = UI.createElement("ellipse", { + x: 0, + y: 80, + width: 300, + height: 120, + }); + const ellipseArrow = UI.createElement("arrow", { + position: 0, + width: 40, + height: 80, + }); + const text = UI.createElement("text", { + position: 220, + }); + await UI.editText(text, "test"); + const textArrow = UI.createElement("arrow", { + x: 360, + y: 300, + width: -100, + height: -40, + }); + + expect(ellipseArrow.endBinding?.elementId).toEqual(ellipse.id); + expect(textArrow.endBinding?.elementId).toEqual(text.id); + + UI.rotate([ellipse, text], [-82, 23], { shift: true }); + + expect(ellipseArrow.endBinding?.elementId).toEqual(ellipse.id); + expect(ellipseArrow.x).toEqual(0); + expect(ellipseArrow.y).toEqual(0); + expect(ellipseArrow.points[0]).toEqual([0, 0]); + expect(ellipseArrow.points[1][0]).toBeCloseTo(48.98, 1); + expect(ellipseArrow.points[1][1]).toBeCloseTo(125.79, 1); + + expect(textArrow.endBinding?.elementId).toEqual(text.id); + expect(textArrow.x).toEqual(360); + expect(textArrow.y).toEqual(300); + expect(textArrow.points[0]).toEqual([0, 0]); + expect(textArrow.points[1][0]).toBeCloseTo(-94, 0); + expect(textArrow.points[1][1]).toBeCloseTo(-116.1, 0); +}); diff --git a/packages/excalidraw/tests/scene/__snapshots__/export.test.ts.snap b/packages/excalidraw/tests/scene/__snapshots__/export.test.ts.snap new file mode 100644 index 0000000..fdf514d --- /dev/null +++ b/packages/excalidraw/tests/scene/__snapshots__/export.test.ts.snap @@ -0,0 +1,251 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`exportToSvg > with a CJK font 1`] = ` +<svg + height="120" + version="1.1" + viewBox="0 0 120 120" + width="120" + xmlns="http://www.w3.org/2000/svg" +> + <!-- svg-source:excalidraw --> + <metadata /> + <defs> + <style + class="style-fonts" + > + + @font-face { font-family: Xiaolai; src: url(data:font/woff2;base64,d09GMgABAAAAAAM0ABEAAAAACzgAAALeAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhQbHhweBmAABAgKCY4uERAKACoLBAABNgIkAwQEIAWCXgcgDBQbNQrIHoVxw13yP14ojTKEaM1QThFEa2X1zD6gAgRNQZKAQm5cJIKwaMzJFzIynkDG/ty7111blluK97oujDnzP3PlZLtT5hYmqg9h0LjgDmEQdkzh6nAwbBu0VTWm4+n/f+8edt9rio0t1vvn35ZoInvhWJaOCTQpDCjgNF7W8TExw4AT7w+6mxSsux9EoBMQCCHQbkSA59iJ+XUoUkEpMLA750+RBUqSEHWibcATRW0pmuVwhdieuzy1BWPlPLkuNci17O28ePvzQmpKB5iQD5r0oB/ZAFbOMuXS5ceIN8RRHjO8o08U+dbNZgZXEB0tbdxeZWiFwSXRzPAKujbPuquqktBMmNNxM+FiOhK31Wzibz0pYa60ovVd1mj9l4U9RH5nLEE00MzIFqzqMVfQxcxilvGTe/43M7oyhzbPLasVZe9Jvz0kyS8EOnZ/Wdl96RdEzYxd0p9UbnX5dYHn2RJJCRvE15iflMm8qOQqBewzxm+myVrJgSoOFdnszjzrcNxbbm/eqxR+dEylqBTkFySbmbgyt6YcKTSC4tLneiYoBE4GhcnOVwlaw+siDDCpobJkhzhC9/7Le/M2wuFc8JjrfJ+gA3woIoKYRlJNAJ8iMi6HguXOLwm6wtciGmhpI9UO8D1i4EaY2Oj8maAX/Cpig51uUj2A/fkdALJWyQBPKTIIkLgulfMG8kj9/ancv1vF279FHfknfP6YqwJ8WZ69K1/LYvWX74DtwwtC/3iu+j+gPvi7TGAWEOuR/phvTm+daFeLq3yiasOS98IUejybkm4NU7bGTFbDpiZ3lcWbzfvAZVrEvh0R2ppJmxKOHNP27UGdWrRrN0hKiEnYFYUt1KWJlTq0BT6YdKe3Tkw6gvFIBvsl1L5Bui0odehpxDHZrEuzKO3kEknkiw3w+CospZxOnHu0zM91+w5c2mopaSeTDxfdiuYpU7b4uN5KrU29YVgOIAMHpAJYBg==); } + @font-face { font-family: Xiaolai; src: url(data:font/woff2;base64,d09GMgABAAAAAAWEABEAAAAADgAAAAUpAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbHhweBmAAPAgKCY4uERAKhQCEIgsIAAE2AiQDDAQgBYJeByAMFBtvDMiuB+xgTH5L2Q7sYnV6IwxVtajxRDxR7GtKihI88f71nrnzBbz9qgQnXapNl3QSW7KD0Qn/q0u/YgfdhsgHcoAPA4wq0oTke+0kbwFCl1XCjWHrjLQwt/2IeoekLpWIJyK1kTJTp2btReFcdLUIb1qEJsAJYAdeLELufzj/wnkkq+3//2UO7p2/rYKVLrZ/z7wzK75CvVAtWJMOUoGWMFYxHMCKoYg29Ix7RvUqDVb+hSCAB+gZhCd4y40I7zdBldVldUDEAeA/gIEbVK/JAQcDZyB27DwCm7hsGHFCxQVRwjs19hY2gut26VfDrP4HqJ33KI9sfncXxtH8A4jBHjCAAgFwCHTwpMCpLZjFiWuk0CLtFzW+R9eRAO9DHgHIamDAtfYeR2gRYej/jnBEngMOHRk2MIAtjPM356lolY0BYjGA0yQDA9TCBSoXWlDTFEwwxbR4HcUFeNPqdSofiuKtT1pPiQmr8OndjFX49kpqFhGdE+sJSTEwwLZg/D5KcAHesphtiZPVv/0RwK1S+AxcWFVTdZMqV4pqVh4RJQnnq1dbS+rVSolEiIEBccl40ju02f+6IOtEGilRG+RrWZx0CaWVWrAED8ybF06DibWTIaCarCaD2dtcaD5DclX7WA6WRFQLlrwaEemXzzMwoK3S2qb8PFGSiIEBY5UyoD5XQ3EsOMbKzwtkgS5KkCCLkk5RZXEQSf5b8N68StA4HQbT5TcFskW3JThwRUepHuB9QsCFiFgofyyQE3oqIUMuZkotAF8SKq5Gw2r5W4Hc0XsJA0ZxUOoEwucRAGo/bAgB/YcCRHC6xxV4CnHANXvwGg44+oV7ij56vY/HWprzFBHb43fPfel3uJOtpr7jvsNvhTgjKD5tQ131bUK484kBBHSOVtbJ9U/gKQGtGxRkO3HsUNDoqT0hnQBSn1zsv4aNI5sQKkzt2e2ujG2MlX3a6p+IeHLIqbX0ac4Xoc3RM9EXBCX5zm22Ho9dp8Pan94R4oDQISjarvr6fGUK4NbQJfcGtNbRp5LtadgHbZW4xDHtUIBW6Jl/i6z4yI8n3M+3qsIb+oEz/oOMwf8g4ELvLfoCspWMu9C40Umgjber1ut+Fwzk5P/4sRzZoCbtVsqt/512alkRf8kNjW9NavBedGRbPOrTV0LBh5Qnq12ccmt+tfn5sxT1aHNObe6evLg1J8HjxIMl9qbOFrs2TRuZ9bVvs/fTMVlo031fPXkFjz35aIOHZ/1RlCtcrd/FNq4XxnxK69UEemhm+IMPZTbv60Fxn/Biy3ooevyTMm4jd7/I4JXsd9rBzxoBZQ7HZ05wsyU02CEz1/tvO/b+/ekqQewcotm7FWGcV1Anxy8PLt0ef3naCp13bGwAH9Qg0M+l0nwCbsbdrXmI5HzjcNsY90fpP9mfHfzcpokzNPlqK7udvZwLchMhC4D8/+kBJSKARkTurn+ebrrZvlLu+g5wb/uXBAB4ma5r/v9Kjf3BTARwwQAg8O+rOXwd0P/TDhBcg3JKD8KuB+CEHjUfKTVtmQMY0GPO9INBHFyBNF4WPiGA2z6DF0d8Sgb0rMOwz5FJsPNO5jWAXsa0WTKjzZgsXgr02bBlzJIFSmIxYsFCKnn06QadOR10l5I4YsBYMcNOKgXTj+/wsqFPO8Oyo8SyQdmMO1YSGnmcXC8OA2sYs7tEUcmvZuH5GtgZsp946WZZnLuWrDiHjhkyYnd26qJ3YsooEaQIBjfW/9AzPEKVLhFAA9OA0gEAAAA=); } + @font-face { font-family: Xiaolai; src: url(data:font/woff2;base64,d09GMgABAAAAAAQsABEAAAAADEwAAAPSAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbHhweBmAANAgKCY4uERAKgViBVwsGAAE2AiQDCAQgBYJeByAMFBsYC8ieg3HjfHBMJtOTaa1WwvFxCyp4oLryX1X1bABVB5wOuYA68sScXDuQb71hhTjC+Lj9PwPnXGW8AK+VdNLzKQrEDnoe3m7v7ABvu521BRHGrY9RSFFEbWkUtjOs/wv9ydv3a3oyez8oIxywcfmZeyFjYwyzAmzZAQqFwrRK1jKOB3a1mozsDbqbFKy/bwkCKA8ACEAiEAigLPXVJ+IT0/MpjQUQiQAMnK7ULfAGChIJiCmJwTBJeYoUUSjhAh/BgvRIKiL6M/RB4SmRKVBTWLVZYufLhyAD8n9ACwASUEB1CoH0FhqWQgzdMX+s7qSs3gxQHX/JCyA4G5js4hIhVrhGRDLAtJ2CQQ0Z7BjAoNYJoXhfDHUMUNAAtrYdAxzUib5qlphb3MTVnvZSRno6UY8dNtK3muFw671GeW477bOJkPbZhu1Hu/WcJaNct6djQLZgVslzYV2PW8zGhclL/ucY0GCa9lXz7OKcYj88redHx7v1bFsn+Bezw78YX23XdQzYJcNJa2Go9nWBl7HWJEdskM5jYdJ0Pa9SamLDOd7V9pq41qZGQFXRVcPs6apZghHh7PZ+uIldD5Wa2OIgt57txjsGMpiWV5wQD3S2m6Y+NzCEiuBoMIz2PiuQOTovQYAoChpJEc3hLvy/eG9eJiicCo2p3tcFskE3JViwRUuuDuBdgsf5CJjv/VAgR/RYQoJUTOSaAT4nFFyJipXerwVyQ28ldOjFTq4D4DrsAcq+naQ2EYECQAJgZelUhiFAZV1Mdq0+eVmh96DwFEtqbW1xsNzSh+W+4oBm+7n0/N4UsJfjCffF5Qg/Za+FheHtu9f367y5DPsKvEnjI6xXcP+4Wv/7jmRd9LfKs1IVK06r+FeDWnXbFJTITX3zlKpSbfTpOQ2qJJbbIy+oGZ2iR8iSoTOFnaJa1vs32DFTXoGd0WUSOqi1fJVagXIzZ385YCz7Utr84eHv16Lm6v3prigqlIn69YTQZ7Tq8PvW1nOl3v+ULqc+AcCjvb+mAPC2TbomMq2sX00FSiMBEPBLKVn/DGj/bQMCN4YmUTvYs0kpvwItMBP5jjLmnlaK/hTCKgO09qSwCqq7bpVUxFgVbXnLrWFrgDmqpqPMx0GxkGEmm2CYkDxxCo0yzXQhk02iddJWBx30Em+UEUaZaDgc1zoLskZH7YMQzCZOH58hzjSYHpZhckPrsEG+fdDoqGsjrZOtdeYYLmTGJbKar68nT+fCxhgzJ645bVmYGyebYq5zyBhjzVh++KLbaumKJSmicINFpmivaxSThAcFVAVoAwAA); } + @font-face { font-family: Xiaolai; src: url(data:font/woff2;base64,d09GMgABAAAAAARMABEAAAAADKAAAAPxAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbHhweBmAANAgKCY4uERAKgiyCFgsGAAE2AiQDCAQgBYJeByAMFBtXC1GUDE6U7OeBbetIH0CFoo4NofKHy4LoNHigvN43M9kPMF9QRZyaQluQR/FEMIiD2+3koQQeGDVsG5FWxPiMp5vLGQs7ueoeHt0+cyXMJTxy/kDp4IIX/P/3q49z/5sUixabN+ffKdER6aaOzdQE2SVcKq4EaWySQtw/qldpsPLvfAjACQAQgETAFOBA0HciKqsbtMAODcA0AQaOVuqimIsFJBIQQxL5PjJjN0EKG5RwASvBnAZb05rSb4DHqCHmEETnYG4XTax5cAOkRf4H9C09ZLECvChEYqihA00gGpcamCbOJe9evWBhWOC9SgUkNhSjCX2F+LAK865A5X8Y4M5Lq8BEAH9xIrpYBSUqMB6tghMhEBUV9MkwhHpYtw/1AM6bvWhRiav1RoewbQmEjy4g/IRSyllnwYG5bBWyBdGSYVuYg+ticnWR0H/ErWhiA0RFd6lHYqislDrOIoKXY10KVrssMluFXdKVFG+X1HWBp1g8iYoNQgPmIgUOrZWkQTgjDPMwgq0dVUBFpRll9mQV5VUY3faaYTCLkgbFXpxFdlaRiY0JSe+gDDmkfa59hEhQOhhEt7zJoC28zcEBl3UknqCUwpTxv3hv2kcEVAgRCS0fM+gEn3LIIGczvhaALhEVVUNDasu3DLrD9xwGGNmBrxOgZ8RCrbCR1fI7gz7wJ4cLbvbi6wOM3wd0AKCwQ+JHmCYKAAmABiigDxiHJnAXleQT1doavdlndbvrQqzxDWx5zXsRRuZGB7I9Ni3DcMh2MNb5y3Ntw1gauevqQKuPtdV1yw3HJY4zNo27RYsJ11jYyro9YHnI4nYtrIt9FovxB3NqxodcT0K+I76C9zbZwmi5DmPlyTZhXmM7/bJ4ynaxHmLU2onKEGdPueuR6dtG1qiG6lSAm4yu0ut30zsPk8Nkup1l3rT20xyydjpO8rWpcRjchIqe8ZJNjo5OiZANorRbywtONfCqnj2t6cR/V2ux/zLjtkugPixM97LpXJ40abubq3VWIANOVRSFuWf+me5XBcV937LnS2e78Judo3oJcHPD+0iAJwlR882n5qr9VUMBO+QC7x8nqX0bkP5/ux2B60F91BI6s76Ub4jBRMTSpdc9ypZ2FEKzB+LdLjQCL8c1Ehf2aRTJrGu1YY2FMTxCNubloI2+uhpsgK76aq5CKz0NM1xfgw2iy5AsTZoClXrqrqeBusGuLlMiy6dLDfIFqqd3R6gwDIW6ZjA4r0vbIJYa5NNlN0J6srtMSbrpa8QlIjrfWj4PNYP53kZO3HTYMhdXDDbEWMO+eutjxNptFz1U10AbNVpTuI7MIVJV9hE7EkLACxzSJAAAAAA=); } + @font-face { font-family: Xiaolai; src: url(data:font/woff2;base64,d09GMgABAAAAAAXgABEAAAAADpwAAAWIAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbHhweBmAARAgKCY4uERAKhgyFHQsKAAE2AiQDEAQgBYJeByAMFBv2DMgeg+NuaWWyxBATxvPTnlMRxF++s2/v9yRUC5YKWouVDvYxFpEYnIYCGg9w+H7+3fTPJXgwT+Wmph4gE2c0nVvY8zBxYZ2lT0SBiX0+Eev/Qn/y/v9f2mDu/FQcwJRmAczPmZcmGqFvKbVs7UAVKELLSym8xRa04YosyKLvuO+guwnBvMtLYARSxW6liBt98+5vgg5auoZAqYKmAfQczfw4toBsKZRKDPFmDUWhbNkKOWQ4gLDwRRd3rR6o+6l76wli/v/zQQ5hyxQpOvzoFkBpSf7DMhkKEuMIgRSpnZ90dRV+6t5a/NYT/+MPln2XbSpbu1G7Zdz9ljXAxrkImhvvFYQ2sSL6vxqaFsZAI18nZYuANjBu5ppCaEq2CJINEdAanS0CsYFbQkRyy4A3kcUCFtpnCLgFD0+dEZImY9H4Jp/A5uIQDN5xOARDd3SojqXsKz6WrcgWQboGk+YFFlbwyGw2Ikji3wF12UYnei2R0uft94b4JipU18RSNI2bQy/3ef3NJopm2RE5+SlF3FnDOc4LRUG+RtLKFRKDTJA2KwilfCINzwgCJSSy5lwRATXUlUSYfJxIbhYR39c85BNpCuUTabGIpWi2KVsE1UbniHxzE7TQbEL9utFBaVByJDSMDL1WIuvoehUGGGWDyaSr4yAQ/7fguXkzwcJZYWPW0NslsoPuVOECJbu0GgP4WEIcFw/M4kNPS+QEeqKKJCTLSVpNAXwqIY1LRwZLD32mRM6iZ6vIQa6co9U84J8/ApDf65VpqAHJUSrWdzFC9ECcQMhA78XUWjCbTJTNeedYo9nuF9wvIhSgrpaebr1JBpNPvnPWefEg8I5wWzjsDqY+hwKHXi25oRbCaSLwzMmpmg22XSiwe/ct0CeDxdfrm15NdPUQ8yGDZyMQYc5enC68m3LqUk40avYTb3lOyysnkz9gjgKPV+vnfubZzxsDu04Rc/jIothI9Qs+YTQhih7DeftuNLTysH7khQN+IVpzyXWJ5HNvtdSZsYjwCVc9/zYrlOwquqK7bVxnnBEyET2LaH5ozx7LtZGg/RZZ7D6lFpJeDwOfHfEFy/Hd3GyLblh2AL/XOWT1q09F3VP/x8jd9p07rTsPMkdpht9z5rXHdnROrCxRWRceXLd87+95PUuKHjD3mIlaa7Gx8Xa7pRuokyn3fi6TWQrL7hVoOonIC13CZP6386Oegdu/2VW12x6TvSC5TmR5reUO0j/lj5y2LM9i+k3j8uhbKiNEJFYpFXB74fjtpcpt5ak9Wi4hZ2H82YDLXZESrVoSz4z6h69RPdycal/Cx2vJjlq8888X3KaC+pTMHrc2cbV92f68L1Vxo+OInv+rsm7i/UU623XZH5+tgVJsZWDyfkNfcsYvZ6QVNR8VDi6e11xJxrYVV8bWL5Y8vvTKm+7bOZW9Rj7d8byzX1E/7iXJKyuVyZmoRzmka1918rGTflLNc1pueE7clyb9E3MsbZbk0vQO1bPzGiSK7kIjmiK9v3nEqFQx7YXq/SPKBQYPbJDUZc+ybp9vCJy51izZqciH7lFtEkwBvDdjWSh5aGZzMdMJKyDkfPqn5trPJ+urflWSxBeA3+PfJQHwl5m87f9Jr/7JBQC6Cgj+nvnqv8f8b+YI2FJZm18wNTfDzHwG5vbaq/K1VCXY3wMFfKrjASEqFbDmbu4M4950FmNqZ9oiXK/6nS0fTIm28XXgDmeqeWabijOokZvPQotw5pkLK5QjX75KTXym85ljGtzEimSxVoE8v1WpZfzmYo0WosTUFPNaWP4KmTy/VaCklsDx44pkm4az+BQpzO9XwRMDsOW3ZOSdC+cJbp1nvhXeHL9hi6+fPelarItXKw+CE6sZytOvIW1dIg4WqIuwDg==); } + @font-face { font-family: Xiaolai; src: url(data:font/woff2;base64,d09GMgABAAAAAARsABEAAAAADIQAAAQSAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbHhweBmAANAgKCY4uERAKghCBeQsGAAE2AiQDCAQgBYJeByAMFBs6C8g+DdADNv0nCQmZKD22SR70ylVVNZPdZj6OHe5K/0hhMiqn7umclEGZzimAaDtIVghBKcMPWGWVkN95NraHe3fvrzXagFFaFFGYhRoP0L17f4Flg//kLFsTzwrwA54HEmhgBxRQpHEJMJCjk9PTE44wSCVqJVHKIbTu/n2XngQFEdWWF3EWBlp68MgyDLQkaGPPFcrRBKBzTzj///wnL/z7U4N5+V1asLHF+nN5l4pXaE8a22IdC2iCtITV8hqAE6tc8Ya9g+4mBevvKwEBTAcAAAFwCCHAVBkjZKWNSsJkHAD6AQY20+IxDXjgMAAyJrqEW4YWLjMEm7kAJ+JNUSE0FYEfveupRg/oiNUPFe769Aqu4iYAYwDgABQwH0WJVqUCFUvNm30sf6sXDXjeRybA9qhAu+mOErJSVIi+hPv8DDTsPctOAQ2MaUrVMsmxU8AMCqgT7BSwA9PK1EyboDMRmcSk8AKJaVlJboGsMsPu1psKJdGByXiGUibjFQQ5UOSdkUJR9LZTwG1BVClJhHlWtpiUuck+//cEfohkMjWP08Xr5B41j29Ui7wgMI18OS7Il9VTQRTtFAiXdCelztLF1wUxLXQkJm0QTmRuUhQlqZIyEWCPtHjJRLSWKgRUGZhVMLudmmkU0hPXXu8xEXiUMhHEsSIviGo7BfIQmajTqHlBEO0UKJ/LLyklSgWNqOLrDNrAmwIssDlL7bCBFPh6fgvvTbsIj/IhIL74IYOO8LGABFIukWsG6BxRUCVUpBS/ZtANvhXQQc91ch0APSImaoaFzOKvDHrD7wIOcOQOcj0Bx/MdAVASRoT8CQBwAEAFoIBMQCU0AX8lXSyanS+9JEt6F22y2JPam3I1Hl62lfQ6bDvgc8DinnT4nfwC4tfXF0Zx3zSPsZMLNuA76Xle9HzlcoT0Lt1q/dDUc8/zzxGX+9xOJqcSXX/dVLq5OyS/8D0nn80b/zX5rsFDFiaJQ5zH5Ph6/Taepq7D9xY+m6+4ZOGUj9YjJYjXOhanTk7ujo4B6rEJsXelpIZu/mmWG3ztliScG6wqW1Awed8kztWxc9Ni4n707OQ84ytrTn41MNIiDR0AAARAZ1ZSYKFH9qwyLgn9DcDrnykAfGO2Vv/qGwAmYwAAgX9MSvUb0PSijOtFEugbDyZx+T/UmJC4s/S5myZ5o6icAsDa28JMPDfNDMPfzDqQxrlhM+9jblgw7wN0SuWqViFXqUQhUhSqU69UtSqMC3tOnPhQK5SvUKU8uI5xZYtx5hgUfGinDzcIUadQLobqAuO0QdwRF5y5N8IsWc2VnTylGi4RZXw+bx5OgIVijRNn1i1zc321Gq32pYqVaBjfd9G1TBSdUKko15UeR1XF1EsowAMlAxsAAA==); } + @font-face { font-family: Xiaolai; src: url(data:font/woff2;base64,d09GMgABAAAAAARoABEAAAAADMQAAAQOAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbHhweBmAANAgKCY4uERAKglCCKAsGAAE2AiQDCAQgBYJeByAMFBtpC8gehU2ZezT6/jg5OeRRMpYk8fDffu2+mUVM8t9BI5nmUSzTRE8nZUL+oWzodAiVPJc1dZVzGM/tSDYH8AocAFKIL2WyW1YKDJei0hcgPRFeoWHbKCtjTK9L58D//8sc3Dt/q1jpYvv3zNsmuiK9U+1pHaSCJITRcGsYqxjliGdYMqpXabDy71ciMAoIJCEEhq1SQ4fJmYXgoAO1wsDVOT9Eq4AhSYjHRN/CHQ7rUthBDhfgFXYdX6SUQGwvla4SxvU+gugesKpGhXZ+eAFSn00PNreMkIMZswSROKpsAh1FDHqpvRXXDjpWb/SZ9X5kC6AccZDPS9sRWkE56l3I4VnrAFm+rwrjAAvGSXKigmrCOKDCATYkjAMuOFkR9Mn5pVqKGWZpDQwn45baBkWlT0KtL2pkNAIr8F3IWIH/giixVHT2NVI6OIwD2YKpiozCGm5dzFpDJNU94r2YkoEVwZBbmleqWBJFJTaRioTgJOVibqlyMVEklIZxYC8ZSpqz5YDrgiuxDcnyG6QKWIg0KGOVopbAbYyJTEutczkCqoqtcJi9UdAncWTJbe9ZtEREt9ESLaEioYlhHISSUVCalCh0hKbEz/UPvIrnJMhM8rwikFW6JkGHIeoaUxHLgSvL34L35i2CzTlwmeN5TyD79EBCiEgM5RoDPiGkXIacZZ4vBHJJryTUaMRari3gO0LPDRjZ4PlJIM/0RcKKTVzlugPn474AskHJIlR7ZZC4uY7EK74g30necE2r9WxmhnOGA8+DdiDriKMpB53b+p19Zr4HloBNT028lh3x2yFYxY1usPX1geufwfr1+52fgWXTPqcNTmwTUJ91DcxvA9qIrM/7bglXO7f5rm8vesZLKTfy8v7NvRPpadONF6QTvMFwYfy/r+NAA5+ZhvgmLE3Jio19/HdQ8vLjEXdF/xQpuU9UTl2AtEiKP52WusE1d/h66ViO4d7VgYD3JiFh+apfcQMZArFf2EmsjupteauX7GiFWQ/XHX2cfZybFuscA7Vu8zf4uI9fYhc/NG6NWr/YCWLiq+nSWJXTx9VSiiGs7f32OonQL0PF14Y8AAjkVXMl/byw2n3o/0Fn4RMAP3t/6AD4DdGv6f3rNau68ngAg5KM/nqKnf8Bbc+3s4IbQXP5G142T+krGDGL3L6MulcNuCKIjiPQeF1YwqzrlmRca8m2scOdYUufT16pfvN94IKs1lij1JIVSFCs0QQTycYaA4sRLkqUIRI1qtdotDo4xSShrB4tMqgPkTx9OkmCCShVm2FsHYvaIBcZ1KP1b6RwcjFJmDqySZfIYH66wTyVD+vNJk88ccKyEDeMNc50R1mzFpMOH77opVimUilKCNxA9VGkTgt9VyKFDbEMugAA); } + @font-face { font-family: Xiaolai; src: url(data:font/woff2;base64,d09GMgABAAAAAAQQABEAAAAADCgAAAO4AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbHhweBmAANAgKCY4uERAKgTSBLwsGAAE2AiQDCAQgBYJeByAMFBvwCsi+DG+MoX6W2WQkaA6TTA8WW+jOLYyHNVxaQ79SPHw/9u3c91Y8mWb1qpJMqkHcytA0VDKhfapmIkMVTdpW/s+/tzuBOBO5Fd7/ISfyC4B2AmuSLGbX3bpTyVLdlnu+UWvbe3HDvREaVAjdpn82QvIjDm4RBBRhIAfCuIbDBfqfK6OkeVXF3v8+/oWTvOL//793D7vvNUTHFut2/m2JJrI3jWXZGkATKMI0XhbFE1NOvC+KuSFO3KvyKxGoAgSSEAIVWrWKmbnkbNBRBHcHGDme83sSsCQJcT/VFmBbZQMpSuTwgFuYWUlcHkT9gdyj3noHxO80U6aBnn/9AqTidAs6FSMho1YWqFWkE0WKBK9WDarcXpvXitXyRx6gZwUoG9s3CD2jAqmWez8CRT469BdAejBWyBJHZb0CqB4BpIv4C6B7sJKruysz810pZpglVDGsxHXlVVzDPQ10XlXNaCDm8IiMOTxDeBS1dxeqKQ3xF8DsITTyjKKC61eL+oB0z/+zgO1JwlzdIz0/I5+PztjzKInaE4IV/GV6Pn8p2RNK/QVwtwyk9SHZ5r7AF7guKS3skMhSAlKNMlbLuxJMMNbCXKm9UyCQVVElAYuH1d0VAo2md3dGXYk9X+JKXAm1J1TyFyD2JGXlKyTNLweQkH7XOwgqAcmgCNn1KiNrdF2CASY3NJYiCoGj0d+C78Y2wUEueMJ1vc/IAT2UEEHMI7kmhFNChnIoRO76kpErei2hgZY3cu0IDwhDNIKxGLl+wshT+kzCHBZ8Ltcl2a9uArIySaO4AxokUJRPT2EetMHzUo/RvUceuW9zcxeiXn48Cj121Dx2Pv3FtfP2esOy/rjl4qI+2/9TH6C3WCgzuEJjea/0uav/6r3feisDHDMzC7Jvn9224AFr9MDJaGxIkiqHjtfP3u0+KNV6eb9fp7Ls8HO4dDWzYKI5c5Xz0zVK165Ic66uPOW1he0JAAK51dvsl5+lxmH/BDl/B/jQXzfAJ+5zKkbMdf4AZRII8v07oOj/nc+M8MJzjuWH8IeQ0i8MrJrGEMk9qtQudVFUji4fS1Oo9c6U1CiYsj6PlovGTcWumCVKrGewTVauRaNysiyxclVr10HWohk2rM+gQdNmVKsMq00q+BA2okenIQOoTpub3+0Uq300UZ5DSxUb3CEzEFaHjLUSnel+I3qdknXeIoXV7ULUxBaqtbpmbmxfF1B1LVr12cpq1elcfnHTg51W5IuTR53n644grQIMR4cMHJCK6AY=); } + @font-face { font-family: Xiaolai; src: url(data:font/woff2;base64,d09GMgABAAAAAARgABEAAAAADJAAAAQIAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbHhweBmAANAgKCY4uERAKghyCBwsGAAE2AiQDCAQgBYJeByAMFBtIC1GUDE6f7OeBbQx7NEQqZWPXPFZaKd1OfRcP04bpD2m/xgP8/njPffd9GCOSgEREtbW5gmlrSohrUtwiGvx8+HH5Dk7ET8dZ7Euw61bBRFWFqolrflVPZGBpPO0w33kK7/r92l28ayq7p9h5Y4jvf9Te4hYazRexTCLq0pYeGP7/e/Vj7n9ZgmOL5eX8myUakb1pzAkyQVrCarhjA+aEq1zxfpcOupsUrL9fiYBiQEAiCAIKNVOhTJsRkQC2FLIMBh6P8SJskEMiQdibyO5jdGBPJYGJGFyARbA2b1VwMnwej6djsSbbC9Eb9G6ie2fefyQ51D+A9i3dZDeiGk1I9OnaQSoI9cfjswxnJzxZPZ+j2vuR92HY4GHFLLI/IdyThuxcgX7mFyNuXKkqcDCAs28kbKoqaLACB2JVwYPBldjjTtkCAbfz7C1ud2FvfUvSnsTZ+q1tXl0HMbD7IIYWZRKlnK1tot6qQrYgaYJXwywcLCYHTnLNv6pCBw+DxN4JO2nlWD8lkwylEMGRRxNWHpkUElUVdklnUjjdj18X8CQ2kESIDYLTzEnyxKcqiQLCJWabu8i6VwMqmpRWs2djj6Ph2ER79VgBU+glBSwgpZBMVZGDh6etY4SDOmGQ+lzbCBFB0cAQ2vwsg+bwPAcBRFaQSIJJFGPH/pb3pmWEQqmgEdX8OoM28CYHC2zW4qsDaBfhUT4ExDd/yKAjfMwhgZRN+JoBOkcUVAkVKc1fM+gG33LooMeaYShfhwPDEVcEUb5EPUqWiSABKSI2gyPQBDeOXVWDdnF0WTFNmSbf1d6lTKX6i8m0d958zb11rHn0Jnh5qXowsT3afgdOPrwpOvPm4qExYN25k/bo8MTxjXv7TvJMzPk3qNdxbNmo51jN2m35AMduUTPJ8Yrb/kPCzf1t4pw9r5aXM42+Fst543qlkwFz01R8WkrxN/g2L05GeyQNjSFxyH53f2Rhm4KTc9KzPVD6yn2WiL9vTHDOf1nw/GHR45yld7JzNv+bi+bdNnf5/9TG6sDmjiJeyPyMwjpdVdFxMVb7ZdaePHZj3Or2mixAQGwecLr79dlY1vctv4j2AW+vfk2Aj4N6zvx/ysr0J+4D+ZIFnl9NIv02IP3/+S6Q60Y98SoWsV6SfOvDJERHSo/7WJ41aELKAQM9Feqg2rE6Ueq+OhqqfT8dVuc4ShDMNbeAVUrlqlYhV6l4/pIUqlOvVLUqhD0rtmy5C1AoP8hVykOXCAcWCDs2MOcuaHqngb+6wWBuBtU5wnaDqE2Qs+PUCJLkYQ4s5SnVcIkw4ee58WAczBVrnHiybpmTy6vVaNUvVaxEw/7aRS8SEVIES0bj2sv2slG+r3+QhFaohsKUQQ==); } + @font-face { font-family: Xiaolai; src: url(data:font/woff2;base64,d09GMgABAAAAAASQABEAAAAADPAAAAQ1AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbHhweBmAAPAgKCY4uERAKgnCCQAsIAAE2AiQDDAQgBYJeByAMFBuNC8geoZdeM0djeU0v6xgWa5nVXFc88I3V/syceBL3fs09RJPkmKdE2hTpkGlE3v7fe283b17N9HQnhZFSmQrcr+nCyHqW+IT4JU9c8zMX2VTB2cCbzX463g1kogUcKTgv//8vc3Dv/K24gKWK7d8zb5voCpXekmCrIBVoCYOtcQLWKnHQM2p5qitUZ2L5d6uAEkAUZEg2i9R7vwk3K6bmgrocyP6HgdtTuhq+AEoUEU7w6AwkAgFiUIUUXIBX8ELz2wkFEPXzyv4/6OT/CUgnMO9Yya19cgcQcxX/0EwFEQnlkAB6pW7ktYK4OHSNCM4tzKRZ79HJVe79yAwA/XMMmGVpH8fNIYz7/8KAcsNRYKSjUncGYD/G8Q1xMlfuzgDqZwD2Cu4M4H4syshezJZsCaaYJlVTLOL6impZaY/B1jvVUOKJZWhuN2AZWtuCHEV4Z0sNISHuDJAtmHIVJVjEjYtZY5A95N8gA7Q/BcvIIVPKkuTxMi9HxRFeEHC8fCazJ5+J0wuEuDPALhlMuiMN5tcFXsR6WYbYIJkjgqxDKK0UbAVYQ9/pqS2xpjIOuCqqzGD2cmQfz7jxzPbRuK3AY8FWUBcRXiBx7gzI/pQcKT6O6ASSpD7XPxAqgqPBMNrzrEDm6LwEAaIoaGSKKA5cjf8teG9eTlBwSqiY0vNqgayhayXooBd1cjUQ3kgwcWZYmNnzVoFso9slOOAUHXJ1Ed5N8HBe+JjX836BHKAHJYQQFkNyjRDn6BBAsk+PqnHZrAQR8pI5Anf4AciBe0Sx3djBqoqaUusNaMJs6VJ62mTxrbzbDRKb81LB5fIlIP1wbLpZscrfwE0V7NUumL4FL61x3HhRAb1keZ8z4JeabeVN+8VQP+ogla8J9z2LtQqmVWPZzdxFC5ZLkmVmS9n/ZrF4NSCt22/Wukga35Q7edN4rdFTf0sx7DCpOrbmrWC+nyCRm8m1qkg5+fPt0su0j+vrDzzg4oyQ02uV808FtArBx7fjaT6mmhtVlcFH8CVvigfWwPGzKTfPLEWr/5RxV3/7wSM7XmdZSK5TLNBPD5LYlQDl26pqpsbEeEiZpDj9HE5p+P//H8fJkqUQgBZ9+Vaf/FbynTMUTSh1fi8ABKR6Z1eU+jnVC/tK0OgtgF/bv3YA8NvvXPD/5X9n5SfNABSIduRXVMj5N6D993GKwNUl29IrmKdNUAFTih+4I9PmR0GEt+XbhYQcDaDX/cISlLtkicpkLMkgujNn2JLrhmEyz3w5sK1BhTbNKjTIEStfjU5dGrRphfny4M1bqDg1qtRoUQnHYX7cYD68gnYocXqzW+xAs0ZFhrY25r1B1ito+whoJHFyOD/uKjXovkQay4OFyGQ2tOv0TNy3c1lQGtq0G3BsUKde987tFz0WSyVJUABxA2XbeRlkePo0mKAAVQL6AAAAAA==); } + @font-face { font-family: Xiaolai; src: url(data:font/woff2;base64,d09GMgABAAAAAASkABEAAAAADOQAAARMAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbHhweBmAANAgKCY4uERAKgnCCQQsGAAE2AiQDCAQgBYJeByAMFBuCC8iuBmxjOtKhpiUZBpuMNcJqr+PgZ/FTNpeLANrv2+zbM/Ms4ri0TyTRTEOlmiZKpl8Hb+aD36Evl2tnVLnKvFS+DnIGq3U6f239G6RfaerUzDFAcp4OkoA9F0BqubvmWdyS64Hb2krk/Qn/tUACs9zgCBOME40ty3zRcbj/4fwL+//dm4N7/2zFBStdbGfPf9tEV6S0llqsVKAKkhBGewXrGOKg54taBt1NCtbfoQLIAyj6CWLMkMP8NyGz8am56E4F/oOB62O8ER4Ch4QCQioxfcQWshYkwRQ0uEBA8E2rhxIKEPkqrymPpv4HMYV1poa9z/+QpCv/0MQEJCCCMhSBRhVDmopMvNL/WHP96eoD6QZ7P/IjoL4VCW5TbyJpDZfk76OrvhMcerLUTQLuYyzOiG0k5iaB9EnAPZqbBNrH4hsp47N1G84EE0nVgsWz+orqRpVJtPVVNYJ7sEZ8DhmsEd9DWmMkN3M21HAe7CaBbcHViuCwyRoW84Youcvf/o/A+1JYI+2ZepbeOH/WDGdi6RpNY3GNV+5DQeeq1ZwoKRKXZNFkcI4x/Logq0QPKVAbZHNYlAxwISoVGw2u+N2uFTbc2lcSUM2uS8xeTBkniWe2j7mNZoYqNpo4i5tpPNZNgu5LydHjYoFB41nmc0OD0lACC46z/ucZWaCLMiTIkqRTVJECvM3/W3hvUSVogg6D6/43Gdmi2zIcuJKjVA+IPiEQQkQ89H/MyAk9lZEhlzKlFkAsCZVQo+G1/1tG7ui9jAGjNCh1Ap7zQwFRd4rhyH8iIOEW0ySu4TGU0pIPqHUdYjNG5B9xvkTC7KDL1vvlh143rdNrO8A8lttaJJoP5OeeILG1c+s1NuQK+yNFlHTOO0eTzs7KrdPOGrJ5N6B3+Tu30uFoC7vbyDP+7T3P4MCuAuOubx+pOGJ5QGZOshGL7XnzrczhdOT++cGBROR/BaAcyr1Tujt/gueeuOU+gwrW7fw0bfbl/Dq9aXHIdWzkP58vuHiu0GYKmSIbr+NSn7TwxMV5uxLqlKOHvtbK4+NfXjleYzMoasmPzJ9e3BXmTKe6v/Ywy+hRpP9J25BhHYGlO5Y0vvz5pPd+Pyy3WfjjTu+YSXsjB/drnRtOVqp3m71WZym1qEUOOP+Zyu/qv9zzau//1dvGOifPpLf7JwtHv2ax9APw+cmXDNjvbd/wf9XYn/gAsigLEv+hStrvAf1vr8CNo3nxOh42nySfCeahOCIT7nWZClEkrQegx6PCTgx21a7oS9opd2KzOmw3Md3ArKn5PjjEUGGc0SoYcsTIV2OiSQzjjMX4cOfFS4hYNarUGKMSzjK+XFnfm2cQgfn46bOTxZgIsxUZxvUZrw2KnkHfm38jy5KD+XJTyTD5EnnGdxfMs9mwX2fKxB0nLoty8zjjzXA31Kk3efPxix7KpNIlKEC54f7H08hHVL0SATQwNagX); } + @font-face { font-family: Xiaolai; src: url(data:font/woff2;base64,d09GMgABAAAAAARIABEAAAAADGgAAAPuAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbHhweBmAANAgKCY4uERAKgXSBZQsGAAE2AiQDCAQgBYJeByAMFBsmC8g+DHCHZeFeEE9POiKOhlQIY7WvD/+XVtbGiSaf+i19yxIRX2KuEPNL3b86QyoupErV9K+9ZXPATTXEhyatj4B+v7bv/l1ILniJYj7dOqGRRBsksf4bjVAJjdDIuMFpGISUW0iNMA64Tm7nb0cKONIqCiSVhCpF91oZTYSWsf6H8y/8f5c2mL+TKla6WCZvf5poRHqn0qx2oAqSEB7VagdY8ZRP/Fb9Bt1NCtbfrwQEYAwAAALgEAQBGHLmjAxnF1ZCHw+AFsDA0zS6Hs7BAIcDkJNEGeMOGOGCLmhwAXbEQuF6Tg1SPwfQAZraIPpOzNrnsfLZD1yK+w8YHQAHoABrFAFY4zEAj0dg+6EWUFi8Xr2kwxr9yBgocEqFPC/2ETJDUol246kegIzuNQapwEOMZWkyFdIcpIIMqcADQpAKaohlK9Qru1z0kJjM5Lx2mWWz7pZ2hfdKPK1v6pClEKbgXdcwBd+6oKRKTs5KhyTFB6mgt6B8WpZggfUspj0e8pp/xWCGCphCvUvFMlGZPOyEw5l0QRBYlnJ8Xgyaw06CJFkJ9pLEk5QnaOyvC1yGHSBxfoNIBfOQkiTLlbSHAGfOlGUPyTqqElBJak7F7HHqlaWSyaXtjckeghNKewjiNslJkDKDVAhDBRViViZQCFJE/Fzn4CU8I4FMJdsrBK3iNR46GJwuM0WpDCxN/lt4b9ZC2IwDLnVs7xG0jw94hBBxoVBjwCaIlMkgp5ntC4Iu8RWPGhquFmoL2A7RMwOMdLD9RNAzfuGxwsatQt2B+fGOACh9HHvknwgAcACABxBABaADmoBbyNIA2XbBkjClJlqurAzc6dLS3o29+aU1hm2nkoV2C2Pu7bHSbS3pCW8ht8Xsrzk3ltO7HiwvWnWfwbdn5wMy8yL3VN5evtO9WQ1A+LH6H+7fMrp1FSPpN21WMR18P5o6+Ws/VaXzvXrT9gEzk8OxfQVckP8+ner81pteoIlit2xOPD8wj+ux0uMko/V3GXNzfJyRriyWn/qNQgYyv8jmP2fal5r31nkXAQACoBW//nGre/fM7A/S9wD37wDwSneOdg30RwVAHwcACPxZ0vzfgPx3KHB9qA36gJ+1ucQnvZiSRLj0u0/pqUWRpAEAfw8LHWHtrI5jStVRweR1flinYzTLiK75PkCk0WKQ/lpoVMhQrcNQw2gMMhATIViYMAkydWjTYYBW2GQiBbJiuNDADWPZ05vDZRgKIy0ZDCoyYRskQoNiuOhGhCUnixSklcbwS8QY3y+eR8phscuIiTuHLvNw1SCDjTHV6NJt+Prmi17IFBLlqEG5rrQJVVP1gwGkYIOYRQEAAA==); } + @font-face { font-family: Xiaolai; src: url(data:font/woff2;base64,d09GMgABAAAAAARcABEAAAAADKQAAAQDAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbHhweBmAANAgKCY4uERAKgjCCGwsGAAE2AiQDCAQgBYJeByAMFBtcC8ieBTmZazoyOypNNJ5/tmYZJx769vvd2VlEk6iETPMo1sg/gsfyT38p0gmZ/P7vvbc7m9RMt2dSiO/VSlKBf7Owsp6F/QlwdNsNUwmR0CCGj/4v9Cfvf63pybzbkI1wwMZlM/dDxsYYwA2BAi6jUChMq2QVaUKhUNbqGt/zooPuJgXr71ciMAwIFCEEBs2xgk7jUnJAXYW2BQxcnXkdzQSGoiA6ibmBO9RXlVCFDBcQFLad38QLoOhO0GVM6/9WEN0DZpayaMWzewClZ9I/LG2ZICIDMYbzwU2sJVRNjLsT/8Fm9vfq7R5j3o9sAOydUZCvxZ0ITZco+h+FZ/4AyPK52J0C3MM4ti5GRqXuFNAeBbjDu1PAezhOZuzjskRbCRNMEisJjsO1ZZUya49jrS+qIpInluG9qcMyfDe8rJA4Z1eVJIW4UyBbMLZCJNjE9YtZfYy85193CsxeMpYZhwwxU5RHYzhZESNxPI9j5eMZonw8huMlyZ0Ce8lY0hmqM78uuCq7Qwr8BiKbxUhbIqRSseXhJkI4YitZJ1IEVKeoU5i9lrGPpWg0o304astzqGLLexWJo2fFuFMQ9pKzxdiYSpTGoBA/NzzwOp6TIDMp8IpEVumaAh2GrBtMjYIDX6N/C96btwg258BlTuA9iezTAwUhIjlUawz4hJByGXKWBb6QyCW9UlCjkWu1toDvCD03YGRD4CeJPNMXBSs2eVXrDryPOwFI/YopqAUJaC1UuOUF+AYMwx1z3dpWIKbz56OxLfciTwlLuLsO603hG5bs17pzxmQe0LlLzW/fzjnfRCwX5921ziMOPcvMFmwQhHtRVss7Y8oq+K3miiRv/pyzUy7f1lyP0IL0e8KKJQMx8j/YZHIn947zPKDGcxd8V/XdXWCut2giY/EnLrc74jnK3GUqaKjdF5c3oTnmg6pMwJL+yW09M4diOfdA88I8lIbyimKRwvSIjtag+K2HRY1Mw+FlxpmscUlra3a/Ch+0pPP/CoO53RVehost1Jb+GopXU+Yunp05h1XlIkyyh7KSPf/0aBn9aAcF4L1dzqgwehDIOefEORo7SvTCvqprMS8A+F3zzg6AP1f72f/Huf7JNgD9b9gOfjqV+o+Nf3NccBNoTv5AP5sr5TU4YhbFE5l0r+pzhxFVA9jxsHCEMWcdxajGkQ6QvXXY0eOVoeg13wdu1CnTolGZOtmi5anSrkOdFs0wXx68eQsVo0qFKk3K4Qzmx421fHgFUZiPmz7TKVo7FGUZWlqY9wZFr6DlI6AhcHI6P+7K1em8RB7zo4VwkQVbNbomHtq+LMYtLVr1edapUatz76aLXoqlEMUTMNxIbYeXfgP9rkQKG2IN7AIA); } + @font-face { font-family: Xiaolai; src: url(data:font/woff2;base64,d09GMgABAAAAAAioABEAAAAAEtwAAAhPAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbHhweBmAAbAgKCY4uERAKjhSLJgsUAAE2AiQDFgQgBYJeByAMFBstEKOiZg5SbLK/SjCHiNHuEL+gTSNT46JXL5d2dDYNgYUXm+YfzDX+SHc2ivETmNDwKPDoyOfVpV+WL0y+vCcH0A6j74JkhdgpEKtIY6duLqtbpm7Ey9y1veFpm/+eeI84UQ+7OKuxt4GVxKIYq2RV4BrW2ogRzSLy4ldEPwOcyCeCnfdVRkkCCYQB5hjiQBYww7YRbXSP73h6NW2R2TuqakKVbhz3k50ogVGlXUK50I8LVfViSxcGPMagWmseZbCGWIllVWfiArkE9Hb6lwgEEAGwFV2GIBgJhPHWk+UvlOfz1L8DziZATUdaX+Q8wIYCpZhcP+KB+PA6KEGAxiAgRLFu785RgZZZUUW3BrYCngg3jkpa3meldKv/AIynPVCeQG0ihKnsw8aJQSlzZIF6zYYtp0uq1jRI/icY1qedWaM6++2MMFyPBO4hH4B2o0dCH7SfEXKWSxI4J/Gmv4UOOjdrSkCDMZ9juWQ+4SAj4xTo2n6wwfwuzfYP25mcCSa6lgXtzNYXll01G1MKz7QieDFz8dkOc/HbutvCNX3ACud1pgQZg6pCcFrMNnrTjRSW/2tQRo0eeAOaM2gP2e5JS3NbLK7pOvO5m4O2u2lpOuemBFtlil9xzElZL3CHFbA5YgQ+wqewLhciJTJ12kMITWTy9IySgPNvKZTo3Jlm+yQ5OVhcezJT10Bk6uJ6XNO5ZUpIo2fE9lkkWOeWOl3fwl8wGuhUM9/Q0CbeMmCDQ+xAV29h4Ovk78Jxsx7CZwIIaWB+pKFjfGIghYykXs0JWyBKpoKaVuY3GrrFdwZ6GEjv1ZGwE2JmFljpYv6moXf8YeCEi5xevUnSpwZAta0UpCCBAAoACHUhrj4WlDVe/cXIVX2uKOEENDX8rOTMlli/EROVJKe36fFD1KMTtjDKWqj3qMNxGjeybHVQDQCuD6ip4FoIR6KmExmBgZoMTD2uR0/IfdxI011dQOd440bepmnocnvC44HAkP869BrUR7WEXceNUw+DGO/lweN1GMA1zePuwzYG9dFeCnb9QCUNdJ5moJPfUV4O64lr7nU4gSG6HujMVy3q1951LpfZqAGob56zo1RAF1Ylnzd1ANnC+ripSof6Vleclgw1oFrgPG1Dydk1BaXWzN9KUduc7zE/yoahnjXcj7lUV8V+PDF1KCWnRBsNVmWqc+eie87DjPfq9KHOrtWqsGDGXxG0AhIrhDvDjLcIN1ooH0kN6d7Q+yK8XnNeFEYrpnBjjt5pCbbM4zB9wrLLSSw6eXf5RUfldzaJjP7QW9CjI9/0w19uCD7yGie8XelZxsnVXdo3yrUFyjbFezqxBji6VgsEwcMZTRRV0blCuDNKBupbPI6gzrEJ6GiwBxKXB4PR9ZrLIQMeI9D9ksAwmdwLdGbypVoQ3AihXinDB9mpFkIVlE9D6DGAq1pYVspVRncJnSslZlW4ETiejo1n1+0UGIKZjFdQz/pQchoA2mZRe/ymacYQaY3FjZ1PXnzzjtnRmX17og53WsOWFDV6ncWnXtLmGG68z9bf1X9ATPFgh3CZpJ/gdj4UoU3pF2xyOWX8/aSZQFW9R/ma8WQTgWbu9HbLwnY5FsKnVeSQCHWc/WT+Yv9dnOPuf4mFvH3q6i4Qk64Wc2MHl82GRaMSgzE5vOAni98cO9r6MHfp/xah5AyBHb3gtw2Lr1x4+wHBFS+xn/fiK2oyidBFyWvouK0gOPIUeUC5982KH71eRKS8PavOkGouu1r7tt8APxR1id6mz1f6frTOnf/k6YibNj3Cz4+PH/Y+WAn3dW/7tsNJ2qbkF68MSYbEfo+uLPsvJ7rGwg//y3E0r87XaC2dZx2Xu8VTUVLkaScy5PVrg8E5txfzFkVVLDydEWfLLc1ZJp0nHf6IwzglNxGo6pxrkPBnLmmKthqXaJRb1WW8hdnqvIw8LL9ewUTcjxbkoOittBBIIse6xotv7g3kan3LTRdzBwiQotwqC/gPs38dqwl8j/hPWvFglcJvPXtcyz4pUktMBDqcUvrj5+IOzvb0JhG6+FCEZP4lAjorUkviX1/1rQxyV6PkMwT2NI+bMH+hF3lVhFUdo7Q2oVtPvLbjp/qSslkQ55YZ81bY9SjMT714SUIpWvr9F8TXfJrAjjyyP//RF9py/SV7Xn6uw/w/g9ZDYaM9qvJwPcNvEp6R1uUN7InGKy32SPKwlhQVFpQEHDkaI2yP3cs7wm846OWzb8dhEhPBl4e6Wal5I4TC3x0Tmwj0l+WnFb7znj96efN436TxXgrf8sMWNQaCwUBE3vGwDbLW1nx0tdtXmQl0qqbOKFwpRlWt/ptgNCYbtIgQOeIgWCgFPQ63hHj7iqMH2qSPna09/G28SuYSS+akzX0j7x3qyouR8a45lot4uYnVvlWlnsZ0t4t9tne7O0BPkK4guWCujdZ0grA6+tGjgtI3+kVIPOQjHNnQl9b5RLSVhG+KhW+Rfe677bNlUcGlYlw75+d025kSUyu55MTRgpEUAAAAgADgeJKmY7fTl9ml/crDOV8AAMAbHd8FAgDwcVjQxf8/CSzqP5xtzQBCKD3Sf9WE+tcE/ffNdISR3uYI9HPL7tnugK32eEKiQ8vXlt5jfNl3cKdgkyjS+AAo8CeOJRK8i1VEkbFUETFWrY/1OCiOB+FHADbHgh22WuAY0W7cit32cOywHVOuSKlS9Swrlmy1zSLUMxUMXpUpAVXP375+r3a7gS+EsEMxpSPkltiqTFWOM39uFUyLHHtXYTNhanUCH6Zqzb6Wk+/ux9tB7nSI504ruW4vjn26Usn09unUYQJlJBfYqkS1JB42eIMjCApAIQA=); } + @font-face { font-family: Xiaolai; src: url(data:font/woff2;base64,d09GMgABAAAAAASoABEAAAAADRwAAARNAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbHhweBmAARAgKCY4uERAKgxCCVQsKAAE2AiQDDAQgBYJeByAMFBuqC1GU7E2R7MdBbqa5F152coabOiktpMaU8/nqrF+pgEOG6JmlMpT2JOPdCTYBcLn7NQC/jRtwbG6L5VhCUeCBxP3vmhbBXh8mAy7ZKcrdkPIHAJ44Y3oeM/51NXZO7GpNX2bvgioTFwlsXC7zGzI2xgBCgR2CRmFaoyI7tYyuU8WyqkpU+v4yNwZtRdw6FQQg5RTBKTUpEv89Yn78kHVodDKEuACwLAA9k2l0kcwBbynglMkj1jwxOBBO4gR80MAhUHKdGH0LS8P4WZI5SevZejDugDn5JrL6wQ2Ak3D+A+RrBulSCnBDkVjFxTSEhO27MC/mOHOSZdPV7/ALoy+MAuG0eNzYjxxBzFWoq+P5u54cAOrZDdStg0k1aF53BAVmQtQxJvX5ERSaUWCkERSZiSaNGq2RpwMRXXEYFEQjo32D0qSJ/Y3PGhIQI9KRjxHpxKmcg99eNARQGEHxGtSUE3cok/HZdNxP4/8D3isxVxEZaet5A5fLzC/nDPyUElWm6rlMMT8FiKD0lP64vTCmnBeyfGqkqXyFeBP30xYIUcqlU5glhF8MwNwRAzDJ+CKaPNWoUVHfX9/c359O/TiXTuVJ4KfAIqgwVzVxlSEFhbinxK/rGUKJkJGAlEqcL+PQDnhHHk7gTJxkLqLxDOz1/y88N+uKcGPcwYO6O9+TQ3vhvXn4gC/xEaofYv0RAUwgyGmg8xUcWolX8VCDhqiFqkWsDqFnDGCkBuebOLQZb+FhBRuxCpVBtkEDOoB+r929kD+Lft5pYdq9B7ABaH0tgOAgI53Hr+PC4/r12C3JZ2KxdhEZSGu7kfbwqv0QIb4npttnTd+IfrerV5OPXkvxVs4KrHGbz5iuDNo/Ku+TP3Y+8T0s0dR9Ax0T1IX36L/qNE5WOYzp13k1C9tyBQ5Qe+Q+SqYsRcrnWor168Ue11MzvM4YxvXoVE5WbcPd9u1XTrnyDctc3Hvn+hY2rI9rstBqorFnWubXzu+ijWTSFheRSavCZHvvh7iEpYs85t+CfWNXW+bVpI2aDzdyy1Oi0bDChT9PqpbPWjfoePN+ETdabVMMk9SdifKisa9T9omTtaZ5I/xzb7TRU1ik56cvDpQstWdYRpwKJJ9yJEd9Mv3CWTD5mMucWt+fL7VPPvpUbw3QxjDJ5+jyvKO/CSX0JQDcXP9eDQBPTJoT9ilr4P6lDQCEODNCf6Yc99cj//92OwJOEC8F/WIFWoJSrOeGb6ALjUsHDwg5lwlkokgsAmB0OzcSbo4bORwcMVJW09Zy+4087VzifOP7gHQVCtSpVqBCkklSlWjUpEKdWnKDWAUJMopdiSIlahTCfnKDmckFY3yfkQwdP9ZskkaIF6RQ15QLWiHN+M1gQ2txeby3wSwKVWg+RVIutjZSxBOhWaZl5KaN8/xCW6deh3WFMuWa12496b5y0dKFSUM53th6jN4j0A99CAA3EBdgAgAAAA==); } + @font-face { font-family: Xiaolai; src: url(data:font/woff2;base64,d09GMgABAAAAAAXIABEAAAAADrwAAAVvAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbHhweBmAAPAgKCY4uERAKhjyFFQsIAAE2AiQDCgQgBYJeByAMFBvgDFGUUM50ZD8PbGPag0X0IGi0GXhhW4zg1rvWFOETxTM8oG/x0LdW/urq3WMOk4tDlIBCroxcHRI2xuWNjJCAjlDH3ty3X5u7w5ol3jubRVT3f9R2Md9DPeqCl0Zb2hAKoVoJNNrJUF6WaZssskJKpIZ+ABc+ljpRT7IBsDra+v8/p5G9+QXbCgdsXH9PXgfGzqif4U8GBUA5QKFQqCk5B5pQTldOTYlJvxgt9xBvdZCtgzECo1AvwtCN32fYKg/HKbt7GOJUaCOAhSsyX8YzmBL1iR56G85B3FKiKyOEaYMmdKm2MWh6190713ZkG2QPnu9HgWefviidso5NolGQmOUS4ImyC91KKGz9Hgu48/X5fj44hLWOWe1bNqB9FwnlmniPeHqDor1sIHgfKPLedD4B+xAtxU3czydq+wjYw/KJ+j60TW63B0WqRL1Jn1/Um2xcnVs0dTuK0Xct6RuFaCSl0EiOmWPSHRxZkrImn2jswWpK34A8BtgsIFQZWlGC93XhQ3NHvxgQps/d5hiXbsbQMi/6RTLmbiYlYQO5pSjqFyr5viCkkT3qKnEHOaQLtSb1w04qlcEerd16q/QupKA5jmWJ9PbcblH0r42v9MFwKw9LZalMupnk+YS0r2tIWNzNNOFL8u/OjuggcigoDJ16qkKm0ekaGGCqDBfLdoyTqP+3fDfPJnA4LniMO/V8hYzRNTVoQVvVsqqO4HUJepw+DJj+1BsqZCO6sQYTmKomVjUTvDnBgsNhyfCpt1LI1nQbDbawU22tai/iHV4FIA0qFjnaCCRk6I5HiRm9eg5AX6vF1w84BKzDpmvTWUxLIzpdr+AAUPrdTZvO0paippqP7umkiCFMRJ5LVSVb1PAeJErAaHURh+dG/bJmCW/wifWbJ1FTMdSZLEojm+3syuwyn0dHTGchWTprxWyxlJWaURpFWjlzF0pKcdXapdq5C9oh0KUtRbmz5yUlpqYeuA501Xs+Rgx1bm4aNnvwJPUexw20D5Z3PtCb2SI2Xb2vm7Z6ZEJ+nMJXRH3dvfkDQKo9f9E8LZhWmUfFEylshmUkk0Yi7Ny9qsZM3bhlKj+tqT+sepiXegQAJZoihjo5suwDIPMeZ3hEArrMH6IjuJZKP1mC+2Nv3fLMCra81IZoVl6AbQWXMVwA8wiRjUXp/1dfD/NPRvhci84dcIc6Qww6l/nTi9jp6YvCw3yPA5xVp0nuFxrjh9SVaruPMNnl5Y/evkA8XkWjx02nAyJXxBoGEPxfsIjhCn3wehNQvwsPN7QiHko90BBioqb0m3oj2+2H949J/kfDvX/ZZn9ai5Iv+EO9jKSZXFdggr9cc7P+KguZPgy52jvTqUHDnkRj0TEQj6swgXzRu4UZEdxzmOXvzqOlLAuBUskXug/4uTt1fjiM4SrMvup4drIJ3N2esg3EXfhxvjBLp15oocQE1ToGYCIGeChZcM+1bjjRBcFtQ1SY8DKG/2nWFyw6EwjdYS4FJkSJbn1KORnhSX07Gq3UZqvUd9PlazTOir+6RSjzOV1DDSmPNaWeV9BZZGEugECuZvPfr+fvxXv/E9H8DR+n/9IA36y2v7bvWrz+zwYAgwoIshfZBvV/+279fQ0K4QJlR/52VOP+/fLPTp6Oa0fmF8LbBtzkEnUIe/w2oTDrM1SMa0LpAH28Loc6HkiSXXdy4DplzpZ1c5QhrUYt2bVH2bIJlSpQrFgtbsmCJRvmYYzK5OmVEkWJANOmj/e12gU5V2Griop3WCtKqiUqBhKLy5XJN0/Zv4WJxvlqDDkI1RUHxDN3ecLwbdl2EqusWLV//O2mN6JuQpsxLuFc7bYirYbF3gR64ICcgfYCAA==); } + @font-face { font-family: Xiaolai; src: url(data:font/woff2;base64,d09GMgABAAAAAARcABEAAAAADMQAAAQEAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbHhweBmAANAgKCY4uERAKglCCKQsGAAE2AiQDCAQgBYJeByAMFBtqC8geBW5bAymcujyXPnR0tWIhHuIv882fK53SGu9tKnCsY6mAiwENQiNLvIOE7+dzzvfrGhrkp4qYtEU1mJeJ6ClMXLJbJu5XlROw/ofzL+z//2UO7p2/TdcWSxfbv2feNtEV6Q1rSUKlg1SQBCjAodapHi8VQxz0jGIG3U0K1t+dBEaAQBHMyMDab4IOvSU1INGCGcDA7ZkPYgUIKQqiSQxvEB+SWSVEkOECfHqWHV/k10P2WTt+Vv74/4FsYEW7AG1//xygtHkj2CJEQWIOXyQ6gS20FNE/e/iPVdmdQzO0zeEf+QZQH3GgOG84gdAymkP/x4F7/SQo5H1rBAeoxtjjc7OoPYIDXs0BqqgIDvg19rJ8h7eqwUZjBjOF3Qz24v6OblbgSMKtT+ph6CjMQvWFD7NQc0Gx2TTprOqh6dQIDgRbMEGKoQ0vHljMBsLkPYcjOBDWxZjlOysaKhvY+W6SzXbTJEVhD3uhooG94CYpmo7gQHTJcNKY4yOuC3FZVJGcZIN0NQuTOs0wlZKNgosYhmRstLUrh4CqsqsczJ7Ld3g4NL+ivTvfRpGoZKPEQTRJ0e4IDqR18YSSxw1DNAzTss8NDBKVhJNDweTeVwpkFV0tQQOtqNHoFNkcuJv/t+C9eT3BwBlhYkbvmwUyQfeTYAEpWuTqD/gAQiAXBMyCvE8JZCvdJsEOh2iXqxPwLkIwF4JQFuL9MIEcTo+QEIkoMVKu0cA15o4AUk+xBM1AghLlWwwNnqDQSi5Y1WrjamXObajxAEusoQevQ8/f69fLHWvhSE1LzbF+nWmd5XleepCWnjyBqY5+w7FnVRZybHQ8vRgAHWsY06nMU4EhqA9pqtuSWCE+fS3RMQjzDVvY4K5/YEGcrHnDhyZRyr8I0TqL99VJc8o5Q+zYnvfJA6/9Us4QZ1JXKrsX3Vx+9+fJrZrg0ZY3OnHRZrPdQ2S/e3lXLAqpCS38YF51SrsjpLj+eN3qgz9PiVe1I4tzEadVVfB/PSNWkzty0pyyhDAUp24gil5ohQVLzURB3V1ktUhLOodUPamt9p7TB1FN+T/q+XItJClj8fY2dfpfiZz/AfCx+5sdgM8wx6r/X0JaQzkB4H3jC2L/nlLrf0D7LzsEN4am5Z9sNl3KZy7MIn8gY+5tXVf4oiUFKq8Ka5hzzVpMvFnTDmZ9a9ja9kiX7pjvAxd8OowzWgefannq9JhoEp9xxsLiRIoRI41bjy49xuiEEyxeOKvFig5CMOudPpksz0SY7sgwrobFbJCPDmqxEhtpnOwvXoROPpMvkcV8e6k8XQVrfaZM3HLisjA3jjPeDGefPv0mrz940WOxEg3y1eNzg80Srf0GwdMAgTBAVoEp); } + @font-face { font-family: Xiaolai; src: url(data:font/woff2;base64,d09GMgABAAAAAAukABEAAAAAGfwAAAtJAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhwbHhyBKAZgAIEECAoJji4REAqabJQ7CyYAATYCJAMoBCAFgl4HIAwUG3wVUVSz+iD7kRjbotgbC5XG/sLmfj+Pm/6fm5sE0aDBg1Sw+oSKQtWZaFnHRKSdMg9PK1Nle1/gqRs8vN3ePziRW3yrJEkobx+peSRpoiWWSPO5vR6v27H+7xHVtaYniRN+TMaKwQrtRlS8ckBl87+fusq+pGWpsTXbISS/5F89wmJMx3pJZ5hhbpXCdmNM4REGtYYKZSxXy7YWiVHUSnAI63ioM6HiQnXqx7NsgABACoAEfxCAAhAoMYhqSQxmeh0UrG3uBiGQAADpNACAlj+nmCV6EgOLooOg+bNREeQFKwJRU0U7uiMi2AvBZBhyuEgrQY7KLMqtVAYJckRRqgIIkAIBCLEmSBvMo5g1xE6VAmQMED0xfh4dBmrTitGyoC0JRSGsaEc0YGQCCEIvCpRBac66PqhciDTCy9LLAF4Gh2eR6Nh7rwGCIv4FAG5MM9uGAUADAgpiSFgmsFsRqDCPaIXWqdXf+0/u5sgn83IHfYfeSKM9OKLKwJ5L38DX37z++gXR2diI8/Y3b3t3WQHanhGZlRRouIBvAN+WBGl7/3WEDoaTKL0zCcEhtVcuPnOGPwnIx3GhaDCBZvmTQPiSgDz26FHYx9UmcEZtZ78zzMW4WMNAjKvl5s8eSJAZvLH0VpFYOJdLQFdGuQR0pz1RGTYZAyLhcLE/CeQYLOmNhWkxtwDNLjCy+E+LMsrXBApxZnt/R39iW9CUqAyGTXY7F0rc355T3y/Zw+EOHnqVxqBqc5RdLwh8tId1CkfALt7IVoZjsYzXaac9YjFTzBnWtkwi4LSVhUno7Y8zQkm0rb3csY3yJsbrtIu7hU32cNCfBJGvqYM3FKSR9jAv3ru6hVohIQEpK9G/TEUtp1dwUALDKMNV6koCdG37XrC7STWFhtCCjtXqX6+iZukNHIxgYoyhaiakhcJK2IBjbfq3q6gd9E4OLshgXKGaScgsimzCDR7WrX+vitpH7+eQA7lMTqjmkdz5DQCA3xlE1rNYxGBGqUS4HFIAyOkA1CwgE8l3JEQO4oKANUAuwlJGg1CjVGukCp2DJHVkkTIYpDWRt4bRajsmh+TVB2rkWkYuZ5QqhlFpRvi0ep4svxP1JfdHfuXVQ927FUg9jBE1sp2TI8agEjYCpM7V5zCkieOmzdhmng9uBIAhjeT3mmBR2uv6Jc4dWpcuc2h1TbVtXJGbrw1uBYUgq4CGWDrSPVl2/w+tVK1/W/RrsuUJS115clHEpO2rNloBVRe9c1qgmOvQ8inBAiUiVrqK2kNNvkQNHgR+34FkaYGKrb6hqL4qRAxp4tyhPIFKaGYdejtHoxjpGBAJXp8xbr0J1GRvUUbgUMQYNxOfXS/X6pq29lyu7D6T4e2B7ZxMGMKfc27SvogtTnZPm7EsSFM2nqrzKpAsO2hy1xsvAocWRHToYGynHjZJdOwqkMn9JlWOcp3NPO42d0E9wgFy5HUC16mupT6EIbynggzXMiZY1EC+SQlDDUrIryWuqHvZM+hsBBL7xoI0QkVcQQWAlziT7qFBQEKXevINYXvbhnnV2uA6wnUzWyYgtdoMnQTo/K15siiGQVp2FBYfjZH20Gb+Y/IgcPSwHih6AoUARxRLDleDbKdouAFmaj5DDGlKxU8qHLV9Qj2kmw/u1L09kEOzCFjHg2AQNMgWQap9i8w7W5Ssu9RfIQZUghb2ijQcair3aePk7mk3XOKa3aG8CjbCdoAU3wqkLB1z/D3s6StMVJpsTZx879GwGbv1JULkm3eZbPQy3KRCZbk7lO7Ubo3BrcBBp+u09LA4fT9GGKQMaXlG4Zenx3O72OnQKs7w5AoloMt0YgahE0iIWSQNtGDli4/cPsyrp6NAGvHAEIMYnGwGHr3dWoFkyXVoSIkYy0d3R45HLG7u0rFRARWby6ShYofVyRDhs5iJw7zqLZUoMS2Z9qj0UcGDxMgGITorE2GIkee+oNStMuk7dmieQ2LgfHndRcn5HUw2JH5q0n3e8RTjDX/R4QbJQGoUlbLat7mQNNJQsa2HlUeqyupkVLv1Z17j2FXtsA0WGgTrboB1F2QfZeiGzO7JKYYO7GcErumexvNPktgzztC9XFtWkhFkj6tonbVt4GtYny3PoG4PH/gALRxh+S9AuAYeNsqY+Hk2+wW9jOVTK96ge0q8f9bK1LbAPobaMSdzy//E4iRf5R1KgbCFDozYsNqE8m4oJrLkHSsfRWjP8Ots9rht6Cqs7PmK/Jl2CYVNfOrvYckbf2PqrUb0DyLfESN4dLL1DvFnS7kQ1H3QjloW4eMnF5myXJIHquonktT+MvRG9bJBwjOuj7vPo+3FhpJRfTzQiXvmZd+Tv8DLuP6vDwy9o9EgPhHCuG+Ku+5n8xoTW8+1DA2/nd1F3xnN4/iUPjDvCkPtSMJD3T+In36t5y+wbuX5sY0ua6Gw4TQ9Q5I9yvIv7aFr8IpP79fJX/igNBgbQXX77tHHjzNj2BKyCounL9y5p6v/JhT08F8wOzDdOMFId2h62tViyj3C8s0FG+ZU+7+wiPcqD+IGg31qjB36SxPCXaj+2RTrvl8vjQwgkemO5MeK9x0RsZ5RfXyaidOeDdzTV3zFOJTSx3bruJZT8olZ+Yc0p+Yheos2Dg+SqhV7qfpJ0j76wq/ooUJKxin/ix2SMzd/ZGulvt+Hqa/3Fcn+PIZ+gdGWoKdtr3b3G6z7qlYkr7GvsBx0lFCWxTF2yMsKj6ySqBUcHtXHxVrhdgVCbUWD6J8xXdyoG6eOLlWv/z5d/yVL2rSHyd9q1uTcZ/j6jY1qUqH7t3/og/DKaE8GvkitT9v2NyyiijbhkRiaFcfwN6YDj9kkJrR1qcA9xvJP1WAGUfVd5T2NA80DKdY9zr7rn7LULRiNMfSOqe2UYFPQ434ZNbAFKDBi4N/4QIgunenrrsytsPApfXPu9JnfLpmK/lp4v+VgjB0qMYiPLEK75LYnUvDa7PXXzPu0dcbtyLXK//P+0Tq7R2UM3W4i1Rd/Xt7tooYDj3vzM71FGulDr5knbhkQzZafM463bQuMsK9SXejUCBX/p15kXa4ar68aH0v8PspQw0+wgQvvXQpEFGEL/iA5ysbd96L++BOq9uxnc2VXUGCcjS/IFZ+7WLThm/ldc1vu0cefYFf1hutvGhxY4FvBv08HmkF/PsEemDFo4C/Aa78dLrRVynrfVtZKexoqDvew8tUXaNqaTod17Yf/zsd1T4nfY7tX7JnqzEW91IMaskzQT33BHVPKX9YLhW/PNBtZ+HFJkM10oSnE0XoOut8kQ9dfdsgsgufi59Xv2ja6zIRyl1IvvL2RxdwtYPppJsXbVuYeDoTyuO2m3CWfkNwXl6nM27ConnIF83jPdCCKzIl+C6G4NV/vzLUb6HXnr4Pjcpl8b8hY9w8ZRqr3kV+fpAPqPsXH8SfUtPuiVeGXLxb1/D91L6bdDxtk5cKrQRcdGBJ9741f+Pm5kZZ3PdVzafh4Tp0tHPi8phAPji944FfYedjs/rW41T9TUfqLUII/Jymvn/rWdR34yJvBpz9Ou8m/8XIAEAJBnPBjyUv+0hDx79ebAYFpBiWAP4cxQhA13zDwEPBOZBQOLo1+dAOdA4v/wq2oVFX5CrplDGdBb6vZXwBTYECkKEnweDMdgcaj6QTIIZmOIUdsItmcTsE6KqTVpwPoFzXbUovMFtWlRq+IFVaKWmoJTqEc+fKVCIqYK2KxObSNU8THVxTIcypK1HZvW6XGCsDZESyt4OSP4MpzKgpMLCAX7KyI3xxRq1YhcMLMigXspBWDVnecfgUOa0BaZj2P0U44HzZyLqxU4jRj/Or0wWAWlF4mT20SsmcHrKABsQheAA==); } + @font-face { font-family: Excalifont; src: url(data:font/woff2;base64,d09GMgABAAAAAAi8AA4AAAAADzAAAAhnAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhwbgygcNAZgAIEEEQgKkVSMfgseAAE2AiQDOAQgBYMYByAbywtRlHFSDdnPhEzuD8sFIqcVVQPGX5YIb+bD42nvJ+naVdw6ZZALDjfzBJj4b9f+F3jESULNBBIk8YHo9T+Af/9523zpFU77LwmVb2Lza2Xqp+0kSjeH096SUBOgohN1ZP7395rmplSqgM8DnC4eC1hpZ26alkadStPk07d4xALYGmWrbUdaMFnbWYLVgF0Ri7yEFwZBAAgBUCLRxJhFIF8D8BFBhzFZsgqhPT07tYH2dmraGtpX16UdtBjg49EEad20UxU8yUwBLYq4wiEjHngcH+v5I35zA1yIgPn/b/jkDVnvv+n/ETziK+6UPaTRTcAFA07Q4YEPwTXqpyCEIzhJnoagAAfazAUfpAmOLKOZMU6OK6JyD07HBwEXhGfag8BkMiYKpjnmFKP3yB4lypV7BcB9imYXqUIyARzwqFFUKYDMaBlSjSi/wJ/goGlZm+AAWD1at3+jwAn6SDcWmN+Jh82TZe8/aA/KgElkZtCh5USKJm/zFKZOU8211EZ7nXTRY2dytlHb7vv8/5AhLSEuJiIsKIBEgA75DxSrEpOrQtZPPQZoC9gKGICpuxcNdOSpJEAij1YGKou1rr5GZ/fYioqSZYtcZl9f7DI/er3HdDho5mtmr/adPvUlZmKm0xznOYVXXF2VCzbbit0ietgcezch5YIABHWP0q419KsVZj45pCaBwclspWl2Uxpynu/n49arGZl/xUkeohIAVUg8xIAlCDEGyjjt8ecgFPLqulfq+LbwrsEfR4reqnwv9mb7cr6UtWL5+Az8YDQl9VWFFTe1B5iksf7B5o7M+oyDeT7aqo/rXtOncoSoksRlXrnCm9rFx9lO+8GvXk3LO2MdBU6C5E2uTeIz9yw+bLnes+K6p2Z92pp5xklLAhBYEdF12uNiJwYYE3wUVDnJ+uWyJji1D6KobtfdkWpvsRVvx9pXpMVc4WdtRCZ4LiRzJAK2KN15RE0ThVzhMeIQosEkFCIBBuONPCKi5VgoZUtbDSmrOcxJXQKBHsIkdJ0JGtGogwC8t0MTmc00Fv0OeVYEuhvr9kD7vC1s8X4ntfkLUHIEGSHCyJUKLxcKfGFCVkvEYTAeIcoEAVY8Hx8+Ldf7qJ9/5nvGScaSMai78X3lo0+yrAYaCtE8MlkRKABnwWM7DkQZUoLq9nszr9Z/msoxAsiyoobY+52dRYDdWsJBgtwkBpgVXzq9Me2gK9whq5AQRvDsfdhN75ZWmS3h+hBSgHgf3ccPpHF+5tF63lYYyzmIAdsynzJjYw9qs0C5rwNxFAiRIBT6iNohI/yIgYoh9uUcjgQoZr9vwPc9uL3ssskz8mbZctFQHr112uQemZMIgV3EcCc5XrY65IKcgaVDJv9iGSGIQbZVd2PoIW1EJTAIhFaqaEgzERVZJfsc4N9AOX+t5u6acdZbPOurqT1mU/eCwqapjfv23JfWHbcxIx8wt1wnHSdui53meS77uNs1/tSgRYNkA7S/Sh0oLavDaN8B89p6qTNJz4ZnfpNzBDU1h3HI4bHeyW9sqxs5RaogEmTeGdLoNdEmyeHKWs5KOFNZMU7TRONV4TRWnMP8jhNcv8Jy2CdmpVrvb+xYaKiSXSd9mtroHtyYDCZ6cunmDkxLLfz3EZ9mrdLyspw+NRzomJfux+SV0WpVdJt7J8XL67MGKwYev23s5OulfDQsV52XwxgCTd7lr7pzOn/V892ogifR4zqqlKejmi+E8lNY4svbztY5N6nTyHebJn33onFrkGN2tZzJo8bFRSYPT7LxY0tbfG8SbwVHECHgdN9mzv37qbXFy5ScbF/VbJfstg9rUNZ48LrNpZcP84HLmJPj0VLcnW2cfSfkBcYffPTTjeUvG1SnkOXmT9WNiOrl3ZMkykpkw53OLIHIzYWbyy3lat1e+h09r2rb/nIozyucUOGBrTBQoTl+j/R1EnH5ghNaSnAupKfa1YPqz6GtfFGoaCd3qp3QZnb9YlGv49hM8Z3lzZrL9ZV+62waLweepz4+Nl8R1qTjQtMeD7tgqhsnm3aUGj+wh1sFi+OJcdNV7+L23rd0notbTWi9yd1aJM+jDxLtK7YF7jBBLffZfzMtWwW1XKcPmJGeMSL9eLRlbXrbvq5fm3aIYDMDitX2BItAv6A58SoMC/E4uktWONHE8w8WEH7bD5taY7z3ZLFJydnoJdREe4nI4gWapWMqBtUoOY7asTalcIW170i+wncUOnXRmMTkpQ8vpoppO62mTFCOixW+l7b5Cnr7KW3FxkM3HN021t9+YExUlAoPJDTf0UK9JMyf7tDMN/LWRv3FpasxsO67RbpRU2WRJPBz1mv/NY2J+Jy7WT/E3L1lHyvTVbnuwMW7ebra3vHZuR3ZfrDNhTmzWjMtMwflNDMkTCn72LT4weRQdfdiHtNxsnTE6pSCOh29oEu+IdE4dVpf1VWpLJ81y+MITQWZljarSNWZVWpbgqwnW38l+leI9ueBvEFTxpcZXkn5lProKGOR0+1wAAAIAAIk/LI3NZKUrzwB/QIAHvYMMgHAo0Wve/8f8t7MKyYRAA/UiOi/NceRE/r9mf2P6xbT9TDXBOXtk+Bbchiig1+c4ZJgBPtC6iFiIkZiShFRcxCZQBAqBxKQkWtxHQ4AAArg6oAOrnFRjgZhHAG0cSkTBEr7BAUxNggaQYYJBj7qBQdpfOAg3Q6Y9dCgThstNdNeO12Eyv/PU1eqyxdrGqAzGZb6WZHCRDj5ZFa62VMHLbhQI0UjKkKATFGL4wXO7oSdnq3bpIBVE05y1LQkTcIY5TvoSaBlL26xEgJ6IDDKR4nwEGOinpSzY/ONOnH57sKa8nptnDnWeO5cQNOImqKb1NcoDA0E+v8vDgAAAA==); } + @font-face { font-family: Excalifont; src: url(data:font/woff2;base64,d09GMgABAAAAAAHcAA0AAAAAA9gAAAGMAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cNAZgAAQRCAoAKgsEAAE2AiQDBAQgBYMYByAbHQPIrgp4MjSeIQLqpl4jnPFQwveqm205gmrZevZ2/8kKGaJ6ictBuCwMMnsKo8hC4RwaLBrJ7dXaFrFk0Re1e/Dk3t9wCUVrYAiZkgiZ5rGDVFdY04wBF4APOCzXHr/ljrNIDqLAY/slkQf0SyjxLNBaAq8Z1AJqYW5j0GjbckRLQhoJCgwYoyfY3q33QS5DrSAwYB4hFruNve4WoN2On4P29GEFWsC+nVM/UvSlVwIcsWycI/5gbqfUhE/9Q2P7UppTOAVeIR4TYIocELVKIEACZJgXCJ5GXl0o+esbFP3g+763DcDPx6/60MrnBaBPAsGPUDRC/8+FGIqGUSBA+NIJOwTWe13HRCIrgEvfRhj1RjLiE42eG7I5DIpVNuqkNNhxwaItTI2srRz4dfHGjhZoO0O8nb2pilFYQKhenFaycLUxsYcoAQRyoRBEnPvcgfysPq+npClt8UxLyvHWjaudqbGJA+TCckNECBGBGFforuTs0M4CUMbCAvp+P4in4o864XECREBtFQAAAAA=); } + @font-face { font-family: Nunito; src: url(data:font/woff2;base64,d09GMgABAAAAAAIsAA4AAAAABLQAAAHYAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wPIrgp4Mt6IIcJZFNCfWmZY6KzqazTt6AiVHCFawzJ7V0CQBKoSQQEpFKrCVxiSFbqqqsaTevKs/q7s6uBYmujMyA9wxb6a7XnEOcNnDLgG4nW8PoPzHzYpS2uqp51pL3eB/xZoxIllCQc80B9o4j/4xMbxQB+j+SC3hsm6JmI8RMaHj+aJApW6ZbkXlg4vXSE5FECg0og6LzxP9pOarug4tF1RLpbHeZqLX0pIt2mfy3pNG6eyGaRIjrnrr/gv2c//yGdjpJ/7DuJLin5eIZRLaObBMM/NpYpuXJ8z3SE088mEFANcCARESfwChCioAwESsVxBgeyxp6+vZ3Xzv8uz7Ae8tRk+p6RUTPOR7BmlEgh+STsAimNuKibilyluhBNe5/wCACSBKrcykVfgyb+RYcK/TGq9yMyCt3bOskSnR1FqTLMSVBsMGLQVltDKKw2IYA+wcGxHJCINY9mDOCaN4IZEo1ChY4xNg8DaB0RxDqnbMNjXJJRzF9iInqahtm67M8dOHFvXaYbn+wrGxKG3YZI+2V4CW58ovdfV1tFHXFJJiPH7T1FAJxEgYo5BKkA5iDIVQluOqZYWhQapGF6TAFhaFAAoTBIZsCFHi/0oV3gpVKwbAA==); } + @font-face { font-family: Nunito; src: url(data:font/woff2;base64,d09GMgABAAAAAAI0AA4AAAAABLQAAAHeAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wPIrgpsY9TYDjGuJ1F+MUed8Q+Do8lDKF1uhErOw/f7/X7tcy+iEk0SUSWRLDGExBCqZYuETCJVS3jlW/rvPv/y4Fra6szKjvjBubd/Nui7lGEMuAbidbw+g/MfFilLa6qnnWkvd4H/FmjEiWUJBzzQH2jiP/jExvFAH6P5ILeGybomYjxExoeP5okC5TpFmecWVieTSw4FIFBuSJ4Vngd7SlWTd4eom7xMLI/zNBO/FJHO0i6T1Zo29kTTT5Ecc9Nf4b9kP/+RT8dQL/MVxGuKXlYhlEpoZMEgy8mRiGYc/001h9DIJhNS9DEhEBBF8QsQokANCJCI5QoKZLddfb/er5n9XZpln/DYYniZkJR+fpjdo1gCwS/pEkByzEzFRPwywYywwWaOVQCAJFDhTCbyMtz5NzKM+ZdJtQeZmfPVzlkWafcoio0oVoJK/QGDNsMiWn6lPh7sAJo3ujXOJwmjmUNvVJIPNzgSgQhtMDoJAmM/EPVmRd2Cwb7gUMZdoCNqiprKqu32bGxtjOs0w8O+gFG9obdhkj7ZHgJXvyi9V1VWUUeciDjE+P07FJBxPiCij0EiQBmINBFCV4aukhKBBImYd0UfgKZEAIBAxeEBHTKU6I9yhpVChXUCAAAA); } + @font-face { font-family: Nunito; src: url(data:font/woff2;base64,d09GMgABAAAAAAIsAA4AAAAABLQAAAHYAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wPILgps9xRFkFEihAtyRcm3sWFhmEeo5Dz8tx/rvpmPqESTBE0lkSBySERCJxSLhLR5U7Uk2jzt3GfnB66lLWNU8Qd04uTtzX70HcgYcA3E63iUZs1n9bQz7eUu8N8CjTixwAIOeKA/0MR/8IlNNx7oYzQf5NYwWddEjIfI+PDR3FGg0bAs9+b23pkrpIJSQKDRjCovPE/2jZqu6Di0XVEulttVWotfakj36YLLQQ0bl7KZpEiOueu9/Jf8839UqzEzzv0A8SF9YJxXCPUSunkwzXN3raIbN+dMdwjdfLKQYoILgYCoiV+AEKVoAwESscqBAvl55Pju68dV2/rv+py/wJu+4f2SMlM2UqnaudRKIPgl5Qwg9QWdJuKXJW6EE163fAIAZIEm97KoGvDi38iw4F8mrV7LbNNzvWJZY9CdqDWnXgiaTQYM2gtraOOVJkRwDlg4tn0SkYaxbEMck0ZwSaJRqNABxqZBYOkVojh71KwZ7AsSyrkLbERHXV9Tu92JA4cOLMs0w5N1BWPi0Fs3SZ8sz4GNN5Tea2tq6SLOqCTE+P1HKKCTCBAxxSAVoBxEkQqhDcdYQ4NCg1QMr04ALA0KABQmiQzYkKPBfpRLvBQqrwwDAA==); } + @font-face { font-family: Nunito; src: url(data:font/woff2;base64,d09GMgABAAAAAAIoAA4AAAAABLQAAAHVAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wMRFZQHZF8mb0PNXjkIbeHQ4UpbWDTlYMnK5cOfavAw/zzrESo5EbTf6+zdCyBIAqHiUlkJRMJHmIKskVWRFZ4Uelb50Iz7S0IKNNGB0oyDDbgkjnxA+VCvYpk1vog5la7RoxGwSoMFQHNIwf6bvMXbb7uB/zbQEScWUOABB/oDJf6DJ5YWB/oY5UFuDZN1TcR4iIwPH80dBd1mzcrh6sxZnRpoDQTd1qyp8Hzy6v/JPaP30LMHRmG53ZSD/CblWFLgqIbNBWE2y5SSY+mk9V/qr9Nr9rO2mFcC8rUMi6qITgWjClaVl+6tZxG4/ezyvYhiVKOKkiUQBNKW3wCR1gQCConGgYL6Vik5uYHBH+T6E3yc6OHLTplP/6g2k2hXQPgla1sH3lJf6DTJbxPEedV9bQIAqqDHcZWmC+/+j44tH3TR772uDr2oN6zbTDun2m3YKIReywFCxkEb6b7Skm4ghBglR9vyebxlKPLNt+27HjvxxGew/DGCDtQsGvyXTehdSPR6qVWpddg/nU/LMs3gtu7yCJFbt56xvjyH9E/ovVql2tAfnq0bv/9CILBNpk8584BQPeMxltJeuez6zONGyYS47AK4ke1Awmg5eZRnZSUd); } + @font-face { font-family: Nunito; src: url(data:font/woff2;base64,d09GMgABAAAAAAY8AA8AAAAADAwAAAXkAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhwbgkocZAZgP1NUQVREAHwRCAqKAIgNCyAAATYCJAM8BCAFhCQHIBv4CVFUchKQ/TyMjWWPhbGVsLhp9GSH50tTI0kpiVprztv5Fg//7Ue7b76siAPJNIlodGtQN2PVLBMSh9QsiSdPzOBa2yGm7yb77eytNyKhUQpUht7Tv2xOByU3QenAtgPMB7F4fyAboXSW9/+/n6v/TjQN8ThtnIUEjdIpZXKR+wRP5pOEmJw+KE2scl5CrJImJdM0EiorlZ5YdBMRC560xeTjuPbtLy7QCkiSUAq1ViHQbDaL3Q2bEgVwd6KYhuDuYjSLwJ09yP8F4sIpGoEjBEIpaZS81yy5rcQrjzVpEJ1J/k3+gsP/ff8Il5G/2fNPGkF4pUAjvJfNawrz6+6nByVFqa9kYlFhOgoleq5SgazextGgsaNdiThmeeBJXBYr4z15AL5i/ioJvcwoWg7EgX1xjpO37MHbHk9cvNKHBpa8cWVIoeH9ZVoeX1Sk6ykIsL08SRz2FoqiqiH0H80ZBl8mbYqf/59QLgvtScQdiPfpNCbtCqFRQq+nzZp37cyEJfB7ESNYvPedtBiOKeCsCogqfiETZge9IpzwbzwL6HdlV21S19DYFITRWJv8H8yErsk/8lQez2N5NH4HI3koD+Y+IfILkbMOmItxUMQzUoFWr05wQJLEsdEC75K6R820tQUE+AQklUUEZHTU+fxbmEyk+i6LRAJoC/0YmL5Nar15FkGqz7RnmvctjIyMwqOsoReDAVLV0rwVJEhjp+jRUxhr1HGbAUa3IvzK1cwbXoQOVDGwuSytwfn+4SGxQ2JzLAdGRkcXscPyBAQhTdHRueyuHlEQQ6pXy5nLIFXRQ1pZLJUzlwhocf+WFaTAQxtDBLYC2io05Fdg1p8uIoNwsB+MJSr3zYkchu8OAaX+TjFMB6rugYymaXp0oIATKKGzrMDBZYnhJWXlS/KliW0IeizZcjbhgO1oMziJbZFUPCTWtzd6RCoQBNwf5Rk9j/mjgYOLfD84LO6H+PcPLaHXF/AAkl/fHOct//vHPIZeatoOmqbhlS9pepY6lKBzQzm1aH3VPlmNXKsvFgpOwj7P/M5wzdP3A0RUCV+Jhh/5CAZiYonQhTdv314aaKBlOWyyKAlHdorbyYAqXy4dZC4kLxJgacdvLOs6r63voK3kZcIV4l/PcsiCT15U/TMevMcVIg6UbhoHmy7WOT1wXDhEE1Z2cY6E4DErivD4ug6TNFezbFAHTdGQiPT+rAfBAhwrgCPZOdT6SsdeXdHTmEap+SEbKnmYUKl1kF9ixbBTvAjDi8Q7YTIeJvVGXPKVlMwVSfFrRErAQx6USuEfFmB4AXxvPKPOeytqahwT5VCsEgL5NVYIh7RnYQnQZ8M1wmpj5aGh4k8+gIk4brwGIaTM7D1T/l89ALkmOy2yKTb5MBsigGfagSxd+I0mLtr2Goo0qyBK3Se3V1eCadQGKKzhwsmjoL8DbcjsH/TZ2UwJIYLL2cX9CiYCEVZUDVWa5Td7EUMhYQMR+3ReC44nK7341kBYyFC0+J0Zy0R0AJ2+LirQcHa3APhUgt78p9aYOtMXb8kQ/JHUseYN+SQyliokRGB+U+fVV+/elyG/lb4jcj1HRtNAyNBRcYlDEpJLEhIroFYlYR+19qt2YF3+7UU6JVDcHxum1ZyNvFzxG3vxGV4PxIB3izP535BXlNqlEKCWQPBN0h6G1nAEQvSyjvhlccIYEk6RqBT+rLBhq++6mCeaVmSnhRSRLhqn858kQhmckK8KdDho33l762L3oRbXFaJswnP/GldY6NuVtHvpKmyw2lmyXZVhDWdtPtWkoNUMGcLYD5WxmaYg9eyDLXQhSUMDU0sLUawuaKqvbGhqbIKVYjiQaQQL2njT0LoS/m7lCr4lQzTmuxjCoadioqGD8nYpaSnBrVwZcjQGguzd9Lz9OWZlztV+GGmLZo3W0dAyABUGMFy9YkQGraQZyvui5GUQK8TxDTs4JGuoWSSGjZraeB5NvHiPSv+dQ4gKw8agodG7ECucqIZ2GpDaFVWXt4wC); } + </style> + </defs> + <g + data-id="vWrqOAfkind2qcm7LDAGZ" + stroke-linecap="round" + transform="translate(10 10) rotate(0 50 50)" + > + <path + d="M0.32 50.63 C0.32 50.63, 0.32 50.63, 0.32 50.63 M0.32 50.63 C0.32 50.63, 0.32 50.63, 0.32 50.63 M2.68 54.01 C17.68 37.03, 33.95 20.86, 40.74 10.24 M2.68 54.01 C16.59 38.38, 31.52 22.36, 40.74 10.24 M5.7 56.63 C19.86 43.02, 33.12 27.27, 52.94 2.29 M5.7 56.63 C23.06 37.39, 40.39 16.53, 52.94 2.29 M8.72 59.26 C23.62 43.85, 37.66 28.7, 55.96 4.92 M8.72 59.26 C27.67 38.75, 44.88 17.7, 55.96 4.92 M11.74 61.88 C23.55 50.54, 31.34 37.36, 58.98 7.54 M11.74 61.88 C25.4 47.12, 38.59 32.54, 58.98 7.54 M14.76 64.51 C25.64 54.29, 34.7 42.69, 61.34 10.92 M14.76 64.51 C25.83 51.81, 38 38.36, 61.34 10.92 M17.12 67.88 C34.34 49.26, 48.5 31.65, 64.36 13.55 M17.12 67.88 C33.86 49.33, 49.44 31.94, 64.36 13.55 M20.14 70.51 C28.87 60.06, 40.4 48.29, 66.72 16.92 M20.14 70.51 C29.76 60.14, 39.05 48.61, 66.72 16.92 M23.16 73.13 C37.73 55.91, 51.72 40.01, 69.74 19.55 M23.16 73.13 C35.28 60.9, 46.76 46.19, 69.74 19.55 M26.18 75.76 C38.75 61.93, 48.87 47, 72.76 22.17 M26.18 75.76 C41.17 57.48, 56.57 40.42, 72.76 22.17 M29.2 78.38 C38.58 66.84, 49.28 56.06, 75.12 25.55 M29.2 78.38 C45.23 60.41, 60.56 42.59, 75.12 25.55 M31.56 81.76 C48.47 62.24, 65.95 45.31, 78.14 28.18 M31.56 81.76 C45.84 64.05, 61.69 48.32, 78.14 28.18 M34.58 84.39 C53.9 64.75, 70.59 44.84, 81.16 30.8 M34.58 84.39 C52.97 64.26, 69.66 43.17, 81.16 30.8 M37.6 87.01 C48.25 74.78, 55.5 66.41, 83.52 34.18 M37.6 87.01 C48.99 71.83, 62.27 58.43, 83.52 34.18 M40.62 89.63 C50.34 79.26, 59.13 68.4, 86.54 36.8 M40.62 89.63 C54.92 72.51, 68.88 57.45, 86.54 36.8 M43.64 92.26 C53.65 80.81, 66.89 67.98, 89.56 39.43 M43.64 92.26 C58.66 75.45, 72.42 57.89, 89.56 39.43 M46 95.64 C61.79 76.78, 78.15 59.99, 91.92 42.81 M46 95.64 C61.9 78.99, 77.17 61.29, 91.92 42.81 M49.02 98.26 C62.86 82.2, 75.55 68.59, 94.94 45.43 M49.02 98.26 C66.35 78.07, 84.72 56.67, 94.94 45.43 M61.22 90.32 C67.86 83.05, 77.21 73.12, 97.96 48.06 M61.22 90.32 C71.29 78.99, 79.23 69.82, 97.96 48.06" + fill="none" + stroke="#15aabf" + stroke-width="0.5" + /> + <path + d="M51 0 C63.55 12.43, 75.75 25.91, 100 51 M51 0 C68.7 19, 85.84 36.05, 100 51 M100 51 C86.33 62.39, 74.98 74.41, 51 100 M100 51 C82.86 68.81, 63.17 88.61, 51 100 M51 100 C30.58 80.76, 12.2 63.63, 0 51 M51 100 C32.58 80.36, 11.39 62.79, 0 51 M0 51 C13.05 38.39, 26.48 26.08, 51 0 M0 51 C14.03 37.59, 27.42 24.3, 51 0" + fill="none" + stroke="#000000" + stroke-width="1" + /> + </g> + <g + data-id="vWrqOAfkind2qcm7LDAGZ" + stroke-linecap="round" + transform="translate(10 10) rotate(0 50 50)" + > + <path + d="M16.06 13.96 C16.06 13.96, 16.06 13.96, 16.06 13.96 M16.06 13.96 C16.06 13.96, 16.06 13.96, 16.06 13.96 M4.65 33.19 C11.77 27.87, 17.81 20.43, 26.3 8.29 M4.65 33.19 C12.84 24.59, 20.75 14.56, 26.3 8.29 M4.39 39.59 C14.94 29.16, 24.78 18.84, 37.19 1.85 M4.39 39.59 C17.83 25.46, 29.51 10.78, 37.19 1.85 M4.13 45.99 C13.58 37.43, 18.93 27.04, 40.86 3.72 M4.13 45.99 C14.89 34.56, 25.16 23.35, 40.86 3.72 M3.86 52.38 C13.79 43.36, 21.85 32.91, 45.85 4.08 M3.86 52.38 C13.79 40.95, 24.84 28.8, 45.85 4.08 M0.98 61.8 C19.05 42.22, 34.04 23.64, 50.84 4.44 M0.98 61.8 C18.6 42.22, 35.05 23.82, 50.84 4.44 M2.69 65.93 C13.07 53.65, 26.22 40.02, 56.48 4.05 M2.69 65.93 C13.71 53.84, 24.49 40.6, 56.48 4.05 M5.05 69.31 C22.52 48.71, 39.47 29.41, 61.47 4.41 M5.05 69.31 C19.68 54.34, 33.61 36.81, 61.47 4.41 M7.41 72.69 C23.29 55.19, 36.64 36.55, 66.46 4.77 M7.41 72.69 C26.51 49.67, 46.1 27.78, 66.46 4.77 M9.12 76.83 C21.59 61.65, 35.54 47.13, 70.13 6.64 M9.12 76.83 C30.41 52.84, 51 28.96, 70.13 6.64 M11.48 80.21 C33.9 54.33, 56.91 31.04, 73.81 8.51 M11.48 80.21 C30.8 56.74, 51.74 35.15, 73.81 8.51 M13.84 83.58 C39.61 56.49, 62.81 29.08, 77.48 10.38 M13.84 83.58 C38.7 55.9, 61.91 27.31, 77.48 10.38 M16.21 86.96 C30.75 70.22, 41.87 57.39, 80.5 13 M16.21 86.96 C32.53 65.84, 50.87 46.62, 80.5 13 M19.88 88.83 C33.21 74.39, 45.68 59.3, 84.17 14.87 M19.88 88.83 C40.06 65.13, 59.82 43.4, 84.17 14.87 M23.56 90.7 C37.96 74.48, 55.4 56.83, 87.85 16.74 M23.56 90.7 C44.27 67.09, 63.87 42.81, 87.85 16.74 M26.57 93.33 C48.43 67.31, 71.05 43.33, 90.21 20.12 M26.57 93.33 C48.35 69.95, 69.55 45.41, 90.21 20.12 M30.25 95.2 C48.46 74.18, 65.49 55.57, 91.92 24.25 M30.25 95.2 C53.67 67.88, 78.29 39.3, 91.92 24.25 M33.92 97.07 C45.33 84.45, 59.49 68.99, 94.28 27.63 M33.92 97.07 C49.95 78.85, 63.69 63.01, 94.28 27.63 M37.6 98.93 C57.98 79.1, 72.57 56.11, 95.99 31.77 M37.6 98.93 C52.82 81.56, 70.36 62.86, 95.99 31.77 M42.59 99.3 C62.5 72.18, 86.66 48.61, 98.35 35.14 M42.59 99.3 C56.8 81.15, 71.26 63.76, 98.35 35.14 M48.23 98.9 C67.5 76.43, 86.2 53.85, 100.71 38.52 M48.23 98.9 C66.04 79.87, 82.62 59.16, 100.71 38.52 M53.87 98.51 C66.57 83.45, 76.58 72.74, 100.45 44.92 M53.87 98.51 C64.66 85.25, 76.57 72.6, 100.45 44.92 M58.86 98.87 C72.85 81.35, 88.43 63.51, 100.85 50.56 M58.86 98.87 C74.11 80.25, 90.51 62.43, 100.85 50.56 M63.19 99.98 C76.96 84.2, 90.81 65.29, 100.59 56.96 M63.19 99.98 C71.88 89.37, 81.43 79.1, 100.59 56.96 M74.08 93.55 C81.37 85.47, 87.74 76.3, 99.67 64.11 M74.08 93.55 C79.97 87.9, 85.67 81.47, 99.67 64.11" + fill="none" + stroke="#15aabf" + stroke-width="0.5" + /> + <path + d="M38.1 1.46 C45.72 -1.06, 55.49 -0.08, 63.58 2.19 C71.68 4.46, 80.88 9.29, 86.67 15.09 C92.46 20.89, 96.33 29.02, 98.32 36.99 C100.32 44.97, 101 54.87, 98.62 62.96 C96.24 71.05, 90.17 79.66, 84.03 85.55 C77.89 91.43, 69.57 96.06, 61.79 98.28 C54 100.51, 45.12 100.97, 37.32 98.91 C29.52 96.84, 21.11 92.03, 14.99 85.89 C8.88 79.75, 2.75 70.16, 0.63 62.05 C-1.48 53.94, -0.22 45.19, 2.3 37.23 C4.82 29.28, 9.28 20.51, 15.77 14.3 C22.25 8.1, 36.21 2.25, 41.23 0.02 C46.25 -2.21, 45.49 0.09, 45.88 0.91 M40.94 1.53 C48.29 -0.28, 59.04 0.38, 66.96 3.27 C74.89 6.16, 83.12 12.22, 88.48 18.88 C93.85 25.53, 97.82 35.15, 99.18 43.21 C100.54 51.28, 99.41 59.58, 96.65 67.29 C93.89 74.99, 88.87 84.03, 82.63 89.47 C76.39 94.91, 67.44 98.55, 59.2 99.93 C50.97 101.3, 41.25 100.52, 33.2 97.7 C25.15 94.87, 16.16 89.68, 10.9 82.99 C5.64 76.3, 2.84 65.86, 1.64 57.57 C0.45 49.28, 0.99 40.72, 3.74 33.24 C6.5 25.76, 11.77 18.2, 18.18 12.7 C24.59 7.2, 38.36 1.99, 42.22 0.24 C46.09 -1.5, 41.49 1.56, 41.37 2.2" + fill="none" + stroke="#000000" + stroke-width="1" + /> + </g> + <g + data-id="vWrqOAfkind2qcm7LDAGZ" + transform="translate(10 10) rotate(0 50 50)" + > + <text + direction="ltr" + dominant-baseline="alphabetic" + fill="#000000" + font-family="Excalifont, Xiaolai, Segoe UI Emoji" + font-size="20px" + style="white-space: pre;" + text-anchor="start" + x="0" + y="17.619999999999997" + > + original text + </text> + </g> + <g + data-id="vWrqOAfkind2qcm7LDAGZ" + transform="translate(10 10) rotate(0 50 50)" + > + <text + direction="ltr" + dominant-baseline="alphabetic" + fill="#000000" + font-family="Nunito, Segoe UI Emoji" + font-size="20px" + style="white-space: pre;" + text-anchor="start" + x="0" + y="19.08" + > + original text + </text> + </g> + <g + data-id="vWrqOAfkind2qcm7LDAGZ" + transform="translate(10 10) rotate(0 50 50)" + > + <text + direction="ltr" + dominant-baseline="alphabetic" + fill="#000000" + font-family="Excalifont, Xiaolai, Segoe UI Emoji" + font-size="20px" + style="white-space: pre;" + text-anchor="start" + x="0" + y="17.619999999999997" + > + 中国你好!这是一个测试。中国你好!日本こんにちは!これはテストです。한국 안녕하세요! 이것은 테스트입니다. + </text> + </g> +</svg> +`; + +exports[`exportToSvg > with default arguments 1`] = ` +<svg + height="120" + version="1.1" + viewBox="0 0 120 120" + width="120" + xmlns="http://www.w3.org/2000/svg" +> + <!-- svg-source:excalidraw --> + <metadata /> + <defs> + <style + class="style-fonts" + > + + @font-face { font-family: Excalifont; src: url(data:font/woff2;base64,d09GMgABAAAAAAf0AA4AAAAADbQAAAegAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbgjgcNAZgAHwRCAqPYItcCxoAATYCJAMwBCAFgxgHIBujClGUblKX7Edh3Pg8OJbt5MlDFPwWk6FmJ08terdayeDx9O8Fa5eunTJ1ykAUGuAvgOF5bPfuH0nXrvQrNkFoNlERBLIQDd/mj2kP7fdg8avL4M/sm4cbu4L2i1VQVjShz/+90/YE3IJFmOC+f0Z1Xhs3bVxiJZZGa1IgY5sHCJ2EM2iBndkLWsZvFxCAhRBGCknKEOQenHCpmE2fVQTl1atzWyjvzs3aQPnWd20PJQZeG3mQp1lnLimFBAhOKFIQSrGa+vOB69gLCRkjLr4NIjd5rXiz8hhu+LIe9DZCgkJjmZEy34MkhXNBJ8GhNMMnJUgmSYA0AowAwT2zRor05iZAeN/80c4UkYlgSTOaKgOQCUOSE5F+z6+DXqndDTk4CEHP2v3/aKw7lXQXgAUAQP5+kgIpTYMHT9MEjIyWxJ5V5jAzgnrNtNBKWx101tXee86rA5O1f1fiIzEtJsS4GBMjYlikIhEvwwXDjYgiApUFdjgCFAA1m2GorwgdA5a6fzxxX0ei+Eqw6MspHfbmte7eLSh2FSEREgnySedVN+VfsbKAVQygCrGALXgDQoSA0kZqfB2OiyyaZolc/i3tnXswySlmifL9eo1ReyWQcYadAK/DD9c80uaI4pamykJM2hp/MF2uj+teZm9LiRlrltgBlcVYlWVWf+WPTpVJj40Kz9Gvi5p21Zcov8bKEL+5Uirz+j2Hjzr+99zosad2XtbpfsbKgyIQ3RJHxrTH1ysQQAjTUaii13B+OTtxrFoLubjpMYtit2uul6DdSPvKaQl/9FkppthYCXEvjoEdStVVjti2fUQf34q9YjychmIsxCAjKkj0GW4mIhlX1snPOEVRVq4UMRQwkzJNhK143IsBOlghz9meJAGf4apbAmYRMj2hsj5X1OGDPuIKXoOyN8yIMYbeusXTa9f4a+1UvYG9FgQxzoQBUoSPD5/ezAuQIP8s8IyVrUGrwSxC95WPAdlx3EkkQq5yNjcaKAAZ4LEHheIMvgHV3fe6X41/6rzCiMBwSxrnvl9RcR0gK5fy4jDVigBySy99iwlppT9aTlWIMSMKBx9WkbupSbZXNC/CKUC6z53jA0mS7340zruutV7xYguW6p/01tYHOQZQ7ptAagFiLAzFWqyWUwN8/eNGSyn2elPguvF9At0XUNlNv4t202nq+EnkKvdW6aLVlJUxhpXYUqMsn5svp9eoTpvKKf6BY8QwAkaJWYSggEuximEYiCVE0TjN5ojkVoznAP0GytsVT1SesrOn/s8LsMpVpXtSc6To4sT6XIl1q0jH96rd5/g5vmi35+y6eeYPNqjBGHtX0nKFGywfPr19aO9Gh/Q1sWZGuqckOoOJmlK2pSPTSgm/46h389ap+VmOnxsPdspP82Xyy2l7u6i2908pVjRkDbEddOKOprOPJ/94eJ59fi6jDtB6VbzuwXb5puJcqcKnUeM72fFnIlssAv/Z6uMq2s1Jd8qpd7DZo538w5PG7cHm2TU2TD41PjYiaUSikYspa/kjJ84AdgqXsT226/L+fW6j99QmJZlWN99tfcdbUPO17tLu8+gVw73h3OfmurdS9BCa5twNfokJhx7/agK3fHC9rXVewbT0kZG9vXqRBOtS6xGOZ5dC3pwleZIyidL1le+xC3btOlwJkXqGESosoDUG2TqcuE/6OcolnOykkpKdD+5l7+JODWBpAycPke+STDMRWidsWCLvfQJbKM7JpnkLG1WV73qjg6eZ1EMVF1NgG5rTaZF2r7tJNs2VzabNrTQfhSOtgxRxRLP5mldJB6/b6R5LWk9ss9nNUGyTTx8iytdCS9xlAlvtN/1hTrYObLVe5T8zLWNk2oko/bq0dv1cvjXrGC5k+pfYm+L1MtXCFsSzKDTY/dhu66JJWqlfkIxw7T5ufjDBa4pCy7ObPC0cojzlZMlCh2VjKwfX8qy5cpyRt1hp6DeKs/UZjc6zfGxC0rJHl1IUtIm2p7Tg+6VKn8vbfWR9fHljiebwTXPXTQ13HmoSbMssDsa32NnSfmmoH92xuU/E7U2qS8vWYFD9D73VJodqvWU8l7tB+b9ZdPiXvC2qoboerfoamG78+oOX7uWn1/WJy87rJPSHcVmUO7sN0ypzcG5zdfzU8k/NSh5OCbHvUSJlOk2xGrkmubA+nV7YtUCdoJk2vZ/dNSvrAkFnE0toKlC7rHllSrrOzt4Yb91LaLga9TtY+etg/uCpE8rVr604yv7YaE2x450wAACIfUvVNvFrrWXyNylLvwSAR70CtQDwePGbPuLQ/32Y10wCAClKi8AXKzralCj+/e0Dwri9RW2ArkwEqBW+cYJzghDkKyuPEB0FElKG8DIXEQkASS4s/2PSN2DGs1Bi6eh9EhVoZDAH0NblyEbw9tsoChtttEDDbQxvDTZWKm+YcYcDOj01qtdWK8110F5XIQq+nMJuwnpSopmb7KJVWbUgQqhwkyQxlOd76aglFYEKoxAJ8OeFkYPjBPTuQejsbXStQgY5klp1cr1LoQHSUa8StKqVbKnrXAqNAgg8wm1EB6RBL7ejLWnSmSI9hGJQZdAW2trTXRJoBsJm6M5VNQlFM3yJ/7EAAAA=); } + @font-face { font-family: Excalifont; src: url(data:font/woff2;base64,d09GMgABAAAAAAHcAA0AAAAAA9gAAAGMAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cNAZgAAQRCAoAKgsEAAE2AiQDBAQgBYMYByAbHQPIrgp4MjSeIQLqpl4jnPFQwveqm205gmrZevZ2/8kKGaJ6ictBuCwMMnsKo8hC4RwaLBrJ7dXaFrFk0Re1e/Dk3t9wCUVrYAiZkgiZ5rGDVFdY04wBF4APOCzXHr/ljrNIDqLAY/slkQf0SyjxLNBaAq8Z1AJqYW5j0GjbckRLQhoJCgwYoyfY3q33QS5DrSAwYB4hFruNve4WoN2On4P29GEFWsC+nVM/UvSlVwIcsWycI/5gbqfUhE/9Q2P7UppTOAVeIR4TYIocELVKIEACZJgXCJ5GXl0o+esbFP3g+763DcDPx6/60MrnBaBPAsGPUDRC/8+FGIqGUSBA+NIJOwTWe13HRCIrgEvfRhj1RjLiE42eG7I5DIpVNuqkNNhxwaItTI2srRz4dfHGjhZoO0O8nb2pilFYQKhenFaycLUxsYcoAQRyoRBEnPvcgfysPq+npClt8UxLyvHWjaudqbGJA+TCckNECBGBGFforuTs0M4CUMbCAvp+P4in4o864XECREBtFQAAAAA=); } + @font-face { font-family: Nunito; src: url(data:font/woff2;base64,d09GMgABAAAAAAIsAA4AAAAABLQAAAHYAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wPIrgp4Mt6IIcJZFNCfWmZY6KzqazTt6AiVHCFawzJ7V0CQBKoSQQEpFKrCVxiSFbqqqsaTevKs/q7s6uBYmujMyA9wxb6a7XnEOcNnDLgG4nW8PoPzHzYpS2uqp51pL3eB/xZoxIllCQc80B9o4j/4xMbxQB+j+SC3hsm6JmI8RMaHj+aJApW6ZbkXlg4vXSE5FECg0og6LzxP9pOarug4tF1RLpbHeZqLX0pIt2mfy3pNG6eyGaRIjrnrr/gv2c//yGdjpJ/7DuJLin5eIZRLaObBMM/NpYpuXJ8z3SE088mEFANcCARESfwChCioAwESsVxBgeyxp6+vZ3Xzv8uz7Ae8tRk+p6RUTPOR7BmlEgh+STsAimNuKibilyluhBNe5/wCACSBKrcykVfgyb+RYcK/TGq9yMyCt3bOskSnR1FqTLMSVBsMGLQVltDKKw2IYA+wcGxHJCINY9mDOCaN4IZEo1ChY4xNg8DaB0RxDqnbMNjXJJRzF9iInqahtm67M8dOHFvXaYbn+wrGxKG3YZI+2V4CW58ovdfV1tFHXFJJiPH7T1FAJxEgYo5BKkA5iDIVQluOqZYWhQapGF6TAFhaFAAoTBIZsCFHi/0oV3gpVKwbAA==); } + @font-face { font-family: Nunito; src: url(data:font/woff2;base64,d09GMgABAAAAAAI0AA4AAAAABLQAAAHeAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wPIrgpsY9TYDjGuJ1F+MUed8Q+Do8lDKF1uhErOw/f7/X7tcy+iEk0SUSWRLDGExBCqZYuETCJVS3jlW/rvPv/y4Fra6szKjvjBubd/Nui7lGEMuAbidbw+g/MfFilLa6qnnWkvd4H/FmjEiWUJBzzQH2jiP/jExvFAH6P5ILeGybomYjxExoeP5okC5TpFmecWVieTSw4FIFBuSJ4Vngd7SlWTd4eom7xMLI/zNBO/FJHO0i6T1Zo29kTTT5Ecc9Nf4b9kP/+RT8dQL/MVxGuKXlYhlEpoZMEgy8mRiGYc/001h9DIJhNS9DEhEBBF8QsQokANCJCI5QoKZLddfb/er5n9XZpln/DYYniZkJR+fpjdo1gCwS/pEkByzEzFRPwywYywwWaOVQCAJFDhTCbyMtz5NzKM+ZdJtQeZmfPVzlkWafcoio0oVoJK/QGDNsMiWn6lPh7sAJo3ujXOJwmjmUNvVJIPNzgSgQhtMDoJAmM/EPVmRd2Cwb7gUMZdoCNqiprKqu32bGxtjOs0w8O+gFG9obdhkj7ZHgJXvyi9V1VWUUeciDjE+P07FJBxPiCij0EiQBmINBFCV4aukhKBBImYd0UfgKZEAIBAxeEBHTKU6I9yhpVChXUCAAAA); } + @font-face { font-family: Nunito; src: url(data:font/woff2;base64,d09GMgABAAAAAAIsAA4AAAAABLQAAAHYAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wPILgps9xRFkFEihAtyRcm3sWFhmEeo5Dz8tx/rvpmPqESTBE0lkSBySERCJxSLhLR5U7Uk2jzt3GfnB66lLWNU8Qd04uTtzX70HcgYcA3E63iUZs1n9bQz7eUu8N8CjTixwAIOeKA/0MR/8IlNNx7oYzQf5NYwWddEjIfI+PDR3FGg0bAs9+b23pkrpIJSQKDRjCovPE/2jZqu6Di0XVEulttVWotfakj36YLLQQ0bl7KZpEiOueu9/Jf8839UqzEzzv0A8SF9YJxXCPUSunkwzXN3raIbN+dMdwjdfLKQYoILgYCoiV+AEKVoAwESscqBAvl55Pju68dV2/rv+py/wJu+4f2SMlM2UqnaudRKIPgl5Qwg9QWdJuKXJW6EE163fAIAZIEm97KoGvDi38iw4F8mrV7LbNNzvWJZY9CdqDWnXgiaTQYM2gtraOOVJkRwDlg4tn0SkYaxbEMck0ZwSaJRqNABxqZBYOkVojh71KwZ7AsSyrkLbERHXV9Tu92JA4cOLMs0w5N1BWPi0Fs3SZ8sz4GNN5Tea2tq6SLOqCTE+P1HKKCTCBAxxSAVoBxEkQqhDcdYQ4NCg1QMr04ALA0KABQmiQzYkKPBfpRLvBQqrwwDAA==); } + @font-face { font-family: Nunito; src: url(data:font/woff2;base64,d09GMgABAAAAAAIoAA4AAAAABLQAAAHVAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wMRFZQHZF8mb0PNXjkIbeHQ4UpbWDTlYMnK5cOfavAw/zzrESo5EbTf6+zdCyBIAqHiUlkJRMJHmIKskVWRFZ4Uelb50Iz7S0IKNNGB0oyDDbgkjnxA+VCvYpk1vog5la7RoxGwSoMFQHNIwf6bvMXbb7uB/zbQEScWUOABB/oDJf6DJ5YWB/oY5UFuDZN1TcR4iIwPH80dBd1mzcrh6sxZnRpoDQTd1qyp8Hzy6v/JPaP30LMHRmG53ZSD/CblWFLgqIbNBWE2y5SSY+mk9V/qr9Nr9rO2mFcC8rUMi6qITgWjClaVl+6tZxG4/ezyvYhiVKOKkiUQBNKW3wCR1gQCConGgYL6Vik5uYHBH+T6E3yc6OHLTplP/6g2k2hXQPgla1sH3lJf6DTJbxPEedV9bQIAqqDHcZWmC+/+j44tH3TR772uDr2oN6zbTDun2m3YKIReywFCxkEb6b7Skm4ghBglR9vyebxlKPLNt+27HjvxxGew/DGCDtQsGvyXTehdSPR6qVWpddg/nU/LMs3gtu7yCJFbt56xvjyH9E/ovVql2tAfnq0bv/9CILBNpk8584BQPeMxltJeuez6zONGyYS47AK4ke1Awmg5eZRnZSUd); } + @font-face { font-family: Nunito; src: url(data:font/woff2;base64,d09GMgABAAAAAAY8AA8AAAAADAwAAAXkAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhwbgkocZAZgP1NUQVREAHwRCAqKAIgNCyAAATYCJAM8BCAFhCQHIBv4CVFUchKQ/TyMjWWPhbGVsLhp9GSH50tTI0kpiVprztv5Fg//7Ue7b76siAPJNIlodGtQN2PVLBMSh9QsiSdPzOBa2yGm7yb77eytNyKhUQpUht7Tv2xOByU3QenAtgPMB7F4fyAboXSW9/+/n6v/TjQN8ThtnIUEjdIpZXKR+wRP5pOEmJw+KE2scl5CrJImJdM0EiorlZ5YdBMRC560xeTjuPbtLy7QCkiSUAq1ViHQbDaL3Q2bEgVwd6KYhuDuYjSLwJ09yP8F4sIpGoEjBEIpaZS81yy5rcQrjzVpEJ1J/k3+gsP/ff8Il5G/2fNPGkF4pUAjvJfNawrz6+6nByVFqa9kYlFhOgoleq5SgazextGgsaNdiThmeeBJXBYr4z15AL5i/ioJvcwoWg7EgX1xjpO37MHbHk9cvNKHBpa8cWVIoeH9ZVoeX1Sk6ykIsL08SRz2FoqiqiH0H80ZBl8mbYqf/59QLgvtScQdiPfpNCbtCqFRQq+nzZp37cyEJfB7ESNYvPedtBiOKeCsCogqfiETZge9IpzwbzwL6HdlV21S19DYFITRWJv8H8yErsk/8lQez2N5NH4HI3koD+Y+IfILkbMOmItxUMQzUoFWr05wQJLEsdEC75K6R820tQUE+AQklUUEZHTU+fxbmEyk+i6LRAJoC/0YmL5Nar15FkGqz7RnmvctjIyMwqOsoReDAVLV0rwVJEhjp+jRUxhr1HGbAUa3IvzK1cwbXoQOVDGwuSytwfn+4SGxQ2JzLAdGRkcXscPyBAQhTdHRueyuHlEQQ6pXy5nLIFXRQ1pZLJUzlwhocf+WFaTAQxtDBLYC2io05Fdg1p8uIoNwsB+MJSr3zYkchu8OAaX+TjFMB6rugYymaXp0oIATKKGzrMDBZYnhJWXlS/KliW0IeizZcjbhgO1oMziJbZFUPCTWtzd6RCoQBNwf5Rk9j/mjgYOLfD84LO6H+PcPLaHXF/AAkl/fHOct//vHPIZeatoOmqbhlS9pepY6lKBzQzm1aH3VPlmNXKsvFgpOwj7P/M5wzdP3A0RUCV+Jhh/5CAZiYonQhTdv314aaKBlOWyyKAlHdorbyYAqXy4dZC4kLxJgacdvLOs6r63voK3kZcIV4l/PcsiCT15U/TMevMcVIg6UbhoHmy7WOT1wXDhEE1Z2cY6E4DErivD4ug6TNFezbFAHTdGQiPT+rAfBAhwrgCPZOdT6SsdeXdHTmEap+SEbKnmYUKl1kF9ixbBTvAjDi8Q7YTIeJvVGXPKVlMwVSfFrRErAQx6USuEfFmB4AXxvPKPOeytqahwT5VCsEgL5NVYIh7RnYQnQZ8M1wmpj5aGh4k8+gIk4brwGIaTM7D1T/l89ALkmOy2yKTb5MBsigGfagSxd+I0mLtr2Goo0qyBK3Se3V1eCadQGKKzhwsmjoL8DbcjsH/TZ2UwJIYLL2cX9CiYCEVZUDVWa5Td7EUMhYQMR+3ReC44nK7341kBYyFC0+J0Zy0R0AJ2+LirQcHa3APhUgt78p9aYOtMXb8kQ/JHUseYN+SQyliokRGB+U+fVV+/elyG/lb4jcj1HRtNAyNBRcYlDEpJLEhIroFYlYR+19qt2YF3+7UU6JVDcHxum1ZyNvFzxG3vxGV4PxIB3izP535BXlNqlEKCWQPBN0h6G1nAEQvSyjvhlccIYEk6RqBT+rLBhq++6mCeaVmSnhRSRLhqn858kQhmckK8KdDho33l762L3oRbXFaJswnP/GldY6NuVtHvpKmyw2lmyXZVhDWdtPtWkoNUMGcLYD5WxmaYg9eyDLXQhSUMDU0sLUawuaKqvbGhqbIKVYjiQaQQL2njT0LoS/m7lCr4lQzTmuxjCoadioqGD8nYpaSnBrVwZcjQGguzd9Lz9OWZlztV+GGmLZo3W0dAyABUGMFy9YkQGraQZyvui5GUQK8TxDTs4JGuoWSSGjZraeB5NvHiPSv+dQ4gKw8agodG7ECucqIZ2GpDaFVWXt4wC); } + </style> + </defs> + <g + data-id="vWrqOAfkind2qcm7LDAGZ" + stroke-linecap="round" + transform="translate(10 10) rotate(0 50 50)" + > + <path + d="M0.32 50.63 C0.32 50.63, 0.32 50.63, 0.32 50.63 M0.32 50.63 C0.32 50.63, 0.32 50.63, 0.32 50.63 M2.68 54.01 C17.68 37.03, 33.95 20.86, 40.74 10.24 M2.68 54.01 C16.59 38.38, 31.52 22.36, 40.74 10.24 M5.7 56.63 C19.86 43.02, 33.12 27.27, 52.94 2.29 M5.7 56.63 C23.06 37.39, 40.39 16.53, 52.94 2.29 M8.72 59.26 C23.62 43.85, 37.66 28.7, 55.96 4.92 M8.72 59.26 C27.67 38.75, 44.88 17.7, 55.96 4.92 M11.74 61.88 C23.55 50.54, 31.34 37.36, 58.98 7.54 M11.74 61.88 C25.4 47.12, 38.59 32.54, 58.98 7.54 M14.76 64.51 C25.64 54.29, 34.7 42.69, 61.34 10.92 M14.76 64.51 C25.83 51.81, 38 38.36, 61.34 10.92 M17.12 67.88 C34.34 49.26, 48.5 31.65, 64.36 13.55 M17.12 67.88 C33.86 49.33, 49.44 31.94, 64.36 13.55 M20.14 70.51 C28.87 60.06, 40.4 48.29, 66.72 16.92 M20.14 70.51 C29.76 60.14, 39.05 48.61, 66.72 16.92 M23.16 73.13 C37.73 55.91, 51.72 40.01, 69.74 19.55 M23.16 73.13 C35.28 60.9, 46.76 46.19, 69.74 19.55 M26.18 75.76 C38.75 61.93, 48.87 47, 72.76 22.17 M26.18 75.76 C41.17 57.48, 56.57 40.42, 72.76 22.17 M29.2 78.38 C38.58 66.84, 49.28 56.06, 75.12 25.55 M29.2 78.38 C45.23 60.41, 60.56 42.59, 75.12 25.55 M31.56 81.76 C48.47 62.24, 65.95 45.31, 78.14 28.18 M31.56 81.76 C45.84 64.05, 61.69 48.32, 78.14 28.18 M34.58 84.39 C53.9 64.75, 70.59 44.84, 81.16 30.8 M34.58 84.39 C52.97 64.26, 69.66 43.17, 81.16 30.8 M37.6 87.01 C48.25 74.78, 55.5 66.41, 83.52 34.18 M37.6 87.01 C48.99 71.83, 62.27 58.43, 83.52 34.18 M40.62 89.63 C50.34 79.26, 59.13 68.4, 86.54 36.8 M40.62 89.63 C54.92 72.51, 68.88 57.45, 86.54 36.8 M43.64 92.26 C53.65 80.81, 66.89 67.98, 89.56 39.43 M43.64 92.26 C58.66 75.45, 72.42 57.89, 89.56 39.43 M46 95.64 C61.79 76.78, 78.15 59.99, 91.92 42.81 M46 95.64 C61.9 78.99, 77.17 61.29, 91.92 42.81 M49.02 98.26 C62.86 82.2, 75.55 68.59, 94.94 45.43 M49.02 98.26 C66.35 78.07, 84.72 56.67, 94.94 45.43 M61.22 90.32 C67.86 83.05, 77.21 73.12, 97.96 48.06 M61.22 90.32 C71.29 78.99, 79.23 69.82, 97.96 48.06" + fill="none" + stroke="#15aabf" + stroke-width="0.5" + /> + <path + d="M51 0 C63.55 12.43, 75.75 25.91, 100 51 M51 0 C68.7 19, 85.84 36.05, 100 51 M100 51 C86.33 62.39, 74.98 74.41, 51 100 M100 51 C82.86 68.81, 63.17 88.61, 51 100 M51 100 C30.58 80.76, 12.2 63.63, 0 51 M51 100 C32.58 80.36, 11.39 62.79, 0 51 M0 51 C13.05 38.39, 26.48 26.08, 51 0 M0 51 C14.03 37.59, 27.42 24.3, 51 0" + fill="none" + stroke="#000000" + stroke-width="1" + /> + </g> + <g + data-id="vWrqOAfkind2qcm7LDAGZ" + stroke-linecap="round" + transform="translate(10 10) rotate(0 50 50)" + > + <path + d="M16.06 13.96 C16.06 13.96, 16.06 13.96, 16.06 13.96 M16.06 13.96 C16.06 13.96, 16.06 13.96, 16.06 13.96 M4.65 33.19 C11.77 27.87, 17.81 20.43, 26.3 8.29 M4.65 33.19 C12.84 24.59, 20.75 14.56, 26.3 8.29 M4.39 39.59 C14.94 29.16, 24.78 18.84, 37.19 1.85 M4.39 39.59 C17.83 25.46, 29.51 10.78, 37.19 1.85 M4.13 45.99 C13.58 37.43, 18.93 27.04, 40.86 3.72 M4.13 45.99 C14.89 34.56, 25.16 23.35, 40.86 3.72 M3.86 52.38 C13.79 43.36, 21.85 32.91, 45.85 4.08 M3.86 52.38 C13.79 40.95, 24.84 28.8, 45.85 4.08 M0.98 61.8 C19.05 42.22, 34.04 23.64, 50.84 4.44 M0.98 61.8 C18.6 42.22, 35.05 23.82, 50.84 4.44 M2.69 65.93 C13.07 53.65, 26.22 40.02, 56.48 4.05 M2.69 65.93 C13.71 53.84, 24.49 40.6, 56.48 4.05 M5.05 69.31 C22.52 48.71, 39.47 29.41, 61.47 4.41 M5.05 69.31 C19.68 54.34, 33.61 36.81, 61.47 4.41 M7.41 72.69 C23.29 55.19, 36.64 36.55, 66.46 4.77 M7.41 72.69 C26.51 49.67, 46.1 27.78, 66.46 4.77 M9.12 76.83 C21.59 61.65, 35.54 47.13, 70.13 6.64 M9.12 76.83 C30.41 52.84, 51 28.96, 70.13 6.64 M11.48 80.21 C33.9 54.33, 56.91 31.04, 73.81 8.51 M11.48 80.21 C30.8 56.74, 51.74 35.15, 73.81 8.51 M13.84 83.58 C39.61 56.49, 62.81 29.08, 77.48 10.38 M13.84 83.58 C38.7 55.9, 61.91 27.31, 77.48 10.38 M16.21 86.96 C30.75 70.22, 41.87 57.39, 80.5 13 M16.21 86.96 C32.53 65.84, 50.87 46.62, 80.5 13 M19.88 88.83 C33.21 74.39, 45.68 59.3, 84.17 14.87 M19.88 88.83 C40.06 65.13, 59.82 43.4, 84.17 14.87 M23.56 90.7 C37.96 74.48, 55.4 56.83, 87.85 16.74 M23.56 90.7 C44.27 67.09, 63.87 42.81, 87.85 16.74 M26.57 93.33 C48.43 67.31, 71.05 43.33, 90.21 20.12 M26.57 93.33 C48.35 69.95, 69.55 45.41, 90.21 20.12 M30.25 95.2 C48.46 74.18, 65.49 55.57, 91.92 24.25 M30.25 95.2 C53.67 67.88, 78.29 39.3, 91.92 24.25 M33.92 97.07 C45.33 84.45, 59.49 68.99, 94.28 27.63 M33.92 97.07 C49.95 78.85, 63.69 63.01, 94.28 27.63 M37.6 98.93 C57.98 79.1, 72.57 56.11, 95.99 31.77 M37.6 98.93 C52.82 81.56, 70.36 62.86, 95.99 31.77 M42.59 99.3 C62.5 72.18, 86.66 48.61, 98.35 35.14 M42.59 99.3 C56.8 81.15, 71.26 63.76, 98.35 35.14 M48.23 98.9 C67.5 76.43, 86.2 53.85, 100.71 38.52 M48.23 98.9 C66.04 79.87, 82.62 59.16, 100.71 38.52 M53.87 98.51 C66.57 83.45, 76.58 72.74, 100.45 44.92 M53.87 98.51 C64.66 85.25, 76.57 72.6, 100.45 44.92 M58.86 98.87 C72.85 81.35, 88.43 63.51, 100.85 50.56 M58.86 98.87 C74.11 80.25, 90.51 62.43, 100.85 50.56 M63.19 99.98 C76.96 84.2, 90.81 65.29, 100.59 56.96 M63.19 99.98 C71.88 89.37, 81.43 79.1, 100.59 56.96 M74.08 93.55 C81.37 85.47, 87.74 76.3, 99.67 64.11 M74.08 93.55 C79.97 87.9, 85.67 81.47, 99.67 64.11" + fill="none" + stroke="#15aabf" + stroke-width="0.5" + /> + <path + d="M38.1 1.46 C45.72 -1.06, 55.49 -0.08, 63.58 2.19 C71.68 4.46, 80.88 9.29, 86.67 15.09 C92.46 20.89, 96.33 29.02, 98.32 36.99 C100.32 44.97, 101 54.87, 98.62 62.96 C96.24 71.05, 90.17 79.66, 84.03 85.55 C77.89 91.43, 69.57 96.06, 61.79 98.28 C54 100.51, 45.12 100.97, 37.32 98.91 C29.52 96.84, 21.11 92.03, 14.99 85.89 C8.88 79.75, 2.75 70.16, 0.63 62.05 C-1.48 53.94, -0.22 45.19, 2.3 37.23 C4.82 29.28, 9.28 20.51, 15.77 14.3 C22.25 8.1, 36.21 2.25, 41.23 0.02 C46.25 -2.21, 45.49 0.09, 45.88 0.91 M40.94 1.53 C48.29 -0.28, 59.04 0.38, 66.96 3.27 C74.89 6.16, 83.12 12.22, 88.48 18.88 C93.85 25.53, 97.82 35.15, 99.18 43.21 C100.54 51.28, 99.41 59.58, 96.65 67.29 C93.89 74.99, 88.87 84.03, 82.63 89.47 C76.39 94.91, 67.44 98.55, 59.2 99.93 C50.97 101.3, 41.25 100.52, 33.2 97.7 C25.15 94.87, 16.16 89.68, 10.9 82.99 C5.64 76.3, 2.84 65.86, 1.64 57.57 C0.45 49.28, 0.99 40.72, 3.74 33.24 C6.5 25.76, 11.77 18.2, 18.18 12.7 C24.59 7.2, 38.36 1.99, 42.22 0.24 C46.09 -1.5, 41.49 1.56, 41.37 2.2" + fill="none" + stroke="#000000" + stroke-width="1" + /> + </g> + <g + data-id="vWrqOAfkind2qcm7LDAGZ" + transform="translate(10 10) rotate(0 50 50)" + > + <text + direction="ltr" + dominant-baseline="alphabetic" + fill="#000000" + font-family="Excalifont, Xiaolai, Segoe UI Emoji" + font-size="20px" + style="white-space: pre;" + text-anchor="start" + x="0" + y="17.619999999999997" + > + original text + </text> + </g> + <g + data-id="vWrqOAfkind2qcm7LDAGZ" + transform="translate(10 10) rotate(0 50 50)" + > + <text + direction="ltr" + dominant-baseline="alphabetic" + fill="#000000" + font-family="Nunito, Segoe UI Emoji" + font-size="20px" + style="white-space: pre;" + text-anchor="start" + x="0" + y="19.08" + > + original text + </text> + </g> +</svg> +`; + +exports[`exportToSvg > with elements that have a link 1`] = ` +"<!-- svg-source:excalidraw --><metadata></metadata><defs><style class="style-fonts"> + </style></defs><a href="excalidraw.com"><g stroke-linecap="round" transform="translate(10 10) rotate(0 107 107)" data-id="vWrqOAfkind2qcm7LDAGZ"><path d="M0 0 C0 0, 0 0, 0 0 M0 0 C0 0, 0 0, 0 0 M-0.26 6.4 C1.86 4, 4.58 1.9, 4.99 0.36 M-0.26 6.4 C1.63 4.44, 3.93 2.27, 4.99 0.36 M0.13 12.04 C3.95 10, 6.87 6.25, 10.63 -0.03 M0.13 12.04 C4.24 8, 8.06 2.86, 10.63 -0.03 M-0.13 18.44 C5.28 13.88, 10.16 9.29, 15.62 0.33 M-0.13 18.44 C6.82 11.86, 11.95 4.69, 15.62 0.33 M0.27 24.08 C6.17 19.69, 7.89 13.49, 21.26 -0.07 M0.27 24.08 C6.69 17.67, 12.57 11.51, 21.26 -0.07 M0 30.48 C6.64 25.53, 11.29 19.01, 26.25 0.29 M0 30.48 C6.03 23.38, 13.21 15.66, 26.25 0.29 M-0.26 36.88 C12.06 23.79, 21.4 11.71, 31.89 -0.1 M-0.26 36.88 C11.45 24.22, 21.96 12.66, 31.89 -0.1 M0.14 42.52 C6.62 34.58, 15.94 25.34, 36.88 0.26 M0.14 42.52 C7.83 34.52, 15.09 25.32, 36.88 0.26 M-0.12 48.92 C13.28 33.05, 26.09 18.51, 42.52 -0.14 M-0.12 48.92 C10.99 37.78, 21.49 24.2, 42.52 -0.14 M0.27 54.56 C13.01 40.54, 23.3 25.42, 47.51 0.22 M0.27 54.56 C15.47 36.04, 31.1 18.72, 47.51 0.22 M0.01 60.96 C10.87 47.68, 23.13 35.11, 53.15 -0.17 M0.01 60.96 C18.56 40.11, 36.41 19.39, 53.15 -0.17 M-0.25 67.36 C20.79 43.07, 42.42 21.37, 58.14 0.19 M-0.25 67.36 C17.81 45.33, 37.48 25.21, 58.14 0.19 M0.14 73 C25.91 45.9, 49.11 18.5, 63.78 -0.21 M0.14 73 C25 45.31, 48.21 16.72, 63.78 -0.21 M-0.12 79.4 C15.39 61.52, 27.49 47.58, 68.77 0.15 M-0.12 79.4 C17.44 56.79, 37.05 36.11, 68.77 0.15 M0.28 85.04 C15.54 68.41, 29.98 51.05, 74.41 -0.24 M0.28 85.04 C23.6 57.82, 46.47 32.51, 74.41 -0.24 M0.01 91.44 C18.03 71.3, 38.92 49.68, 79.4 0.12 M0.01 91.44 C25.4 62.24, 49.81 32.43, 79.4 0.12 M-0.25 97.84 C29.02 63.08, 59.29 30.29, 85.04 -0.28 M-0.25 97.84 C28.7 66.24, 57.17 33.34, 85.04 -0.28 M0.15 103.48 C26.17 73.61, 51 46.03, 90.03 0.08 M0.15 103.48 C34.47 63.4, 70.29 21.93, 90.03 0.08 M-0.11 109.88 C18.44 89.24, 39.8 65.49, 95.67 -0.31 M-0.11 109.88 C24.86 81.34, 47.28 55.5, 95.67 -0.31 M0.28 115.52 C33.95 80.6, 61.48 42.72, 100.66 0.05 M0.28 115.52 C27.07 85.07, 56.15 53.1, 100.66 0.05 M0.02 121.92 C39.77 71.9, 83.86 25.4, 106.3 -0.34 M0.02 121.92 C27.47 87.71, 55.48 54.73, 106.3 -0.34 M-0.24 128.32 C40.26 81.78, 80.15 34.82, 111.29 0.02 M-0.24 128.32 C37.61 86.75, 73.92 43.34, 111.29 0.02 M0.15 133.96 C30.8 98.07, 58.38 67.16, 116.27 0.38 M0.15 133.96 C27.66 100.67, 56.66 68.37, 116.27 0.38 M-0.11 140.36 C40.59 90.66, 83.77 41.08, 121.92 -0.02 M-0.11 140.36 C44.96 87.24, 91.49 34.77, 121.92 -0.02 M0.29 146 C47.5 90.78, 95.63 32.43, 126.91 0.34 M0.29 146 C29.66 110.08, 60.49 75.33, 126.91 0.34 M0.02 152.4 C35.86 111.47, 70.38 70.12, 132.55 -0.05 M0.02 152.4 C29.98 119.8, 59.3 86.19, 132.55 -0.05 M-0.24 158.8 C36.95 113.82, 74.41 71.44, 137.54 0.31 M-0.24 158.8 C44.93 107.87, 90.54 55.25, 137.54 0.31 M0.16 164.44 C59.09 100.28, 114.1 35.84, 143.18 -0.09 M0.16 164.44 C45.21 114.44, 88.37 64.27, 143.18 -0.09 M-0.1 170.84 C43.02 117.18, 86.47 68.38, 148.17 0.27 M-0.1 170.84 C30.93 135.74, 61.3 100.57, 148.17 0.27 M0.29 176.48 C46.83 122.33, 89.85 71.57, 153.81 -0.12 M0.29 176.48 C43.99 127.03, 88.54 74.59, 153.81 -0.12 M0.03 182.88 C48.59 127.82, 96.48 72.69, 158.8 0.24 M0.03 182.88 C60.61 109.68, 123.78 38.97, 158.8 0.24 M-0.23 189.27 C32.06 148.78, 65.75 112.55, 164.44 -0.16 M-0.23 189.27 C40.24 142.67, 81.3 96.18, 164.44 -0.16 M0.16 194.92 C38.01 148.88, 74.39 107.93, 169.43 0.2 M0.16 194.92 C38.3 149.33, 76.87 105.56, 169.43 0.2 M-0.1 201.32 C41.83 150.49, 86.42 99.86, 175.07 -0.19 M-0.1 201.32 C46.63 145.56, 96.25 88.59, 175.07 -0.19 M0.3 206.96 C49.1 152.74, 97.83 95.64, 180.06 0.17 M0.3 206.96 C46.53 152.54, 93.85 98.73, 180.06 0.17 M0.03 213.36 C44.3 164.56, 87.56 112.2, 185.7 -0.23 M0.03 213.36 C54.58 152.94, 107.86 90.98, 185.7 -0.23 M4.36 214.47 C64.78 144.22, 126.94 73.15, 190.69 0.13 M4.36 214.47 C52.36 157.93, 101.34 101.25, 190.69 0.13 M10.01 214.08 C53.42 162.57, 98.03 111.13, 196.33 -0.26 M10.01 214.08 C50.54 169, 91.26 123.9, 196.33 -0.26 M14.99 214.44 C90.59 131.61, 162.36 46.07, 201.32 0.1 M14.99 214.44 C59.65 166.34, 101.09 116.11, 201.32 0.1 M20.64 214.04 C63.23 162.27, 107.34 111.15, 206.96 -0.3 M20.64 214.04 C58.98 171.57, 96.82 126.63, 206.96 -0.3 M25.62 214.4 C84.94 149.41, 141.6 83.44, 211.95 0.06 M25.62 214.4 C94.43 134.68, 162.22 55.16, 211.95 0.06 M30.61 214.76 C78.58 155.55, 128.08 98.95, 214.31 3.44 M30.61 214.76 C75.03 164.16, 118.12 115.46, 214.31 3.44 M36.26 214.37 C74.5 174.12, 110.12 130.96, 214.05 9.84 M36.26 214.37 C92.39 150.27, 149.3 85.75, 214.05 9.84 M41.24 214.73 C87.17 160.35, 132.74 107.6, 214.44 15.48 M41.24 214.73 C91.24 156.59, 140.46 100.04, 214.44 15.48 M46.89 214.33 C98.71 152.16, 151.56 91.85, 214.18 21.88 M46.89 214.33 C98.74 156.45, 150.37 97.64, 214.18 21.88 M51.87 214.69 C90.69 172.15, 130.23 125.37, 214.57 27.53 M51.87 214.69 C111.77 146.37, 171.45 76.76, 214.57 27.53 M57.52 214.3 C110.5 151.43, 163.83 91.58, 214.31 33.92 M57.52 214.3 C93 175.68, 126.88 135.68, 214.31 33.92 M62.5 214.66 C104.96 168.13, 147.06 117.35, 214.05 40.32 M62.5 214.66 C109.29 158.93, 155.94 104.22, 214.05 40.32 M68.15 214.26 C105.89 170.44, 144.19 128.05, 214.45 45.96 M68.15 214.26 C116.92 156.96, 165.72 101.69, 214.45 45.96 M73.13 214.62 C121.61 157.37, 171.93 100.84, 214.19 52.36 M73.13 214.62 C123.2 156.26, 172.77 97, 214.19 52.36 M78.78 214.23 C123.99 162.65, 168.54 112.83, 214.58 58 M78.78 214.23 C126.69 159.63, 175.57 104.04, 214.58 58 M83.76 214.59 C125.13 166.22, 168.45 116.61, 214.32 64.4 M83.76 214.59 C131.17 159.77, 180.03 104.16, 214.32 64.4 M89.41 214.2 C127.13 170.54, 163.12 131.23, 214.06 70.8 M89.41 214.2 C116.58 182.86, 142.76 153.97, 214.06 70.8 M94.39 214.56 C129.16 172.19, 168.93 128.63, 214.45 76.44 M94.39 214.56 C123.53 182.2, 151.09 151.6, 214.45 76.44 M100.04 214.16 C143.57 163.36, 190.88 114.02, 214.19 82.84 M100.04 214.16 C126.89 183.31, 153.16 153.27, 214.19 82.84 M105.02 214.52 C142.5 172.88, 182.51 127.18, 214.58 88.48 M105.02 214.52 C131.4 183.31, 158.54 153.16, 214.58 88.48 M110.67 214.13 C141.86 175.44, 175.2 139.12, 214.32 94.88 M110.67 214.13 C152.79 167.39, 193.06 120.28, 214.32 94.88 M115.65 214.49 C148.13 176.9, 179.74 140.07, 214.06 101.28 M115.65 214.49 C148.07 178.59, 180.64 142.78, 214.06 101.28 M121.3 214.09 C145.71 188.17, 167.91 157.36, 214.46 106.92 M121.3 214.09 C147.71 184.83, 171.36 157.26, 214.46 106.92 M126.28 214.45 C146.54 192.43, 164.94 168.59, 214.19 113.32 M126.28 214.45 C144.98 193.3, 161.8 171.97, 214.19 113.32 M131.93 214.06 C155.88 185.67, 180.21 158.65, 214.59 118.96 M131.93 214.06 C151.85 191.06, 171.09 168.01, 214.59 118.96 M136.91 214.42 C160.1 188.38, 182.21 161.51, 214.33 125.36 M136.91 214.42 C158.68 191.07, 177.98 167.43, 214.33 125.36 M142.56 214.02 C169.06 182.79, 195.5 153.12, 214.07 131.76 M142.56 214.02 C166.82 186.11, 192.2 158.08, 214.07 131.76 M147.54 214.38 C167.86 193.37, 186.97 171.57, 214.46 137.4 M147.54 214.38 C164.09 194.7, 181.74 174.84, 214.46 137.4 M152.53 214.74 C171.53 190.99, 190.47 172.46, 214.2 143.8 M152.53 214.74 C165.32 200.71, 178.49 184.48, 214.2 143.8 M158.17 214.35 C171.73 200.55, 184.23 185.22, 214.59 149.44 M158.17 214.35 C180.24 188.99, 202.79 163.92, 214.59 149.44 M163.16 214.71 C175.07 199.76, 187.35 187.33, 214.33 155.84 M163.16 214.71 C177.61 196.45, 193.13 178.88, 214.33 155.84 M168.8 214.31 C177.01 204.44, 189.05 191.27, 214.07 162.24 M168.8 214.31 C180.2 200.58, 193.81 186.51, 214.07 162.24 M173.79 214.67 C186.68 199.8, 199.12 182.17, 214.47 167.88 M173.79 214.67 C184.37 203.24, 195.74 190.73, 214.47 167.88 M179.43 214.28 C186.54 206.43, 193.87 194.75, 214.2 174.28 M179.43 214.28 C192.03 200.39, 202.21 186.14, 214.2 174.28 M184.42 214.64 C193.06 202.86, 202.37 194.14, 214.6 179.92 M184.42 214.64 C191.6 207.41, 197.45 199.78, 214.6 179.92 M190.06 214.25 C197.36 204.92, 207.53 195.21, 214.34 186.32 M190.06 214.25 C196.03 207.43, 201.66 200.16, 214.34 186.32 M195.05 214.61 C200.06 210.45, 203.81 203.75, 214.08 192.72 M195.05 214.61 C200.37 208.28, 204.8 203.01, 214.08 192.72 M200.69 214.21 C203.31 207.7, 208.66 205.6, 214.47 198.36 M200.69 214.21 C204.63 209.52, 207.41 205.7, 214.47 198.36 M205.68 214.57 C208.36 211.42, 210.09 210.31, 214.21 204.76 M205.68 214.57 C208.96 210.99, 211.69 207.87, 214.21 204.76 M211.32 214.18 C212.2 213.32, 213.44 212.01, 214.6 210.4 M211.32 214.18 C212.31 213.55, 212.84 212.38, 214.6 210.4" stroke="#15aabf" stroke-width="0.5" fill="none"></path><path d="M0 0 C56.1 -0.99, 111.94 -0.97, 214 0 M0 0 C76.32 0.56, 152.1 -0.37, 214 0 M214 0 C213.17 51.96, 213.7 104.26, 214 214 M214 0 C216.19 79.54, 215.05 161.15, 214 214 M214 214 C134.35 212.91, 56.14 213.64, 0 214 M214 214 C132.56 212.83, 48.96 213.92, 0 214 M0 214 C0.52 161.49, 1.36 109.33, 0 0 M0 214 C-0.59 157.45, -0.74 100.81, 0 0" stroke="#000000" stroke-width="1" fill="none"></path></g></a>" +`; + +exports[`exportToSvg > with exportEmbedScene 1`] = ` +"<!-- svg-source:excalidraw --><metadata><!-- payload-type:application/vnd.excalidraw+json --><!-- payload-version:2 --><!-- payload-start -->eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1WW2vbMFx1MDAxNH7vrzDaa1llJ2myvGXrLoWxwTIorPRBtY5tYVlyJTmXhfz3SvJiOV7Z81x1MDAxYeJcdTAwMDfD+c5Vn76DvbuIXCJktjWgeYRgk1x1MDAxMs6oXCJrdOnwXHUwMDE1KM2ksK7E21o2KvWRhTH1/OqKS5tQSG3mI4xxm1x1MDAwNFx1MDAxYypcdTAwMTBG27B7a0fRzr+th1GXurpTT99cdTAwMTdZyVx1MDAwNE2e0mr69Wbx+ZdP9UFcdTAwMWJcdTAwMWIzjsedvXXdR9POXjNqXG6LxVx1MDAxOHdYXHUwMDAxLC/MXHUwMDAwJFwi527WgGijZFx0XHUwMDFmJJfKXHLyXHUwMDA2+ye0fiRpmSvZXGJcdTAwMWFi4lx0IY9ZiMlcdTAwMTjnS7PlLVx1MDAwYiQtXHUwMDFhXHUwMDA1aNDh7jDiXHUwMDAw7/K0tCyHLNsyL1x1MDAwNGh9lCNrkjKzXHUwMDFknMrNV99Sz+5DmEqRXG5uXHUwMDFkvaLhvF9Y0D+Fj1x1MDAxY5Z7cEQj0ju+XHUwMDA2oL7bOL6eTN/hWedcdDqIXHUwMDEzPES/SeE1XHUwMDExx7NcdTAwMTnG42R6XHUwMDFk2uhcdTAwMWKrXHUwMDA248tmhGtcYlS7yT5cdTAwMDalXHUwMDFjTdfUlLRJgVxyzkQ5jLPqK1+ofVAzZaSSglwij+8vz1r837VcdTAwMTifrlx1MDAxNoFzVms4a/G1aDE5XS1cdTAwMWHYmN7FSWGW7Lfz9M7g0E+kYtzxPFx0JVxcqi0hXHUwMDE1y5kgPDqudYB//jvMWVx1MDAwYs5yR1x1MDAxY+KQ9VxcljvD7D9F5zayXHUwMDBl3tRORZhcdTAwMDD195VaXHUwMDFl4Esn97dJmJk0Rv5cdTAwMDDdXHUwMDFl0TNyXsJXsoSj81x1MDAxMnZLXHUwMDE4XHUwMDA2P6UltG8vXHUwMDE0ROp6aSyx1t2uJFoxWL9/QfSZf9yX1K+wXHUwMDEzPbhb2u0v9s9cdTAwMGXWRI8ifQ==<!-- payload-end --></metadata><defs><style class="style-fonts"> + @font-face { font-family: Excalifont; src: url(data:font/woff2;base64,d09GMgABAAAAAAf0AA4AAAAADbQAAAegAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbgjgcNAZgAHwRCAqPYItcCxoAATYCJAMwBCAFgxgHIBujClGUblKX7Edh3Pg8OJbt5MlDFPwWk6FmJ08terdayeDx9O8Fa5eunTJ1ykAUGuAvgOF5bPfuH0nXrvQrNkFoNlERBLIQDd/mj2kP7fdg8avL4M/sm4cbu4L2i1VQVjShz/+90/YE3IJFmOC+f0Z1Xhs3bVxiJZZGa1IgY5sHCJ2EM2iBndkLWsZvFxCAhRBGCknKEOQenHCpmE2fVQTl1atzWyjvzs3aQPnWd20PJQZeG3mQp1lnLimFBAhOKFIQSrGa+vOB69gLCRkjLr4NIjd5rXiz8hhu+LIe9DZCgkJjmZEy34MkhXNBJ8GhNMMnJUgmSYA0AowAwT2zRor05iZAeN/80c4UkYlgSTOaKgOQCUOSE5F+z6+DXqndDTk4CEHP2v3/aKw7lXQXgAUAQP5+kgIpTYMHT9MEjIyWxJ5V5jAzgnrNtNBKWx101tXee86rA5O1f1fiIzEtJsS4GBMjYlikIhEvwwXDjYgiApUFdjgCFAA1m2GorwgdA5a6fzxxX0ei+Eqw6MspHfbmte7eLSh2FSEREgnySedVN+VfsbKAVQygCrGALXgDQoSA0kZqfB2OiyyaZolc/i3tnXswySlmifL9eo1ReyWQcYadAK/DD9c80uaI4pamykJM2hp/MF2uj+teZm9LiRlrltgBlcVYlWVWf+WPTpVJj40Kz9Gvi5p21Zcov8bKEL+5Uirz+j2Hjzr+99zosad2XtbpfsbKgyIQ3RJHxrTH1ysQQAjTUaii13B+OTtxrFoLubjpMYtit2uul6DdSPvKaQl/9FkppthYCXEvjoEdStVVjti2fUQf34q9YjychmIsxCAjKkj0GW4mIhlX1snPOEVRVq4UMRQwkzJNhK143IsBOlghz9meJAGf4apbAmYRMj2hsj5X1OGDPuIKXoOyN8yIMYbeusXTa9f4a+1UvYG9FgQxzoQBUoSPD5/ezAuQIP8s8IyVrUGrwSxC95WPAdlx3EkkQq5yNjcaKAAZ4LEHheIMvgHV3fe6X41/6rzCiMBwSxrnvl9RcR0gK5fy4jDVigBySy99iwlppT9aTlWIMSMKBx9WkbupSbZXNC/CKUC6z53jA0mS7340zruutV7xYguW6p/01tYHOQZQ7ptAagFiLAzFWqyWUwN8/eNGSyn2elPguvF9At0XUNlNv4t202nq+EnkKvdW6aLVlJUxhpXYUqMsn5svp9eoTpvKKf6BY8QwAkaJWYSggEuximEYiCVE0TjN5ojkVoznAP0GytsVT1SesrOn/s8LsMpVpXtSc6To4sT6XIl1q0jH96rd5/g5vmi35+y6eeYPNqjBGHtX0nKFGywfPr19aO9Gh/Q1sWZGuqckOoOJmlK2pSPTSgm/46h389ap+VmOnxsPdspP82Xyy2l7u6i2908pVjRkDbEddOKOprOPJ/94eJ59fi6jDtB6VbzuwXb5puJcqcKnUeM72fFnIlssAv/Z6uMq2s1Jd8qpd7DZo538w5PG7cHm2TU2TD41PjYiaUSikYspa/kjJ84AdgqXsT226/L+fW6j99QmJZlWN99tfcdbUPO17tLu8+gVw73h3OfmurdS9BCa5twNfokJhx7/agK3fHC9rXVewbT0kZG9vXqRBOtS6xGOZ5dC3pwleZIyidL1le+xC3btOlwJkXqGESosoDUG2TqcuE/6OcolnOykkpKdD+5l7+JODWBpAycPke+STDMRWidsWCLvfQJbKM7JpnkLG1WV73qjg6eZ1EMVF1NgG5rTaZF2r7tJNs2VzabNrTQfhSOtgxRxRLP5mldJB6/b6R5LWk9ss9nNUGyTTx8iytdCS9xlAlvtN/1hTrYObLVe5T8zLWNk2oko/bq0dv1cvjXrGC5k+pfYm+L1MtXCFsSzKDTY/dhu66JJWqlfkIxw7T5ufjDBa4pCy7ObPC0cojzlZMlCh2VjKwfX8qy5cpyRt1hp6DeKs/UZjc6zfGxC0rJHl1IUtIm2p7Tg+6VKn8vbfWR9fHljiebwTXPXTQ13HmoSbMssDsa32NnSfmmoH92xuU/E7U2qS8vWYFD9D73VJodqvWU8l7tB+b9ZdPiXvC2qoboerfoamG78+oOX7uWn1/WJy87rJPSHcVmUO7sN0ypzcG5zdfzU8k/NSh5OCbHvUSJlOk2xGrkmubA+nV7YtUCdoJk2vZ/dNSvrAkFnE0toKlC7rHllSrrOzt4Yb91LaLga9TtY+etg/uCpE8rVr604yv7YaE2x450wAACIfUvVNvFrrWXyNylLvwSAR70CtQDwePGbPuLQ/32Y10wCAClKi8AXKzralCj+/e0Dwri9RW2ArkwEqBW+cYJzghDkKyuPEB0FElKG8DIXEQkASS4s/2PSN2DGs1Bi6eh9EhVoZDAH0NblyEbw9tsoChtttEDDbQxvDTZWKm+YcYcDOj01qtdWK8110F5XIQq+nMJuwnpSopmb7KJVWbUgQqhwkyQxlOd76aglFYEKoxAJ8OeFkYPjBPTuQejsbXStQgY5klp1cr1LoQHSUa8StKqVbKnrXAqNAgg8wm1EB6RBL7ejLWnSmSI9hGJQZdAW2trTXRJoBsJm6M5VNQlFM3yJ/7EAAAA=); } + @font-face { font-family: Excalifont; src: url(data:font/woff2;base64,d09GMgABAAAAAAHcAA0AAAAAA9gAAAGMAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cNAZgAAQRCAoAKgsEAAE2AiQDBAQgBYMYByAbHQPIrgp4MjSeIQLqpl4jnPFQwveqm205gmrZevZ2/8kKGaJ6ictBuCwMMnsKo8hC4RwaLBrJ7dXaFrFk0Re1e/Dk3t9wCUVrYAiZkgiZ5rGDVFdY04wBF4APOCzXHr/ljrNIDqLAY/slkQf0SyjxLNBaAq8Z1AJqYW5j0GjbckRLQhoJCgwYoyfY3q33QS5DrSAwYB4hFruNve4WoN2On4P29GEFWsC+nVM/UvSlVwIcsWycI/5gbqfUhE/9Q2P7UppTOAVeIR4TYIocELVKIEACZJgXCJ5GXl0o+esbFP3g+763DcDPx6/60MrnBaBPAsGPUDRC/8+FGIqGUSBA+NIJOwTWe13HRCIrgEvfRhj1RjLiE42eG7I5DIpVNuqkNNhxwaItTI2srRz4dfHGjhZoO0O8nb2pilFYQKhenFaycLUxsYcoAQRyoRBEnPvcgfysPq+npClt8UxLyvHWjaudqbGJA+TCckNECBGBGFforuTs0M4CUMbCAvp+P4in4o864XECREBtFQAAAAA=); } + @font-face { font-family: Nunito; src: url(data:font/woff2;base64,d09GMgABAAAAAAIsAA4AAAAABLQAAAHYAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wPIrgp4Mt6IIcJZFNCfWmZY6KzqazTt6AiVHCFawzJ7V0CQBKoSQQEpFKrCVxiSFbqqqsaTevKs/q7s6uBYmujMyA9wxb6a7XnEOcNnDLgG4nW8PoPzHzYpS2uqp51pL3eB/xZoxIllCQc80B9o4j/4xMbxQB+j+SC3hsm6JmI8RMaHj+aJApW6ZbkXlg4vXSE5FECg0og6LzxP9pOarug4tF1RLpbHeZqLX0pIt2mfy3pNG6eyGaRIjrnrr/gv2c//yGdjpJ/7DuJLin5eIZRLaObBMM/NpYpuXJ8z3SE088mEFANcCARESfwChCioAwESsVxBgeyxp6+vZ3Xzv8uz7Ae8tRk+p6RUTPOR7BmlEgh+STsAimNuKibilyluhBNe5/wCACSBKrcykVfgyb+RYcK/TGq9yMyCt3bOskSnR1FqTLMSVBsMGLQVltDKKw2IYA+wcGxHJCINY9mDOCaN4IZEo1ChY4xNg8DaB0RxDqnbMNjXJJRzF9iInqahtm67M8dOHFvXaYbn+wrGxKG3YZI+2V4CW58ovdfV1tFHXFJJiPH7T1FAJxEgYo5BKkA5iDIVQluOqZYWhQapGF6TAFhaFAAoTBIZsCFHi/0oV3gpVKwbAA==); } + @font-face { font-family: Nunito; src: url(data:font/woff2;base64,d09GMgABAAAAAAI0AA4AAAAABLQAAAHeAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wPIrgpsY9TYDjGuJ1F+MUed8Q+Do8lDKF1uhErOw/f7/X7tcy+iEk0SUSWRLDGExBCqZYuETCJVS3jlW/rvPv/y4Fra6szKjvjBubd/Nui7lGEMuAbidbw+g/MfFilLa6qnnWkvd4H/FmjEiWUJBzzQH2jiP/jExvFAH6P5ILeGybomYjxExoeP5okC5TpFmecWVieTSw4FIFBuSJ4Vngd7SlWTd4eom7xMLI/zNBO/FJHO0i6T1Zo29kTTT5Ecc9Nf4b9kP/+RT8dQL/MVxGuKXlYhlEpoZMEgy8mRiGYc/001h9DIJhNS9DEhEBBF8QsQokANCJCI5QoKZLddfb/er5n9XZpln/DYYniZkJR+fpjdo1gCwS/pEkByzEzFRPwywYywwWaOVQCAJFDhTCbyMtz5NzKM+ZdJtQeZmfPVzlkWafcoio0oVoJK/QGDNsMiWn6lPh7sAJo3ujXOJwmjmUNvVJIPNzgSgQhtMDoJAmM/EPVmRd2Cwb7gUMZdoCNqiprKqu32bGxtjOs0w8O+gFG9obdhkj7ZHgJXvyi9V1VWUUeciDjE+P07FJBxPiCij0EiQBmINBFCV4aukhKBBImYd0UfgKZEAIBAxeEBHTKU6I9yhpVChXUCAAAA); } + @font-face { font-family: Nunito; src: url(data:font/woff2;base64,d09GMgABAAAAAAIsAA4AAAAABLQAAAHYAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wPILgps9xRFkFEihAtyRcm3sWFhmEeo5Dz8tx/rvpmPqESTBE0lkSBySERCJxSLhLR5U7Uk2jzt3GfnB66lLWNU8Qd04uTtzX70HcgYcA3E63iUZs1n9bQz7eUu8N8CjTixwAIOeKA/0MR/8IlNNx7oYzQf5NYwWddEjIfI+PDR3FGg0bAs9+b23pkrpIJSQKDRjCovPE/2jZqu6Di0XVEulttVWotfakj36YLLQQ0bl7KZpEiOueu9/Jf8839UqzEzzv0A8SF9YJxXCPUSunkwzXN3raIbN+dMdwjdfLKQYoILgYCoiV+AEKVoAwESscqBAvl55Pju68dV2/rv+py/wJu+4f2SMlM2UqnaudRKIPgl5Qwg9QWdJuKXJW6EE163fAIAZIEm97KoGvDi38iw4F8mrV7LbNNzvWJZY9CdqDWnXgiaTQYM2gtraOOVJkRwDlg4tn0SkYaxbEMck0ZwSaJRqNABxqZBYOkVojh71KwZ7AsSyrkLbERHXV9Tu92JA4cOLMs0w5N1BWPi0Fs3SZ8sz4GNN5Tea2tq6SLOqCTE+P1HKKCTCBAxxSAVoBxEkQqhDcdYQ4NCg1QMr04ALA0KABQmiQzYkKPBfpRLvBQqrwwDAA==); } + @font-face { font-family: Nunito; src: url(data:font/woff2;base64,d09GMgABAAAAAAIoAA4AAAAABLQAAAHVAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wMRFZQHZF8mb0PNXjkIbeHQ4UpbWDTlYMnK5cOfavAw/zzrESo5EbTf6+zdCyBIAqHiUlkJRMJHmIKskVWRFZ4Uelb50Iz7S0IKNNGB0oyDDbgkjnxA+VCvYpk1vog5la7RoxGwSoMFQHNIwf6bvMXbb7uB/zbQEScWUOABB/oDJf6DJ5YWB/oY5UFuDZN1TcR4iIwPH80dBd1mzcrh6sxZnRpoDQTd1qyp8Hzy6v/JPaP30LMHRmG53ZSD/CblWFLgqIbNBWE2y5SSY+mk9V/qr9Nr9rO2mFcC8rUMi6qITgWjClaVl+6tZxG4/ezyvYhiVKOKkiUQBNKW3wCR1gQCConGgYL6Vik5uYHBH+T6E3yc6OHLTplP/6g2k2hXQPgla1sH3lJf6DTJbxPEedV9bQIAqqDHcZWmC+/+j44tH3TR772uDr2oN6zbTDun2m3YKIReywFCxkEb6b7Skm4ghBglR9vyebxlKPLNt+27HjvxxGew/DGCDtQsGvyXTehdSPR6qVWpddg/nU/LMs3gtu7yCJFbt56xvjyH9E/ovVql2tAfnq0bv/9CILBNpk8584BQPeMxltJeuez6zONGyYS47AK4ke1Awmg5eZRnZSUd); } + @font-face { font-family: Nunito; src: url(data:font/woff2;base64,d09GMgABAAAAAAY8AA8AAAAADAwAAAXkAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhwbgkocZAZgP1NUQVREAHwRCAqKAIgNCyAAATYCJAM8BCAFhCQHIBv4CVFUchKQ/TyMjWWPhbGVsLhp9GSH50tTI0kpiVprztv5Fg//7Ue7b76siAPJNIlodGtQN2PVLBMSh9QsiSdPzOBa2yGm7yb77eytNyKhUQpUht7Tv2xOByU3QenAtgPMB7F4fyAboXSW9/+/n6v/TjQN8ThtnIUEjdIpZXKR+wRP5pOEmJw+KE2scl5CrJImJdM0EiorlZ5YdBMRC560xeTjuPbtLy7QCkiSUAq1ViHQbDaL3Q2bEgVwd6KYhuDuYjSLwJ09yP8F4sIpGoEjBEIpaZS81yy5rcQrjzVpEJ1J/k3+gsP/ff8Il5G/2fNPGkF4pUAjvJfNawrz6+6nByVFqa9kYlFhOgoleq5SgazextGgsaNdiThmeeBJXBYr4z15AL5i/ioJvcwoWg7EgX1xjpO37MHbHk9cvNKHBpa8cWVIoeH9ZVoeX1Sk6ykIsL08SRz2FoqiqiH0H80ZBl8mbYqf/59QLgvtScQdiPfpNCbtCqFRQq+nzZp37cyEJfB7ESNYvPedtBiOKeCsCogqfiETZge9IpzwbzwL6HdlV21S19DYFITRWJv8H8yErsk/8lQez2N5NH4HI3koD+Y+IfILkbMOmItxUMQzUoFWr05wQJLEsdEC75K6R820tQUE+AQklUUEZHTU+fxbmEyk+i6LRAJoC/0YmL5Nar15FkGqz7RnmvctjIyMwqOsoReDAVLV0rwVJEhjp+jRUxhr1HGbAUa3IvzK1cwbXoQOVDGwuSytwfn+4SGxQ2JzLAdGRkcXscPyBAQhTdHRueyuHlEQQ6pXy5nLIFXRQ1pZLJUzlwhocf+WFaTAQxtDBLYC2io05Fdg1p8uIoNwsB+MJSr3zYkchu8OAaX+TjFMB6rugYymaXp0oIATKKGzrMDBZYnhJWXlS/KliW0IeizZcjbhgO1oMziJbZFUPCTWtzd6RCoQBNwf5Rk9j/mjgYOLfD84LO6H+PcPLaHXF/AAkl/fHOct//vHPIZeatoOmqbhlS9pepY6lKBzQzm1aH3VPlmNXKsvFgpOwj7P/M5wzdP3A0RUCV+Jhh/5CAZiYonQhTdv314aaKBlOWyyKAlHdorbyYAqXy4dZC4kLxJgacdvLOs6r63voK3kZcIV4l/PcsiCT15U/TMevMcVIg6UbhoHmy7WOT1wXDhEE1Z2cY6E4DErivD4ug6TNFezbFAHTdGQiPT+rAfBAhwrgCPZOdT6SsdeXdHTmEap+SEbKnmYUKl1kF9ixbBTvAjDi8Q7YTIeJvVGXPKVlMwVSfFrRErAQx6USuEfFmB4AXxvPKPOeytqahwT5VCsEgL5NVYIh7RnYQnQZ8M1wmpj5aGh4k8+gIk4brwGIaTM7D1T/l89ALkmOy2yKTb5MBsigGfagSxd+I0mLtr2Goo0qyBK3Se3V1eCadQGKKzhwsmjoL8DbcjsH/TZ2UwJIYLL2cX9CiYCEVZUDVWa5Td7EUMhYQMR+3ReC44nK7341kBYyFC0+J0Zy0R0AJ2+LirQcHa3APhUgt78p9aYOtMXb8kQ/JHUseYN+SQyliokRGB+U+fVV+/elyG/lb4jcj1HRtNAyNBRcYlDEpJLEhIroFYlYR+19qt2YF3+7UU6JVDcHxum1ZyNvFzxG3vxGV4PxIB3izP535BXlNqlEKCWQPBN0h6G1nAEQvSyjvhlccIYEk6RqBT+rLBhq++6mCeaVmSnhRSRLhqn858kQhmckK8KdDho33l762L3oRbXFaJswnP/GldY6NuVtHvpKmyw2lmyXZVhDWdtPtWkoNUMGcLYD5WxmaYg9eyDLXQhSUMDU0sLUawuaKqvbGhqbIKVYjiQaQQL2njT0LoS/m7lCr4lQzTmuxjCoadioqGD8nYpaSnBrVwZcjQGguzd9Lz9OWZlztV+GGmLZo3W0dAyABUGMFy9YkQGraQZyvui5GUQK8TxDTs4JGuoWSSGjZraeB5NvHiPSv+dQ4gKw8agodG7ECucqIZ2GpDaFVWXt4wC); }</style></defs><g stroke-linecap="round" transform="translate(10 10) rotate(0 50 50)" data-id="vWrqOAfkind2qcm7LDAGZ"><path d="M0.32 50.63 C0.32 50.63, 0.32 50.63, 0.32 50.63 M0.32 50.63 C0.32 50.63, 0.32 50.63, 0.32 50.63 M2.68 54.01 C17.68 37.03, 33.95 20.86, 40.74 10.24 M2.68 54.01 C16.59 38.38, 31.52 22.36, 40.74 10.24 M5.7 56.63 C19.86 43.02, 33.12 27.27, 52.94 2.29 M5.7 56.63 C23.06 37.39, 40.39 16.53, 52.94 2.29 M8.72 59.26 C23.62 43.85, 37.66 28.7, 55.96 4.92 M8.72 59.26 C27.67 38.75, 44.88 17.7, 55.96 4.92 M11.74 61.88 C23.55 50.54, 31.34 37.36, 58.98 7.54 M11.74 61.88 C25.4 47.12, 38.59 32.54, 58.98 7.54 M14.76 64.51 C25.64 54.29, 34.7 42.69, 61.34 10.92 M14.76 64.51 C25.83 51.81, 38 38.36, 61.34 10.92 M17.12 67.88 C34.34 49.26, 48.5 31.65, 64.36 13.55 M17.12 67.88 C33.86 49.33, 49.44 31.94, 64.36 13.55 M20.14 70.51 C28.87 60.06, 40.4 48.29, 66.72 16.92 M20.14 70.51 C29.76 60.14, 39.05 48.61, 66.72 16.92 M23.16 73.13 C37.73 55.91, 51.72 40.01, 69.74 19.55 M23.16 73.13 C35.28 60.9, 46.76 46.19, 69.74 19.55 M26.18 75.76 C38.75 61.93, 48.87 47, 72.76 22.17 M26.18 75.76 C41.17 57.48, 56.57 40.42, 72.76 22.17 M29.2 78.38 C38.58 66.84, 49.28 56.06, 75.12 25.55 M29.2 78.38 C45.23 60.41, 60.56 42.59, 75.12 25.55 M31.56 81.76 C48.47 62.24, 65.95 45.31, 78.14 28.18 M31.56 81.76 C45.84 64.05, 61.69 48.32, 78.14 28.18 M34.58 84.39 C53.9 64.75, 70.59 44.84, 81.16 30.8 M34.58 84.39 C52.97 64.26, 69.66 43.17, 81.16 30.8 M37.6 87.01 C48.25 74.78, 55.5 66.41, 83.52 34.18 M37.6 87.01 C48.99 71.83, 62.27 58.43, 83.52 34.18 M40.62 89.63 C50.34 79.26, 59.13 68.4, 86.54 36.8 M40.62 89.63 C54.92 72.51, 68.88 57.45, 86.54 36.8 M43.64 92.26 C53.65 80.81, 66.89 67.98, 89.56 39.43 M43.64 92.26 C58.66 75.45, 72.42 57.89, 89.56 39.43 M46 95.64 C61.79 76.78, 78.15 59.99, 91.92 42.81 M46 95.64 C61.9 78.99, 77.17 61.29, 91.92 42.81 M49.02 98.26 C62.86 82.2, 75.55 68.59, 94.94 45.43 M49.02 98.26 C66.35 78.07, 84.72 56.67, 94.94 45.43 M61.22 90.32 C67.86 83.05, 77.21 73.12, 97.96 48.06 M61.22 90.32 C71.29 78.99, 79.23 69.82, 97.96 48.06" stroke="#15aabf" stroke-width="0.5" fill="none"></path><path d="M51 0 C63.55 12.43, 75.75 25.91, 100 51 M51 0 C68.7 19, 85.84 36.05, 100 51 M100 51 C86.33 62.39, 74.98 74.41, 51 100 M100 51 C82.86 68.81, 63.17 88.61, 51 100 M51 100 C30.58 80.76, 12.2 63.63, 0 51 M51 100 C32.58 80.36, 11.39 62.79, 0 51 M0 51 C13.05 38.39, 26.48 26.08, 51 0 M0 51 C14.03 37.59, 27.42 24.3, 51 0" stroke="#000000" stroke-width="1" fill="none"></path></g><g stroke-linecap="round" transform="translate(10 10) rotate(0 50 50)" data-id="vWrqOAfkind2qcm7LDAGZ"><path d="M16.06 13.96 C16.06 13.96, 16.06 13.96, 16.06 13.96 M16.06 13.96 C16.06 13.96, 16.06 13.96, 16.06 13.96 M4.65 33.19 C11.77 27.87, 17.81 20.43, 26.3 8.29 M4.65 33.19 C12.84 24.59, 20.75 14.56, 26.3 8.29 M4.39 39.59 C14.94 29.16, 24.78 18.84, 37.19 1.85 M4.39 39.59 C17.83 25.46, 29.51 10.78, 37.19 1.85 M4.13 45.99 C13.58 37.43, 18.93 27.04, 40.86 3.72 M4.13 45.99 C14.89 34.56, 25.16 23.35, 40.86 3.72 M3.86 52.38 C13.79 43.36, 21.85 32.91, 45.85 4.08 M3.86 52.38 C13.79 40.95, 24.84 28.8, 45.85 4.08 M0.98 61.8 C19.05 42.22, 34.04 23.64, 50.84 4.44 M0.98 61.8 C18.6 42.22, 35.05 23.82, 50.84 4.44 M2.69 65.93 C13.07 53.65, 26.22 40.02, 56.48 4.05 M2.69 65.93 C13.71 53.84, 24.49 40.6, 56.48 4.05 M5.05 69.31 C22.52 48.71, 39.47 29.41, 61.47 4.41 M5.05 69.31 C19.68 54.34, 33.61 36.81, 61.47 4.41 M7.41 72.69 C23.29 55.19, 36.64 36.55, 66.46 4.77 M7.41 72.69 C26.51 49.67, 46.1 27.78, 66.46 4.77 M9.12 76.83 C21.59 61.65, 35.54 47.13, 70.13 6.64 M9.12 76.83 C30.41 52.84, 51 28.96, 70.13 6.64 M11.48 80.21 C33.9 54.33, 56.91 31.04, 73.81 8.51 M11.48 80.21 C30.8 56.74, 51.74 35.15, 73.81 8.51 M13.84 83.58 C39.61 56.49, 62.81 29.08, 77.48 10.38 M13.84 83.58 C38.7 55.9, 61.91 27.31, 77.48 10.38 M16.21 86.96 C30.75 70.22, 41.87 57.39, 80.5 13 M16.21 86.96 C32.53 65.84, 50.87 46.62, 80.5 13 M19.88 88.83 C33.21 74.39, 45.68 59.3, 84.17 14.87 M19.88 88.83 C40.06 65.13, 59.82 43.4, 84.17 14.87 M23.56 90.7 C37.96 74.48, 55.4 56.83, 87.85 16.74 M23.56 90.7 C44.27 67.09, 63.87 42.81, 87.85 16.74 M26.57 93.33 C48.43 67.31, 71.05 43.33, 90.21 20.12 M26.57 93.33 C48.35 69.95, 69.55 45.41, 90.21 20.12 M30.25 95.2 C48.46 74.18, 65.49 55.57, 91.92 24.25 M30.25 95.2 C53.67 67.88, 78.29 39.3, 91.92 24.25 M33.92 97.07 C45.33 84.45, 59.49 68.99, 94.28 27.63 M33.92 97.07 C49.95 78.85, 63.69 63.01, 94.28 27.63 M37.6 98.93 C57.98 79.1, 72.57 56.11, 95.99 31.77 M37.6 98.93 C52.82 81.56, 70.36 62.86, 95.99 31.77 M42.59 99.3 C62.5 72.18, 86.66 48.61, 98.35 35.14 M42.59 99.3 C56.8 81.15, 71.26 63.76, 98.35 35.14 M48.23 98.9 C67.5 76.43, 86.2 53.85, 100.71 38.52 M48.23 98.9 C66.04 79.87, 82.62 59.16, 100.71 38.52 M53.87 98.51 C66.57 83.45, 76.58 72.74, 100.45 44.92 M53.87 98.51 C64.66 85.25, 76.57 72.6, 100.45 44.92 M58.86 98.87 C72.85 81.35, 88.43 63.51, 100.85 50.56 M58.86 98.87 C74.11 80.25, 90.51 62.43, 100.85 50.56 M63.19 99.98 C76.96 84.2, 90.81 65.29, 100.59 56.96 M63.19 99.98 C71.88 89.37, 81.43 79.1, 100.59 56.96 M74.08 93.55 C81.37 85.47, 87.74 76.3, 99.67 64.11 M74.08 93.55 C79.97 87.9, 85.67 81.47, 99.67 64.11" stroke="#15aabf" stroke-width="0.5" fill="none"></path><path d="M38.1 1.46 C45.72 -1.06, 55.49 -0.08, 63.58 2.19 C71.68 4.46, 80.88 9.29, 86.67 15.09 C92.46 20.89, 96.33 29.02, 98.32 36.99 C100.32 44.97, 101 54.87, 98.62 62.96 C96.24 71.05, 90.17 79.66, 84.03 85.55 C77.89 91.43, 69.57 96.06, 61.79 98.28 C54 100.51, 45.12 100.97, 37.32 98.91 C29.52 96.84, 21.11 92.03, 14.99 85.89 C8.88 79.75, 2.75 70.16, 0.63 62.05 C-1.48 53.94, -0.22 45.19, 2.3 37.23 C4.82 29.28, 9.28 20.51, 15.77 14.3 C22.25 8.1, 36.21 2.25, 41.23 0.02 C46.25 -2.21, 45.49 0.09, 45.88 0.91 M40.94 1.53 C48.29 -0.28, 59.04 0.38, 66.96 3.27 C74.89 6.16, 83.12 12.22, 88.48 18.88 C93.85 25.53, 97.82 35.15, 99.18 43.21 C100.54 51.28, 99.41 59.58, 96.65 67.29 C93.89 74.99, 88.87 84.03, 82.63 89.47 C76.39 94.91, 67.44 98.55, 59.2 99.93 C50.97 101.3, 41.25 100.52, 33.2 97.7 C25.15 94.87, 16.16 89.68, 10.9 82.99 C5.64 76.3, 2.84 65.86, 1.64 57.57 C0.45 49.28, 0.99 40.72, 3.74 33.24 C6.5 25.76, 11.77 18.2, 18.18 12.7 C24.59 7.2, 38.36 1.99, 42.22 0.24 C46.09 -1.5, 41.49 1.56, 41.37 2.2" stroke="#000000" stroke-width="1" fill="none"></path></g><g transform="translate(10 10) rotate(0 50 50)" data-id="vWrqOAfkind2qcm7LDAGZ"><text x="0" y="17.619999999999997" font-family="Excalifont, Xiaolai, Segoe UI Emoji" font-size="20px" fill="#000000" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">original text</text></g><g transform="translate(10 10) rotate(0 50 50)" data-id="vWrqOAfkind2qcm7LDAGZ"><text x="0" y="19.08" font-family="Nunito, Segoe UI Emoji" font-size="20px" fill="#000000" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">original text</text></g>" +`; diff --git a/packages/excalidraw/tests/scene/export.test.ts b/packages/excalidraw/tests/scene/export.test.ts new file mode 100644 index 0000000..6fd8a8b --- /dev/null +++ b/packages/excalidraw/tests/scene/export.test.ts @@ -0,0 +1,521 @@ +import type { + ExcalidrawTextElement, + FractionalIndex, + NonDeletedExcalidrawElement, +} from "../../element/types"; +import * as exportUtils from "../../scene/export"; +import { + diamondFixture, + ellipseFixture, + rectangleWithLinkFixture, + textFixture, +} from "../fixtures/elementFixture"; +import { API } from "../helpers/api"; +import { exportToCanvas, exportToSvg } from "@excalidraw/utils"; +import { FONT_FAMILY, FRAME_STYLE } from "../../constants"; +import { prepareElementsForExport } from "../../data"; + +describe("exportToSvg", () => { + const ELEMENT_HEIGHT = 100; + const ELEMENT_WIDTH = 100; + const ELEMENTS = [ + { + ...diamondFixture, + height: ELEMENT_HEIGHT, + width: ELEMENT_WIDTH, + index: "a0", + }, + { + ...ellipseFixture, + height: ELEMENT_HEIGHT, + width: ELEMENT_WIDTH, + index: "a1", + }, + { + ...textFixture, + height: ELEMENT_HEIGHT, + width: ELEMENT_WIDTH, + index: "a2", + }, + { + ...textFixture, + fontFamily: FONT_FAMILY.Nunito, // test embedding external font + height: ELEMENT_HEIGHT, + width: ELEMENT_WIDTH, + index: "a3", + }, + ] as NonDeletedExcalidrawElement[]; + + const DEFAULT_OPTIONS = { + exportBackground: false, + viewBackgroundColor: "#ffffff", + files: {}, + }; + + it("with default arguments", async () => { + const svgElement = await exportUtils.exportToSvg( + ELEMENTS, + DEFAULT_OPTIONS, + null, + ); + + expect(svgElement).toMatchSnapshot(); + }); + + it("with a CJK font", async () => { + const svgElement = await exportUtils.exportToSvg( + [ + ...ELEMENTS, + { + ...textFixture, + height: ELEMENT_HEIGHT, + width: ELEMENT_WIDTH, + text: "中国你好!这是一个测试。中国你好!日本こんにちは!これはテストです。한국 안녕하세요! 이것은 테스트입니다.", + originalText: + "中国你好!这是一个测试。中国你好!日本こんにちは!これはテストです。한국 안녕하세요! 이것은 테스트입니다.", + index: "a4" as FractionalIndex, + } as ExcalidrawTextElement, + ], + DEFAULT_OPTIONS, + null, + ); + + expect(svgElement).toMatchSnapshot(); + // extend the timeout, as it needs to first load the fonts from disk and then perform whole woff2 decode, subset and encode (without workers) + }, 30_000); + + it("with background color", async () => { + const BACKGROUND_COLOR = "#abcdef"; + + const svgElement = await exportUtils.exportToSvg( + ELEMENTS, + { + ...DEFAULT_OPTIONS, + exportBackground: true, + viewBackgroundColor: BACKGROUND_COLOR, + }, + null, + ); + + expect(svgElement.querySelector("rect")).toHaveAttribute( + "fill", + BACKGROUND_COLOR, + ); + }); + + it("with dark mode", async () => { + const svgElement = await exportUtils.exportToSvg( + ELEMENTS, + { + ...DEFAULT_OPTIONS, + exportWithDarkMode: true, + }, + null, + ); + + expect(svgElement.getAttribute("filter")).toMatchInlineSnapshot( + `"invert(93%) hue-rotate(180deg)"`, + ); + }); + + it("with exportPadding", async () => { + const svgElement = await exportUtils.exportToSvg( + ELEMENTS, + { + ...DEFAULT_OPTIONS, + exportPadding: 0, + }, + null, + ); + + expect(svgElement).toHaveAttribute("height", ELEMENT_HEIGHT.toString()); + expect(svgElement).toHaveAttribute("width", ELEMENT_WIDTH.toString()); + expect(svgElement).toHaveAttribute( + "viewBox", + `0 0 ${ELEMENT_WIDTH} ${ELEMENT_HEIGHT}`, + ); + }); + + it("with scale", async () => { + const SCALE = 2; + + const svgElement = await exportUtils.exportToSvg( + ELEMENTS, + { + ...DEFAULT_OPTIONS, + exportPadding: 0, + exportScale: SCALE, + }, + null, + ); + + expect(svgElement).toHaveAttribute( + "height", + (ELEMENT_HEIGHT * SCALE).toString(), + ); + expect(svgElement).toHaveAttribute( + "width", + (ELEMENT_WIDTH * SCALE).toString(), + ); + }); + + it("with exportEmbedScene", async () => { + const svgElement = await exportUtils.exportToSvg( + ELEMENTS, + { + ...DEFAULT_OPTIONS, + exportEmbedScene: true, + }, + null, + ); + expect(svgElement.innerHTML).toMatchSnapshot(); + }); + + it("with elements that have a link", async () => { + const svgElement = await exportUtils.exportToSvg( + [rectangleWithLinkFixture], + DEFAULT_OPTIONS, + null, + ); + expect(svgElement.innerHTML).toMatchSnapshot(); + }); +}); + +describe("exporting frames", () => { + const getFrameNameHeight = (exportType: "canvas" | "svg") => { + const height = + FRAME_STYLE.nameFontSize * FRAME_STYLE.nameLineHeight + + FRAME_STYLE.nameOffsetY; + // canvas truncates dimensions to integers + if (exportType === "canvas") { + return Math.trunc(height); + } + return height; + }; + + // a few tests with exportToCanvas (where we can't inspect elements) + // --------------------------------------------------------------------------- + + describe("exportToCanvas", () => { + it("exporting canvas with a single frame shouldn't crop if not exporting frame directly", async () => { + const elements = [ + API.createElement({ + type: "frame", + width: 100, + height: 100, + x: 0, + y: 0, + }), + API.createElement({ + type: "rectangle", + width: 100, + height: 100, + x: 100, + y: 0, + }), + ]; + + const canvas = await exportToCanvas({ + elements, + files: null, + exportPadding: 0, + }); + + expect(canvas.width).toEqual(200); + expect(canvas.height).toEqual(100 + getFrameNameHeight("canvas")); + }); + + it("exporting canvas with a single frame should crop when exporting frame directly", async () => { + const frame = API.createElement({ + type: "frame", + width: 100, + height: 100, + x: 0, + y: 0, + }); + const elements = [ + frame, + API.createElement({ + type: "rectangle", + width: 100, + height: 100, + x: 100, + y: 0, + }), + ]; + + const canvas = await exportToCanvas({ + elements, + files: null, + exportPadding: 0, + exportingFrame: frame, + }); + + expect(canvas.width).toEqual(frame.width); + expect(canvas.height).toEqual(frame.height); + }); + }); + + // exportToSvg (so we can test for element existence) + // --------------------------------------------------------------------------- + describe("exportToSvg", () => { + it("exporting frame should include overlapping elements, but crop to frame", async () => { + const frame = API.createElement({ + type: "frame", + width: 100, + height: 100, + x: 0, + y: 0, + }); + const frameChild = API.createElement({ + type: "rectangle", + width: 100, + height: 100, + x: 0, + y: 50, + frameId: frame.id, + }); + const rectOverlapping = API.createElement({ + type: "rectangle", + width: 100, + height: 100, + x: 50, + y: 0, + }); + + const svg = await exportToSvg({ + elements: [rectOverlapping, frame, frameChild], + files: null, + exportPadding: 0, + exportingFrame: frame, + }); + + // frame itself isn't exported + expect(svg.querySelector(`[data-id="${frame.id}"]`)).toBeNull(); + // frame child is exported + expect(svg.querySelector(`[data-id="${frameChild.id}"]`)).not.toBeNull(); + // overlapping element is exported + expect( + svg.querySelector(`[data-id="${rectOverlapping.id}"]`), + ).not.toBeNull(); + + expect(svg.getAttribute("width")).toBe(frame.width.toString()); + expect(svg.getAttribute("height")).toBe(frame.height.toString()); + }); + + it("should filter non-overlapping elements when exporting a frame", async () => { + const frame = API.createElement({ + type: "frame", + width: 100, + height: 100, + x: 0, + y: 0, + }); + const frameChild = API.createElement({ + type: "rectangle", + width: 100, + height: 100, + x: 0, + y: 50, + frameId: frame.id, + }); + const elementOutside = API.createElement({ + type: "rectangle", + width: 100, + height: 100, + x: 200, + y: 0, + }); + + const svg = await exportToSvg({ + elements: [frameChild, frame, elementOutside], + files: null, + exportPadding: 0, + exportingFrame: frame, + }); + + // frame itself isn't exported + expect(svg.querySelector(`[data-id="${frame.id}"]`)).toBeNull(); + // frame child is exported + expect(svg.querySelector(`[data-id="${frameChild.id}"]`)).not.toBeNull(); + // non-overlapping element is not exported + expect(svg.querySelector(`[data-id="${elementOutside.id}"]`)).toBeNull(); + + expect(svg.getAttribute("width")).toBe(frame.width.toString()); + expect(svg.getAttribute("height")).toBe(frame.height.toString()); + }); + + it("should export multiple frames when selected, excluding overlapping elements", async () => { + const frame1 = API.createElement({ + type: "frame", + width: 100, + height: 100, + x: 0, + y: 0, + }); + const frame2 = API.createElement({ + type: "frame", + width: 100, + height: 100, + x: 200, + y: 0, + }); + + const frame1Child = API.createElement({ + type: "rectangle", + width: 100, + height: 100, + x: 0, + y: 50, + frameId: frame1.id, + }); + const frame2Child = API.createElement({ + type: "rectangle", + width: 100, + height: 100, + x: 200, + y: 0, + frameId: frame2.id, + }); + const frame2Overlapping = API.createElement({ + type: "rectangle", + width: 100, + height: 100, + x: 350, + y: 0, + }); + + // low-level exportToSvg api expects elements to be pre-filtered, so let's + // use the filter we use in the editor + const { exportedElements, exportingFrame } = prepareElementsForExport( + [frame1Child, frame1, frame2Child, frame2, frame2Overlapping], + { + selectedElementIds: { [frame1.id]: true, [frame2.id]: true }, + }, + true, + ); + + const svg = await exportToSvg({ + elements: exportedElements, + files: null, + exportPadding: 0, + exportingFrame, + }); + + // frames themselves should be exported when multiple frames selected + expect(svg.querySelector(`[data-id="${frame1.id}"]`)).not.toBeNull(); + expect(svg.querySelector(`[data-id="${frame2.id}"]`)).not.toBeNull(); + // children should be epxorted + expect(svg.querySelector(`[data-id="${frame1Child.id}"]`)).not.toBeNull(); + expect(svg.querySelector(`[data-id="${frame2Child.id}"]`)).not.toBeNull(); + // overlapping elements or non-overlapping elements should not be exported + expect( + svg.querySelector(`[data-id="${frame2Overlapping.id}"]`), + ).toBeNull(); + + expect(svg.getAttribute("width")).toBe( + (frame2.x + frame2.width).toString(), + ); + expect(svg.getAttribute("height")).toBe( + (frame2.y + frame2.height + getFrameNameHeight("svg")).toString(), + ); + }); + + it("should render frame alone when not selected", async () => { + const frame = API.createElement({ + type: "frame", + width: 100, + height: 100, + x: 0, + y: 0, + }); + + // low-level exportToSvg api expects elements to be pre-filtered, so let's + // use the filter we use in the editor + const { exportedElements, exportingFrame } = prepareElementsForExport( + [frame], + { + selectedElementIds: {}, + }, + false, + ); + + const svg = await exportToSvg({ + elements: exportedElements, + files: null, + exportPadding: 0, + exportingFrame, + }); + + // frame itself isn't exported + expect(svg.querySelector(`[data-id="${frame.id}"]`)).not.toBeNull(); + + expect(svg.getAttribute("width")).toBe(frame.width.toString()); + expect(svg.getAttribute("height")).toBe( + (frame.height + getFrameNameHeight("svg")).toString(), + ); + }); + + it("should not export frame-overlapping elements belonging to different frame", async () => { + const frame1 = API.createElement({ + type: "frame", + width: 100, + height: 100, + x: 0, + y: 0, + }); + const frame2 = API.createElement({ + type: "frame", + width: 100, + height: 100, + x: 200, + y: 0, + }); + + const frame1Child = API.createElement({ + type: "rectangle", + width: 150, + height: 100, + x: 0, + y: 50, + frameId: frame1.id, + }); + const frame2Child = API.createElement({ + type: "rectangle", + width: 150, + height: 100, + x: 50, + y: 0, + frameId: frame2.id, + }); + + // low-level exportToSvg api expects elements to be pre-filtered, so let's + // use the filter we use in the editor + const { exportedElements, exportingFrame } = prepareElementsForExport( + [frame1Child, frame1, frame2Child, frame2], + { + selectedElementIds: { [frame1.id]: true }, + }, + true, + ); + + const svg = await exportToSvg({ + elements: exportedElements, + files: null, + exportPadding: 0, + exportingFrame, + }); + + // frame shouldn't be exported + expect(svg.querySelector(`[data-id="${frame1.id}"]`)).toBeNull(); + // frame1 child should be epxorted + expect(svg.querySelector(`[data-id="${frame1Child.id}"]`)).not.toBeNull(); + // frame2 child should not be exported even if it physically overlaps with + // frame1 + expect(svg.querySelector(`[data-id="${frame2Child.id}"]`)).toBeNull(); + + expect(svg.getAttribute("width")).toBe(frame1.width.toString()); + expect(svg.getAttribute("height")).toBe(frame1.height.toString()); + }); + }); +}); diff --git a/packages/excalidraw/tests/scroll.test.tsx b/packages/excalidraw/tests/scroll.test.tsx new file mode 100644 index 0000000..1a3f80c --- /dev/null +++ b/packages/excalidraw/tests/scroll.test.tsx @@ -0,0 +1,112 @@ +import React from "react"; +import { + mockBoundingClientRect, + render, + restoreOriginalGetBoundingClientRect, + waitFor, +} from "./test-utils"; +import { Excalidraw } from "../index"; +import { API } from "./helpers/api"; +import { Keyboard } from "./helpers/ui"; +import { KEYS } from "../keys"; + +const { h } = window; + +describe("appState", () => { + it("scroll-to-content on init works with non-zero offsets", async () => { + const WIDTH = 200; + const HEIGHT = 100; + const OFFSET_LEFT = 20; + const OFFSET_TOP = 10; + + const ELEM_WIDTH = 100; + const ELEM_HEIGHT = 60; + + mockBoundingClientRect(); + + await render( + <div> + <Excalidraw + initialData={{ + elements: [ + API.createElement({ + type: "rectangle", + id: "A", + width: ELEM_WIDTH, + height: ELEM_HEIGHT, + }), + ], + scrollToContent: true, + }} + /> + </div>, + ); + await waitFor(() => { + expect(h.state.width).toBe(200); + expect(h.state.height).toBe(100); + expect(h.state.offsetLeft).toBe(OFFSET_LEFT); + expect(h.state.offsetTop).toBe(OFFSET_TOP); + + // assert scroll is in center + expect(h.state.scrollX).toBe(WIDTH / 2 - ELEM_WIDTH / 2); + expect(h.state.scrollY).toBe(HEIGHT / 2 - ELEM_HEIGHT / 2); + }); + restoreOriginalGetBoundingClientRect(); + }); + + it("moving by page up/down/left/right", async () => { + mockBoundingClientRect(); + await render(<Excalidraw handleKeyboardGlobally={true} />, {}); + + const scrollTest = () => { + const initialScrollY = h.state.scrollY; + const initialScrollX = h.state.scrollX; + const pageStepY = h.state.height / h.state.zoom.value; + const pageStepX = h.state.width / h.state.zoom.value; + // Assert the following assertions have meaning + expect(pageStepY).toBeGreaterThan(0); + expect(pageStepX).toBeGreaterThan(0); + // Assert we scroll up + Keyboard.keyPress(KEYS.PAGE_UP); + expect(h.state.scrollY).toBe(initialScrollY + pageStepY); + // x-axis unchanged + expect(h.state.scrollX).toBe(initialScrollX); + + // Assert we scroll down + Keyboard.keyPress(KEYS.PAGE_DOWN); + Keyboard.keyPress(KEYS.PAGE_DOWN); + expect(h.state.scrollY).toBe(initialScrollY - pageStepY); + // x-axis unchanged + expect(h.state.scrollX).toBe(initialScrollX); + + // Assert we scroll left + Keyboard.withModifierKeys({ shift: true }, () => { + Keyboard.keyPress(KEYS.PAGE_UP); + }); + expect(h.state.scrollX).toBe(initialScrollX + pageStepX); + // y-axis unchanged + expect(h.state.scrollY).toBe(initialScrollY - pageStepY); + + // Assert we scroll right + Keyboard.withModifierKeys({ shift: true }, () => { + Keyboard.keyPress(KEYS.PAGE_DOWN); + Keyboard.keyPress(KEYS.PAGE_DOWN); + }); + expect(h.state.scrollX).toBe(initialScrollX - pageStepX); + // y-axis unchanged + expect(h.state.scrollY).toBe(initialScrollY - pageStepY); + }; + + const zoom = h.state.zoom.value; + // Assert we scroll properly when zoomed in + API.setAppState({ zoom: { value: (zoom * 1.1) as typeof zoom } }); + scrollTest(); + // Assert we scroll properly when zoomed out + API.setAppState({ zoom: { value: (zoom * 0.9) as typeof zoom } }); + scrollTest(); + // Assert we scroll properly with normal zoom + API.setAppState({ zoom: { value: zoom } }); + scrollTest(); + restoreOriginalGetBoundingClientRect(); + }); +}); diff --git a/packages/excalidraw/tests/search.test.tsx b/packages/excalidraw/tests/search.test.tsx new file mode 100644 index 0000000..68ad658 --- /dev/null +++ b/packages/excalidraw/tests/search.test.tsx @@ -0,0 +1,148 @@ +import React from "react"; +import { act, render, waitFor } from "./test-utils"; +import { Excalidraw } from "../index"; +import { CANVAS_SEARCH_TAB, CLASSES, DEFAULT_SIDEBAR } from "../constants"; +import { Keyboard } from "./helpers/ui"; +import { KEYS } from "../keys"; +import { updateTextEditor } from "./queries/dom"; +import { API } from "./helpers/api"; +import type { ExcalidrawTextElement } from "../element/types"; + +const { h } = window; + +const querySearchInput = async () => { + const input = + h.app.excalidrawContainerValue.container?.querySelector<HTMLInputElement>( + `.${CLASSES.SEARCH_MENU_INPUT_WRAPPER} input`, + )!; + await waitFor(() => expect(input).not.toBeNull()); + return input; +}; + +describe("search", () => { + beforeEach(async () => { + await render(<Excalidraw handleKeyboardGlobally />); + API.setAppState({ + openSidebar: null, + }); + }); + + it("should toggle search on cmd+f", async () => { + expect(h.app.state.openSidebar).toBeNull(); + + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.F); + }); + expect(h.app.state.openSidebar).not.toBeNull(); + expect(h.app.state.openSidebar?.name).toBe(DEFAULT_SIDEBAR.name); + expect(h.app.state.openSidebar?.tab).toBe(CANVAS_SEARCH_TAB); + + const searchInput = await querySearchInput(); + expect(searchInput.matches(":focus")).toBe(true); + }); + + it("should refocus search input with cmd+f when search sidebar is still open", async () => { + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.F); + }); + + const searchInput = + h.app.excalidrawContainerValue.container?.querySelector<HTMLInputElement>( + `.${CLASSES.SEARCH_MENU_INPUT_WRAPPER} input`, + ); + + act(() => { + searchInput?.blur(); + }); + + expect(h.app.state.openSidebar).not.toBeNull(); + expect(searchInput?.matches(":focus")).toBe(false); + + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.F); + }); + expect(searchInput?.matches(":focus")).toBe(true); + }); + + it("should match text and cycle through matches on Enter", async () => { + const scrollIntoViewMock = jest.fn(); + window.HTMLElement.prototype.scrollIntoView = scrollIntoViewMock; + + API.setElements([ + API.createElement({ type: "text", text: "test one" }), + API.createElement({ type: "text", text: "test two" }), + ]); + + expect(h.app.state.openSidebar).toBeNull(); + + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.F); + }); + expect(h.app.state.openSidebar).not.toBeNull(); + expect(h.app.state.openSidebar?.name).toBe(DEFAULT_SIDEBAR.name); + expect(h.app.state.openSidebar?.tab).toBe(CANVAS_SEARCH_TAB); + + const searchInput = await querySearchInput(); + + expect(searchInput.matches(":focus")).toBe(true); + + updateTextEditor(searchInput, "test"); + + await waitFor(() => { + expect(h.app.state.searchMatches.length).toBe(2); + expect(h.app.state.searchMatches[0].focus).toBe(true); + }); + + Keyboard.keyPress(KEYS.ENTER, searchInput); + expect(h.app.state.searchMatches[0].focus).toBe(false); + expect(h.app.state.searchMatches[1].focus).toBe(true); + + Keyboard.keyPress(KEYS.ENTER, searchInput); + expect(h.app.state.searchMatches[0].focus).toBe(true); + expect(h.app.state.searchMatches[1].focus).toBe(false); + }); + + it("should match text split across multiple lines", async () => { + const scrollIntoViewMock = jest.fn(); + window.HTMLElement.prototype.scrollIntoView = scrollIntoViewMock; + + API.setElements([ + API.createElement({ + type: "text", + text: "", + }), + ]); + + API.updateElement(h.elements[0] as ExcalidrawTextElement, { + text: "t\ne\ns\nt \nt\ne\nx\nt \ns\np\nli\nt \ni\nn\nt\no\nm\nu\nlt\ni\np\nl\ne \nli\nn\ne\ns", + originalText: "test text split into multiple lines", + }); + + expect(h.app.state.openSidebar).toBeNull(); + + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.F); + }); + expect(h.app.state.openSidebar).not.toBeNull(); + expect(h.app.state.openSidebar?.name).toBe(DEFAULT_SIDEBAR.name); + expect(h.app.state.openSidebar?.tab).toBe(CANVAS_SEARCH_TAB); + + const searchInput = await querySearchInput(); + + expect(searchInput.matches(":focus")).toBe(true); + + updateTextEditor(searchInput, "test"); + + await waitFor(() => { + expect(h.app.state.searchMatches.length).toBe(1); + expect(h.app.state.searchMatches[0]?.matchedLines?.length).toBe(4); + }); + + updateTextEditor(searchInput, "ext spli"); + + await waitFor(() => { + expect(h.app.state.searchMatches.length).toBe(1); + expect(h.app.state.searchMatches[0]?.matchedLines?.length).toBe(6); + }); + }); +}); diff --git a/packages/excalidraw/tests/selection.test.tsx b/packages/excalidraw/tests/selection.test.tsx new file mode 100644 index 0000000..0b1f6e5 --- /dev/null +++ b/packages/excalidraw/tests/selection.test.tsx @@ -0,0 +1,536 @@ +import React from "react"; +import { + render, + fireEvent, + mockBoundingClientRect, + restoreOriginalGetBoundingClientRect, + assertSelectedElements, + unmountComponent, +} from "./test-utils"; +import { Excalidraw } from "../index"; +import * as StaticScene from "../renderer/staticScene"; +import * as InteractiveCanvas from "../renderer/interactiveScene"; +import { KEYS } from "../keys"; +import { reseed } from "../random"; +import { API } from "./helpers/api"; +import { Keyboard, Pointer, UI } from "./helpers/ui"; +import { SHAPES } from "../shapes"; +import { vi } from "vitest"; + +unmountComponent(); + +const renderInteractiveScene = vi.spyOn( + InteractiveCanvas, + "renderInteractiveScene", +); +const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene"); + +beforeEach(() => { + localStorage.clear(); + renderInteractiveScene.mockClear(); + renderStaticScene.mockClear(); + reseed(7); +}); + +const { h } = window; + +const mouse = new Pointer("mouse"); + +describe("box-selection", () => { + beforeEach(async () => { + await render(<Excalidraw />); + }); + + it("should allow adding to selection via box-select when holding shift", async () => { + const rect1 = API.createElement({ + type: "rectangle", + x: 0, + y: 0, + width: 50, + height: 50, + backgroundColor: "red", + fillStyle: "solid", + }); + const rect2 = API.createElement({ + type: "rectangle", + x: 100, + y: 0, + width: 50, + height: 50, + }); + + API.setElements([rect1, rect2]); + + mouse.downAt(175, -20); + mouse.moveTo(85, 70); + mouse.up(); + + assertSelectedElements([rect2.id]); + + Keyboard.withModifierKeys({ shift: true }, () => { + mouse.downAt(75, -20); + mouse.moveTo(-15, 70); + mouse.up(); + }); + + assertSelectedElements([rect2.id, rect1.id]); + }); + + it("should (de)select element when box-selecting over and out while not holding shift", async () => { + const rect1 = API.createElement({ + type: "rectangle", + x: 0, + y: 0, + width: 50, + height: 50, + backgroundColor: "red", + fillStyle: "solid", + }); + + API.setElements([rect1]); + + mouse.downAt(75, -20); + mouse.moveTo(-15, 70); + + assertSelectedElements([rect1.id]); + + mouse.moveTo(100, -100); + + assertSelectedElements([]); + + mouse.up(); + + assertSelectedElements([]); + }); +}); + +describe("inner box-selection", () => { + beforeEach(async () => { + await render(<Excalidraw />); + }); + it("selecting elements visually nested inside another", async () => { + const rect1 = API.createElement({ + type: "rectangle", + x: 0, + y: 0, + width: 300, + height: 300, + backgroundColor: "red", + fillStyle: "solid", + }); + const rect2 = API.createElement({ + type: "rectangle", + x: 50, + y: 50, + width: 50, + height: 50, + }); + const rect3 = API.createElement({ + type: "rectangle", + x: 150, + y: 150, + width: 50, + height: 50, + }); + API.setElements([rect1, rect2, rect3]); + Keyboard.withModifierKeys({ ctrl: true }, () => { + mouse.downAt(40, 40); + mouse.moveTo(290, 290); + mouse.up(); + + assertSelectedElements([rect2.id, rect3.id]); + }); + }); + + it("selecting grouped elements visually nested inside another", async () => { + const rect1 = API.createElement({ + type: "rectangle", + x: 0, + y: 0, + width: 300, + height: 300, + backgroundColor: "red", + fillStyle: "solid", + }); + const rect2 = API.createElement({ + type: "rectangle", + x: 50, + y: 50, + width: 50, + height: 50, + groupIds: ["A"], + }); + const rect3 = API.createElement({ + type: "rectangle", + x: 150, + y: 150, + width: 50, + height: 50, + groupIds: ["A"], + }); + API.setElements([rect1, rect2, rect3]); + + Keyboard.withModifierKeys({ ctrl: true }, () => { + mouse.downAt(40, 40); + mouse.moveTo(rect2.x + rect2.width + 10, rect2.y + rect2.height + 10); + mouse.up(); + + assertSelectedElements([rect2.id, rect3.id]); + expect(h.state.selectedGroupIds).toEqual({ A: true }); + }); + }); + + it("selecting & deselecting grouped elements visually nested inside another", async () => { + const rect1 = API.createElement({ + type: "rectangle", + x: 0, + y: 0, + width: 300, + height: 300, + backgroundColor: "red", + fillStyle: "solid", + }); + const rect2 = API.createElement({ + type: "rectangle", + x: 50, + y: 50, + width: 50, + height: 50, + groupIds: ["A"], + }); + const rect3 = API.createElement({ + type: "rectangle", + x: 150, + y: 150, + width: 50, + height: 50, + groupIds: ["A"], + }); + API.setElements([rect1, rect2, rect3]); + Keyboard.withModifierKeys({ ctrl: true }, () => { + mouse.downAt(rect2.x - 20, rect2.y - 20); + mouse.moveTo(rect2.x + rect2.width + 10, rect2.y + rect2.height + 10); + assertSelectedElements([rect2.id, rect3.id]); + expect(h.state.selectedGroupIds).toEqual({ A: true }); + mouse.moveTo(rect2.x - 10, rect2.y - 10); + assertSelectedElements([rect1.id]); + expect(h.state.selectedGroupIds).toEqual({}); + mouse.up(); + }); + }); +}); + +describe("selection element", () => { + it("create selection element on pointer down", async () => { + const { getByToolName, container } = await render(<Excalidraw />); + // select tool + const tool = getByToolName("selection"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas.interactive")!; + fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 }); + + expect(renderInteractiveScene).toHaveBeenCalledTimes(3); + expect(renderStaticScene).toHaveBeenCalledTimes(3); + const selectionElement = h.state.selectionElement!; + expect(selectionElement).not.toBeNull(); + expect(selectionElement.type).toEqual("selection"); + expect([selectionElement.x, selectionElement.y]).toEqual([60, 100]); + expect([selectionElement.width, selectionElement.height]).toEqual([0, 0]); + + // TODO: There is a memory leak if pointer up is not triggered + fireEvent.pointerUp(canvas); + }); + + it("resize selection element on pointer move", async () => { + const { getByToolName, container } = await render(<Excalidraw />); + // select tool + const tool = getByToolName("selection"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas.interactive")!; + fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 }); + fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 }); + + expect(renderInteractiveScene).toHaveBeenCalledTimes(4); + expect(renderStaticScene).toHaveBeenCalledTimes(3); + const selectionElement = h.state.selectionElement!; + expect(selectionElement).not.toBeNull(); + expect(selectionElement.type).toEqual("selection"); + expect([selectionElement.x, selectionElement.y]).toEqual([60, 30]); + expect([selectionElement.width, selectionElement.height]).toEqual([90, 70]); + + // TODO: There is a memory leak if pointer up is not triggered + fireEvent.pointerUp(canvas); + }); + + it("remove selection element on pointer up", async () => { + const { getByToolName, container } = await render(<Excalidraw />); + // select tool + const tool = getByToolName("selection"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas.interactive")!; + fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 }); + fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 }); + fireEvent.pointerUp(canvas); + + expect(renderInteractiveScene).toHaveBeenCalledTimes(5); + expect(renderStaticScene).toHaveBeenCalledTimes(3); + expect(h.state.selectionElement).toBeNull(); + }); +}); + +describe("select single element on the scene", () => { + beforeAll(() => { + mockBoundingClientRect(); + }); + + afterAll(() => { + restoreOriginalGetBoundingClientRect(); + }); + + it("rectangle", async () => { + const { getByToolName, container } = await render( + <Excalidraw handleKeyboardGlobally={true} />, + ); + const canvas = container.querySelector("canvas.interactive")!; + { + // create element + const tool = getByToolName("rectangle"); + fireEvent.click(tool); + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); + fireEvent.pointerUp(canvas); + fireEvent.keyDown(document, { + key: KEYS.ESCAPE, + }); + } + + const tool = getByToolName("selection"); + fireEvent.click(tool); + // click on a line on the rectangle + fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 }); + fireEvent.pointerUp(canvas); + + expect(renderInteractiveScene).toHaveBeenCalledTimes(8); + expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(h.state.selectionElement).toBeNull(); + expect(h.elements.length).toEqual(1); + expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); + + h.elements.forEach((element) => expect(element).toMatchSnapshot()); + }); + + it("diamond", async () => { + const { getByToolName, container } = await render( + <Excalidraw handleKeyboardGlobally={true} />, + ); + const canvas = container.querySelector("canvas.interactive")!; + { + // create element + const tool = getByToolName("diamond"); + fireEvent.click(tool); + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); + fireEvent.pointerUp(canvas); + fireEvent.keyDown(document, { + key: KEYS.ESCAPE, + }); + } + + const tool = getByToolName("selection"); + fireEvent.click(tool); + // click on a line on the rectangle + fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 }); + fireEvent.pointerUp(canvas); + + expect(renderInteractiveScene).toHaveBeenCalledTimes(8); + expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(h.state.selectionElement).toBeNull(); + expect(h.elements.length).toEqual(1); + expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); + + h.elements.forEach((element) => expect(element).toMatchSnapshot()); + }); + + it("ellipse", async () => { + const { getByToolName, container } = await render( + <Excalidraw handleKeyboardGlobally={true} />, + ); + const canvas = container.querySelector("canvas.interactive")!; + { + // create element + const tool = getByToolName("ellipse"); + fireEvent.click(tool); + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); + fireEvent.pointerUp(canvas); + fireEvent.keyDown(document, { + key: KEYS.ESCAPE, + }); + } + + const tool = getByToolName("selection"); + fireEvent.click(tool); + // click on a line on the rectangle + fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 }); + fireEvent.pointerUp(canvas); + + expect(renderInteractiveScene).toHaveBeenCalledTimes(8); + expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(h.state.selectionElement).toBeNull(); + expect(h.elements.length).toEqual(1); + expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); + + h.elements.forEach((element) => expect(element).toMatchSnapshot()); + }); + + it("arrow", async () => { + const { getByToolName, container } = await render( + <Excalidraw handleKeyboardGlobally={true} />, + ); + const canvas = container.querySelector("canvas.interactive")!; + { + // create element + const tool = getByToolName("arrow"); + fireEvent.click(tool); + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); + fireEvent.pointerUp(canvas); + fireEvent.keyDown(document, { + key: KEYS.ESCAPE, + }); + } + + /* + 1 2 3 4 5 6 7 8 9 + 1 + 2 x + 3 + 4 . + 5 + 6 + 7 x + 8 + 9 + */ + + const tool = getByToolName("selection"); + fireEvent.click(tool); + // click on a line on the arrow + fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 }); + fireEvent.pointerUp(canvas); + + expect(renderInteractiveScene).toHaveBeenCalledTimes(8); + expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(h.state.selectionElement).toBeNull(); + expect(h.elements.length).toEqual(1); + expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); + h.elements.forEach((element) => expect(element).toMatchSnapshot()); + }); + + it("arrow escape", async () => { + const { getByToolName, container } = await render( + <Excalidraw handleKeyboardGlobally={true} />, + ); + const canvas = container.querySelector("canvas.interactive")!; + { + // create element + const tool = getByToolName("line"); + fireEvent.click(tool); + fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); + fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); + fireEvent.pointerUp(canvas); + fireEvent.keyDown(document, { + key: KEYS.ESCAPE, + }); + } + + /* + 1 2 3 4 5 6 7 8 9 + 1 + 2 x + 3 + 4 . + 5 + 6 + 7 x + 8 + 9 + */ + + const tool = getByToolName("selection"); + fireEvent.click(tool); + // click on a line on the arrow + fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 }); + fireEvent.pointerUp(canvas); + + expect(renderInteractiveScene).toHaveBeenCalledTimes(8); + expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(h.state.selectionElement).toBeNull(); + expect(h.elements.length).toEqual(1); + expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); + + h.elements.forEach((element) => expect(element).toMatchSnapshot()); + }); +}); + +describe("tool locking & selection", () => { + it("should not select newly created element while tool is locked", async () => { + await render(<Excalidraw />); + + UI.clickTool("lock"); + expect(h.state.activeTool.locked).toBe(true); + + for (const { value } of Object.values(SHAPES)) { + if (value !== "image" && value !== "selection" && value !== "eraser") { + const element = UI.createElement(value); + expect(h.state.selectedElementIds[element.id]).not.toBe(true); + } + } + }); +}); + +describe("selectedElementIds stability", () => { + beforeEach(async () => { + await render(<Excalidraw />); + }); + + it("box-selection should be stable when not changing selection", () => { + const rectangle = API.createElement({ + type: "rectangle", + x: 0, + y: 0, + width: 10, + height: 10, + }); + + API.setElements([rectangle]); + + const selectedElementIds_1 = h.state.selectedElementIds; + + mouse.downAt(-100, -100); + mouse.moveTo(-50, -50); + mouse.up(); + + expect(h.state.selectedElementIds).toBe(selectedElementIds_1); + + mouse.downAt(-50, -50); + mouse.moveTo(50, 50); + + const selectedElementIds_2 = h.state.selectedElementIds; + + expect(selectedElementIds_2).toEqual({ [rectangle.id]: true }); + + mouse.moveTo(60, 60); + + // box-selecting further without changing selection should keep + // selectedElementIds stable (the same object) + expect(h.state.selectedElementIds).toBe(selectedElementIds_2); + + mouse.up(); + + expect(h.state.selectedElementIds).toBe(selectedElementIds_2); + }); +}); diff --git a/packages/excalidraw/tests/shortcuts.test.tsx b/packages/excalidraw/tests/shortcuts.test.tsx new file mode 100644 index 0000000..e02ed01 --- /dev/null +++ b/packages/excalidraw/tests/shortcuts.test.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { KEYS } from "../keys"; +import { Excalidraw } from "../index"; +import { API } from "./helpers/api"; +import { Keyboard } from "./helpers/ui"; +import { fireEvent, render, waitFor } from "./test-utils"; + +describe("shortcuts", () => { + it("Clear canvas shortcut should display confirm dialog", async () => { + await render( + <Excalidraw + initialData={{ elements: [API.createElement({ type: "rectangle" })] }} + handleKeyboardGlobally + />, + ); + + expect(window.h.elements.length).toBe(1); + + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyDown(KEYS.DELETE); + }); + const confirmDialog = document.querySelector(".confirm-dialog")!; + expect(confirmDialog).not.toBe(null); + + fireEvent.click(confirmDialog.querySelector('[aria-label="Confirm"]')!); + + await waitFor(() => { + expect(window.h.elements[0].isDeleted).toBe(true); + }); + }); +}); diff --git a/packages/excalidraw/tests/test-utils.ts b/packages/excalidraw/tests/test-utils.ts new file mode 100644 index 0000000..84936f5 --- /dev/null +++ b/packages/excalidraw/tests/test-utils.ts @@ -0,0 +1,425 @@ +import "pepjs"; + +import type { RenderResult, RenderOptions } from "@testing-library/react"; +import { act } from "@testing-library/react"; +import { + render, + queries, + waitFor, + fireEvent, + cleanup, +} from "@testing-library/react"; + +import * as toolQueries from "./queries/toolQueries"; +import type { ImportedDataState } from "../data/types"; +import { STORAGE_KEYS } from "../../../excalidraw-app/app_constants"; +import { getSelectedElements } from "../scene/selection"; +import type { ExcalidrawElement } from "../element/types"; +import { UI } from "./helpers/ui"; +import ansi from "ansicolor"; +import { ORIG_ID } from "../constants"; +import { arrayToMap } from "../utils"; +import type { AllPossibleKeys } from "../utility-types"; + +export { cleanup as unmountComponent }; + +const customQueries = { + ...queries, + ...toolQueries, +}; + +type TestRenderFn = ( + ui: React.ReactElement, + options?: Omit< + RenderOptions & { localStorageData?: ImportedDataState }, + "queries" + >, +) => Promise<RenderResult<typeof customQueries>>; + +const renderApp: TestRenderFn = async (ui, options) => { + if (options?.localStorageData) { + initLocalStorage(options.localStorageData); + delete options.localStorageData; + } + + const renderResult = render(ui, { + queries: customQueries, + ...options, + }); + + GlobalTestState.renderResult = renderResult; + + Object.defineProperty(GlobalTestState, "canvas", { + // must be a getter because at the time of ExcalidrawApp render the + // child App component isn't likely mounted yet (and thus canvas not + // present in DOM) + get() { + return renderResult.container.querySelector("canvas.static")!; + }, + }); + + Object.defineProperty(GlobalTestState, "interactiveCanvas", { + // must be a getter because at the time of ExcalidrawApp render the + // child App component isn't likely mounted yet (and thus canvas not + // present in DOM) + get() { + return renderResult.container.querySelector("canvas.interactive")!; + }, + }); + + await waitFor(() => { + const canvas = renderResult.container.querySelector("canvas.static"); + if (!canvas) { + throw new Error("not initialized yet"); + } + + const interactiveCanvas = + renderResult.container.querySelector("canvas.interactive"); + if (!interactiveCanvas) { + throw new Error("not initialized yet"); + } + + // hack-awaiting app.initialScene() which solves some test race conditions + // (later we may switch this with proper event listener) + if (window.h.state.isLoading) { + throw new Error("still loading"); + } + }); + + return renderResult; +}; + +// re-export everything +export * from "@testing-library/react"; + +// override render method +export { renderApp as render }; + +/** + * For state-sharing across test helpers. + * NOTE: there shouldn't be concurrency issues as each test is running in its + * own process and thus gets its own instance of this module when running + * tests in parallel. + */ +export class GlobalTestState { + /** + * automatically updated on each call to render() + */ + static renderResult: RenderResult<typeof customQueries> = null!; + /** + * retrieves static canvas for currently rendered app instance + */ + static get canvas(): HTMLCanvasElement { + return null!; + } + /** + * retrieves interactive canvas for currently rendered app instance + */ + static get interactiveCanvas(): HTMLCanvasElement { + return null!; + } +} + +const initLocalStorage = (data: ImportedDataState) => { + if (data.elements) { + localStorage.setItem( + STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS, + JSON.stringify(data.elements), + ); + } + if (data.appState) { + localStorage.setItem( + STORAGE_KEYS.LOCAL_STORAGE_APP_STATE, + JSON.stringify(data.appState), + ); + } +}; + +const originalGetBoundingClientRect = + global.window.HTMLDivElement.prototype.getBoundingClientRect; + +export const mockBoundingClientRect = ( + { + top = 0, + left = 0, + bottom = 0, + right = 0, + width = 1920, + height = 1080, + x = 0, + y = 0, + toJSON = () => {}, + } = { + top: 10, + left: 20, + bottom: 10, + right: 10, + width: 200, + x: 10, + y: 20, + height: 100, + }, +) => { + // override getBoundingClientRect as by default it will always return all values as 0 even if customized in html + global.window.HTMLDivElement.prototype.getBoundingClientRect = () => ({ + top, + left, + bottom, + right, + width, + height, + x, + y, + toJSON, + }); +}; + +export const withExcalidrawDimensions = async ( + dimensions: { width: number; height: number }, + cb: () => void, +) => { + mockBoundingClientRect(dimensions); + act(() => { + // @ts-ignore + h.app.refreshViewportBreakpoints(); + // @ts-ignore + h.app.refreshEditorBreakpoints(); + window.h.app.refresh(); + }); + + await cb(); + + restoreOriginalGetBoundingClientRect(); + act(() => { + // @ts-ignore + h.app.refreshViewportBreakpoints(); + // @ts-ignore + h.app.refreshEditorBreakpoints(); + window.h.app.refresh(); + }); +}; + +export const restoreOriginalGetBoundingClientRect = () => { + global.window.HTMLDivElement.prototype.getBoundingClientRect = + originalGetBoundingClientRect; +}; + +export const assertSelectedElements = ( + ...elements: ( + | (ExcalidrawElement["id"] | ExcalidrawElement)[] + | ExcalidrawElement["id"] + | ExcalidrawElement + )[] +) => { + const { h } = window; + const selectedElementIds = getSelectedElements( + h.app.getSceneElements(), + h.state, + ).map((el) => el.id); + const ids = elements + .flat() + .map((item) => (typeof item === "string" ? item : item.id)); + expect(selectedElementIds.length).toBe(ids.length); + expect(selectedElementIds).toEqual(expect.arrayContaining(ids)); +}; + +export const toggleMenu = (container: HTMLElement) => { + // open menu + fireEvent.click(container.querySelector(".dropdown-menu-button")!); +}; + +export const togglePopover = (label: string) => { + // Needed for radix-ui/react-popover as tests fail due to resize observer not being present + (global as any).ResizeObserver = class ResizeObserver { + constructor(cb: any) { + (this as any).cb = cb; + } + + observe() {} + + unobserve() {} + disconnect() {} + }; + + UI.clickLabeledElement(label); +}; + +expect.extend({ + toBeNonNaNNumber(received) { + const pass = typeof received === "number" && !isNaN(received); + if (pass) { + return { + message: () => `expected ${received} not to be a non-NaN number`, + pass: true, + }; + } + return { + message: () => `expected ${received} to be a non-NaN number`, + pass: false, + }; + }, +}); + +/** + * Serializer for IEE754 float pointing numbers to avoid random failures due to tiny precision differences + */ +expect.addSnapshotSerializer({ + serialize(val, config, indentation, depth, refs, printer) { + return printer(val.toFixed(5), config, indentation, depth, refs); + }, + test(val) { + return ( + typeof val === "number" && + Number.isFinite(val) && + !Number.isNaN(val) && + !Number.isInteger(val) + ); + }, +}); + +export const getCloneByOrigId = <T extends boolean = false>( + origId: ExcalidrawElement["id"], + returnNullIfNotExists: T = false as T, +): T extends true ? ExcalidrawElement | null : ExcalidrawElement => { + const clonedElement = window.h.elements?.find( + (el) => (el as any)[ORIG_ID] === origId, + ); + if (clonedElement) { + return clonedElement; + } + if (returnNullIfNotExists !== true) { + throw new Error(`cloned element not found for origId: ${origId}`); + } + return null as T extends true ? ExcalidrawElement | null : ExcalidrawElement; +}; + +/** + * Assertion helper that strips the actual elements of extra attributes + * so that diffs are easier to read in case of failure. + * + * Asserts element order as well, and selected element ids + * (when `selected: true` set for given element). + * + * If testing cloned elements, you can use { `[ORIG_ID]: origElement.id } + * If you need to refer to cloned element properties, you can use + * `getCloneByOrigId()`, e.g.: `{ frameId: getCloneByOrigId(origFrame.id)?.id }` + */ +export const assertElements = <T extends AllPossibleKeys<ExcalidrawElement>>( + actualElements: readonly ExcalidrawElement[], + /** array order matters */ + expectedElements: (Partial<Record<T, any>> & { + /** meta, will be stripped for element attribute checks */ + selected?: true; + } & ( + | { + id: ExcalidrawElement["id"]; + } + | { [ORIG_ID]?: string } + ))[], +) => { + const h = window.h; + + const expectedElementsWithIds: (typeof expectedElements[number] & { + id: ExcalidrawElement["id"]; + })[] = expectedElements.map((el) => { + if ("id" in el) { + return el; + } + const actualElement = actualElements.find( + (act) => (act as any)[ORIG_ID] === el[ORIG_ID], + ); + if (actualElement) { + return { ...el, id: actualElement.id }; + } + return { + ...el, + id: "UNKNOWN_ID", + }; + }); + + const map_expectedElements = arrayToMap(expectedElementsWithIds); + + const selectedElementIds = expectedElementsWithIds.reduce( + (acc: Record<ExcalidrawElement["id"], true>, el) => { + if (el.selected) { + acc[el.id] = true; + } + return acc; + }, + {}, + ); + + const mappedActualElements = actualElements.map((el) => { + const expectedElement = map_expectedElements.get(el.id); + if (expectedElement) { + const pickedAttrs: Record<string, any> = {}; + + for (const key of Object.keys(expectedElement)) { + if (key === "selected") { + delete expectedElement.selected; + continue; + } + pickedAttrs[key] = (el as any)[key]; + } + + if (ORIG_ID in expectedElement) { + // @ts-ignore + pickedAttrs[ORIG_ID] = (el as any)[ORIG_ID]; + } + + return pickedAttrs; + } + return el; + }); + + try { + // testing order separately for even easier diffs + expect(actualElements.map((x) => x.id)).toEqual( + expectedElementsWithIds.map((x) => x.id), + ); + } catch (err: any) { + let errStr = "\n\nmismatched element order\n\n"; + + errStr += `actual: ${ansi.lightGray( + `[${err.actual + .map((id: string, index: number) => { + const act = actualElements[index]; + + return `${ + id === err.expected[index] ? ansi.green(id) : ansi.red(id) + } (${act.type.slice(0, 4)}${ + ORIG_ID in act ? ` ↳ ${(act as any)[ORIG_ID]}` : "" + })`; + }) + .join(", ")}]`, + )}\n${ansi.lightGray( + `expected: [${err.expected + .map((exp: string, index: number) => { + const expEl = actualElements.find((el) => el.id === exp); + const origEl = + expEl && + actualElements.find((el) => el.id === (expEl as any)[ORIG_ID]); + return expEl + ? `${ + exp === err.actual[index] + ? ansi.green(expEl.id) + : ansi.red(expEl.id) + } (${expEl.type.slice(0, 4)}${origEl ? ` ↳ ${origEl.id}` : ""})` + : exp; + }) + .join(", ")}]\n`, + )}`; + + const error = new Error(errStr); + const stack = err.stack.split("\n"); + stack.splice(1, 1); + error.stack = stack.join("\n"); + throw error; + } + + expect(mappedActualElements).toEqual( + expect.arrayContaining(expectedElementsWithIds), + ); + + expect(h.state.selectedElementIds).toEqual(selectedElementIds); +}; diff --git a/packages/excalidraw/tests/tool.test.tsx b/packages/excalidraw/tests/tool.test.tsx new file mode 100644 index 0000000..662c61b --- /dev/null +++ b/packages/excalidraw/tests/tool.test.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { Excalidraw } from "../index"; +import type { ExcalidrawImperativeAPI } from "../types"; +import { resolvablePromise } from "../utils"; +import { act, render } from "./test-utils"; +import { Pointer } from "./helpers/ui"; + +describe("setActiveTool()", () => { + const h = window.h; + + let excalidrawAPI: ExcalidrawImperativeAPI; + + const mouse = new Pointer("mouse"); + + beforeEach(async () => { + const excalidrawAPIPromise = resolvablePromise<ExcalidrawImperativeAPI>(); + await render( + <Excalidraw + excalidrawAPI={(api) => excalidrawAPIPromise.resolve(api as any)} + />, + ); + excalidrawAPI = await excalidrawAPIPromise; + }); + + it("should expose setActiveTool on package API", () => { + expect(excalidrawAPI.setActiveTool).toBeDefined(); + expect(excalidrawAPI.setActiveTool).toBe(h.app.setActiveTool); + }); + + it("should set the active tool type", async () => { + expect(h.state.activeTool.type).toBe("selection"); + act(() => { + excalidrawAPI.setActiveTool({ type: "rectangle" }); + }); + expect(h.state.activeTool.type).toBe("rectangle"); + + mouse.down(10, 10); + mouse.up(20, 20); + + expect(h.state.activeTool.type).toBe("selection"); + }); + + it("should support tool locking", async () => { + expect(h.state.activeTool.type).toBe("selection"); + act(() => { + excalidrawAPI.setActiveTool({ type: "rectangle", locked: true }); + }); + expect(h.state.activeTool.type).toBe("rectangle"); + + mouse.down(10, 10); + mouse.up(20, 20); + + expect(h.state.activeTool.type).toBe("rectangle"); + }); + + it("should set custom tool", async () => { + expect(h.state.activeTool.type).toBe("selection"); + act(() => { + excalidrawAPI.setActiveTool({ type: "custom", customType: "comment" }); + }); + expect(h.state.activeTool.type).toBe("custom"); + expect(h.state.activeTool.customType).toBe("comment"); + }); +}); diff --git a/packages/excalidraw/tests/utils.test.ts b/packages/excalidraw/tests/utils.test.ts new file mode 100644 index 0000000..34944fa --- /dev/null +++ b/packages/excalidraw/tests/utils.test.ts @@ -0,0 +1,13 @@ +import { isTransparent } from "../utils"; + +describe("Test isTransparent", () => { + it("should return true when color is rgb transparent", () => { + expect(isTransparent("#ff00")).toEqual(true); + expect(isTransparent("#fff00000")).toEqual(true); + expect(isTransparent("transparent")).toEqual(true); + }); + + it("should return false when color is not transparent", () => { + expect(isTransparent("#ced4da")).toEqual(false); + }); +}); diff --git a/packages/excalidraw/tests/viewMode.test.tsx b/packages/excalidraw/tests/viewMode.test.tsx new file mode 100644 index 0000000..66eca8c --- /dev/null +++ b/packages/excalidraw/tests/viewMode.test.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { render, GlobalTestState } from "./test-utils"; +import { Excalidraw } from "../index"; +import { KEYS } from "../keys"; +import { Keyboard, Pointer, UI } from "./helpers/ui"; +import { CURSOR_TYPE } from "../constants"; +import { API } from "./helpers/api"; + +const mouse = new Pointer("mouse"); +const touch = new Pointer("touch"); +const pen = new Pointer("pen"); +const pointerTypes = [mouse, touch, pen]; + +describe("view mode", () => { + beforeEach(async () => { + await render(<Excalidraw />); + }); + + it("after switching to view mode – cursor type should be pointer", async () => { + API.setAppState({ viewModeEnabled: true }); + expect(GlobalTestState.interactiveCanvas.style.cursor).toBe( + CURSOR_TYPE.GRAB, + ); + }); + + it("after switching to view mode, moving, clicking, and pressing space key – cursor type should be pointer", async () => { + API.setAppState({ viewModeEnabled: true }); + + pointerTypes.forEach((pointerType) => { + const pointer = pointerType; + pointer.reset(); + pointer.move(100, 100); + pointer.click(); + Keyboard.keyPress(KEYS.SPACE); + expect(GlobalTestState.interactiveCanvas.style.cursor).toBe( + CURSOR_TYPE.GRAB, + ); + }); + }); + + it("cursor should stay as grabbing type when hovering over canvas elements", async () => { + // create a rectangle, then hover over it – cursor should be + // move type for mouse and grab for touch & pen + // then switch to view-mode and cursor should be grabbing type + UI.createElement("rectangle", { size: 100 }); + + pointerTypes.forEach((pointerType) => { + const pointer = pointerType; + + pointer.moveTo(50, 50); + // eslint-disable-next-line dot-notation + if (pointerType["pointerType"] === "mouse") { + expect(GlobalTestState.interactiveCanvas.style.cursor).toBe( + CURSOR_TYPE.MOVE, + ); + } else { + expect(GlobalTestState.interactiveCanvas.style.cursor).toBe( + CURSOR_TYPE.GRAB, + ); + } + + API.setAppState({ viewModeEnabled: true }); + expect(GlobalTestState.interactiveCanvas.style.cursor).toBe( + CURSOR_TYPE.GRAB, + ); + }); + }); +}); diff --git a/packages/excalidraw/tests/zindex.test.tsx b/packages/excalidraw/tests/zindex.test.tsx new file mode 100644 index 0000000..f15d559 --- /dev/null +++ b/packages/excalidraw/tests/zindex.test.tsx @@ -0,0 +1,1502 @@ +import React from "react"; +import { act, getCloneByOrigId, render, unmountComponent } from "./test-utils"; +import { Excalidraw } from "../index"; +import { reseed } from "../random"; +import { + actionSendBackward, + actionBringForward, + actionBringToFront, + actionSendToBack, + actionDuplicateSelection, +} from "../actions"; +import type { AppState } from "../types"; +import { API } from "./helpers/api"; +import { selectGroupsForSelectedElements } from "../groups"; +import type { + ExcalidrawElement, + ExcalidrawFrameElement, + ExcalidrawSelectionElement, +} from "../element/types"; + +unmountComponent(); + +beforeEach(() => { + localStorage.clear(); + reseed(7); +}); + +const { h } = window; + +type ExcalidrawElementType = Exclude< + ExcalidrawElement, + ExcalidrawSelectionElement +>["type"]; + +const populateElements = ( + elements: { + id: string; + type?: ExcalidrawElementType; + isDeleted?: boolean; + isSelected?: boolean; + groupIds?: string[]; + y?: number; + x?: number; + width?: number; + height?: number; + containerId?: string; + frameId?: ExcalidrawFrameElement["id"]; + index?: ExcalidrawElement["index"]; + }[], + appState?: Partial<AppState>, +) => { + const selectedElementIds: any = {}; + + const newElements = elements.map( + ({ + id, + isDeleted = false, + isSelected = false, + groupIds = [], + y = 100, + x = 100, + width = 100, + height = 100, + containerId = null, + frameId = null, + type, + }) => { + const element = API.createElement({ + type: type ?? (containerId ? "text" : "rectangle"), + id, + isDeleted, + x, + y, + width, + height, + groupIds, + containerId, + frameId: frameId || null, + }); + if (isSelected) { + selectedElementIds[element.id] = true; + } + return element; + }, + ); + + // initialize `boundElements` on containers, if applicable + API.setElements( + newElements.map((element, index, elements) => { + const nextElement = elements[index + 1]; + if ( + nextElement && + "containerId" in nextElement && + element.id === nextElement.containerId + ) { + return { + ...element, + boundElements: [{ type: "text", id: nextElement.id }], + }; + } + return element; + }), + ); + + act(() => { + h.setState({ + ...selectGroupsForSelectedElements( + { ...h.state, ...appState, selectedElementIds }, + h.elements, + h.state, + null, + ), + ...appState, + selectedElementIds, + } as AppState); + }); + + return selectedElementIds; +}; + +type Actions = + | typeof actionBringForward + | typeof actionSendBackward + | typeof actionBringToFront + | typeof actionSendToBack; + +const assertZindex = ({ + elements, + appState, + operations, +}: { + elements: { + id: string; + isDeleted?: true; + isSelected?: true; + groupIds?: string[]; + containerId?: string; + frameId?: ExcalidrawFrameElement["id"]; + type?: ExcalidrawElementType; + }[]; + appState?: Partial<AppState>; + operations: [Actions, string[]][]; +}) => { + const selectedElementIds = populateElements(elements, appState); + operations.forEach(([action, expected]) => { + API.executeAction(action); + expect(h.elements.map((element) => element.id)).toEqual(expected); + expect(h.state.selectedElementIds).toEqual(selectedElementIds); + }); +}; + +describe("z-index manipulation", () => { + beforeEach(async () => { + await render(<Excalidraw />); + }); + + it("send back", () => { + assertZindex({ + elements: [ + { id: "A" }, + { id: "B", isDeleted: true }, + { id: "C", isDeleted: true }, + { id: "D", isSelected: true }, + ], + operations: [ + [actionSendBackward, ["D", "A", "B", "C"]], + // noop + [actionSendBackward, ["D", "A", "B", "C"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A", isSelected: true }, + { id: "B", isSelected: true }, + { id: "C", isSelected: true }, + ], + operations: [ + // noop + [actionSendBackward, ["A", "B", "C"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A", isDeleted: true }, + { id: "B" }, + { id: "C", isDeleted: true }, + { id: "D", isSelected: true }, + ], + operations: [[actionSendBackward, ["A", "D", "B", "C"]]], + }); + + assertZindex({ + elements: [ + { id: "A" }, + { id: "B", isDeleted: true }, + { id: "C", isDeleted: true }, + { id: "D", isSelected: true }, + { id: "E", isSelected: true }, + { id: "F" }, + ], + operations: [ + [actionSendBackward, ["D", "E", "A", "B", "C", "F"]], + // noop + [actionSendBackward, ["D", "E", "A", "B", "C", "F"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A" }, + { id: "B" }, + { id: "C", isDeleted: true }, + { id: "D", isDeleted: true }, + { id: "E", isSelected: true }, + { id: "F" }, + { id: "G", isSelected: true }, + ], + operations: [ + [actionSendBackward, ["A", "E", "B", "C", "D", "G", "F"]], + [actionSendBackward, ["E", "A", "G", "B", "C", "D", "F"]], + [actionSendBackward, ["E", "G", "A", "B", "C", "D", "F"]], + // noop + [actionSendBackward, ["E", "G", "A", "B", "C", "D", "F"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A" }, + { id: "B" }, + { id: "C", isDeleted: true }, + { id: "D", isSelected: true }, + { id: "E", isDeleted: true }, + { id: "F", isSelected: true }, + { id: "G" }, + ], + operations: [ + [actionSendBackward, ["A", "D", "E", "F", "B", "C", "G"]], + [actionSendBackward, ["D", "E", "F", "A", "B", "C", "G"]], + // noop + [actionSendBackward, ["D", "E", "F", "A", "B", "C", "G"]], + ], + }); + + // elements should not duplicate + assertZindex({ + elements: [ + { id: "A", containerId: "C" }, + { id: "B" }, + { id: "C", isSelected: true }, + ], + operations: [ + [actionSendBackward, ["A", "C", "B"]], + // noop + [actionSendBackward, ["A", "C", "B"]], + ], + }); + + // grouped elements should be atomic + // ------------------------------------------------------------------------- + + assertZindex({ + elements: [ + { id: "A" }, + { id: "B", groupIds: ["g1"] }, + { id: "C", groupIds: ["g1"] }, + { id: "D", isDeleted: true }, + { id: "E", isDeleted: true }, + { id: "F", isSelected: true }, + ], + operations: [ + [actionSendBackward, ["A", "F", "B", "C", "D", "E"]], + [actionSendBackward, ["F", "A", "B", "C", "D", "E"]], + // noop + [actionSendBackward, ["F", "A", "B", "C", "D", "E"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A" }, + { id: "B", groupIds: ["g2", "g1"] }, + { id: "C", groupIds: ["g2", "g1"] }, + { id: "D", groupIds: ["g1"] }, + { id: "E", isDeleted: true }, + { id: "F", isSelected: true }, + ], + operations: [ + [actionSendBackward, ["A", "F", "B", "C", "D", "E"]], + [actionSendBackward, ["F", "A", "B", "C", "D", "E"]], + // noop + [actionSendBackward, ["F", "A", "B", "C", "D", "E"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A" }, + { id: "B", groupIds: ["g1"] }, + { id: "C", groupIds: ["g2", "g1"] }, + { id: "D", groupIds: ["g2", "g1"] }, + { id: "E", isDeleted: true }, + { id: "F", isSelected: true }, + ], + operations: [ + [actionSendBackward, ["A", "F", "B", "C", "D", "E"]], + [actionSendBackward, ["F", "A", "B", "C", "D", "E"]], + // noop + [actionSendBackward, ["F", "A", "B", "C", "D", "E"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A" }, + { id: "B1", groupIds: ["g1"] }, + { id: "C1", groupIds: ["g1"] }, + { id: "D2", groupIds: ["g2"], isSelected: true }, + { id: "E2", groupIds: ["g2"], isSelected: true }, + ], + appState: { + editingGroupId: null, + }, + operations: [[actionSendBackward, ["A", "D2", "E2", "B1", "C1"]]], + }); + + // in-group siblings + // ------------------------------------------------------------------------- + + assertZindex({ + elements: [ + { id: "A" }, + { id: "B", groupIds: ["g1"] }, + { id: "C", groupIds: ["g2", "g1"] }, + { id: "D", groupIds: ["g2", "g1"], isSelected: true }, + ], + appState: { + editingGroupId: "g2", + }, + operations: [ + [actionSendBackward, ["A", "B", "D", "C"]], + // noop (prevented) + [actionSendBackward, ["A", "B", "D", "C"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A" }, + { id: "B", groupIds: ["g2", "g1"] }, + { id: "C", groupIds: ["g2", "g1"] }, + { id: "D", groupIds: ["g1"], isSelected: true }, + ], + appState: { + editingGroupId: "g1", + }, + operations: [ + [actionSendBackward, ["A", "D", "B", "C"]], + // noop (prevented) + [actionSendBackward, ["A", "D", "B", "C"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A" }, + { id: "B", groupIds: ["g1"] }, + { id: "C", groupIds: ["g2", "g1"], isSelected: true }, + { id: "D", groupIds: ["g2", "g1"], isDeleted: true }, + { id: "E", groupIds: ["g2", "g1"], isSelected: true }, + ], + appState: { + editingGroupId: "g1", + }, + operations: [ + [actionSendBackward, ["A", "C", "D", "E", "B"]], + // noop (prevented) + [actionSendBackward, ["A", "C", "D", "E", "B"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A" }, + { id: "B", groupIds: ["g1"] }, + { id: "C", groupIds: ["g2", "g1"] }, + { id: "D", groupIds: ["g2", "g1"] }, + { id: "E", groupIds: ["g3", "g1"], isSelected: true }, + { id: "F", groupIds: ["g3", "g1"], isSelected: true }, + ], + appState: { + editingGroupId: "g1", + }, + operations: [ + [actionSendBackward, ["A", "B", "E", "F", "C", "D"]], + [actionSendBackward, ["A", "E", "F", "B", "C", "D"]], + // noop (prevented) + [actionSendBackward, ["A", "E", "F", "B", "C", "D"]], + ], + }); + + // invalid z-indexes across groups (legacy) → allow to sort to next sibling + assertZindex({ + elements: [ + { id: "A", groupIds: ["g1"] }, + { id: "B", groupIds: ["g2"] }, + { id: "C", groupIds: ["g1"] }, + { id: "D", groupIds: ["g2"], isSelected: true }, + { id: "E", groupIds: ["g2"], isSelected: true }, + ], + appState: { + editingGroupId: "g2", + }, + operations: [ + [actionSendBackward, ["A", "D", "E", "B", "C"]], + // noop + [actionSendBackward, ["A", "D", "E", "B", "C"]], + ], + }); + + // invalid z-indexes across groups (legacy) → allow to sort to next sibling + assertZindex({ + elements: [ + { id: "A", groupIds: ["g1"] }, + { id: "B", groupIds: ["g2"] }, + { id: "C", groupIds: ["g1"] }, + { id: "D", groupIds: ["g2"], isSelected: true }, + { id: "F" }, + { id: "G", groupIds: ["g2"], isSelected: true }, + ], + appState: { + editingGroupId: "g2", + }, + operations: [ + [actionSendBackward, ["A", "D", "G", "B", "C", "F"]], + // noop + [actionSendBackward, ["A", "D", "G", "B", "C", "F"]], + ], + }); + }); + + it("bring forward", () => { + assertZindex({ + elements: [ + { id: "A" }, + { id: "B", isSelected: true }, + { id: "C", isSelected: true }, + { id: "D", isDeleted: true }, + { id: "E" }, + ], + operations: [ + [actionBringForward, ["A", "D", "E", "B", "C"]], + // noop + [actionBringForward, ["A", "D", "E", "B", "C"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A", isSelected: true }, + { id: "B", isSelected: true }, + { id: "C", isSelected: true }, + ], + operations: [ + // noop + [actionBringForward, ["A", "B", "C"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A", isSelected: true }, + { id: "B", isDeleted: true }, + { id: "C", isDeleted: true }, + { id: "D" }, + { id: "E", isSelected: true }, + { id: "F", isDeleted: true }, + { id: "G" }, + ], + operations: [ + [actionBringForward, ["B", "C", "D", "A", "F", "G", "E"]], + [actionBringForward, ["B", "C", "D", "F", "G", "A", "E"]], + // noop + [actionBringForward, ["B", "C", "D", "F", "G", "A", "E"]], + ], + }); + + // grouped elements should be atomic + // ------------------------------------------------------------------------- + + assertZindex({ + elements: [ + { id: "A", isSelected: true }, + { id: "B", isDeleted: true }, + { id: "C", isDeleted: true }, + { id: "D", groupIds: ["g1"] }, + { id: "E", groupIds: ["g1"] }, + { id: "F" }, + ], + operations: [ + [actionBringForward, ["B", "C", "D", "E", "A", "F"]], + [actionBringForward, ["B", "C", "D", "E", "F", "A"]], + // noop + [actionBringForward, ["B", "C", "D", "E", "F", "A"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A" }, + { id: "B", isSelected: true }, + { id: "C", groupIds: ["g2", "g1"] }, + { id: "D", groupIds: ["g2", "g1"] }, + { id: "E", groupIds: ["g1"] }, + { id: "F" }, + ], + operations: [ + [actionBringForward, ["A", "C", "D", "E", "B", "F"]], + [actionBringForward, ["A", "C", "D", "E", "F", "B"]], + // noop + [actionBringForward, ["A", "C", "D", "E", "F", "B"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A" }, + { id: "B", isSelected: true }, + { id: "C", groupIds: ["g1"] }, + { id: "D", groupIds: ["g2", "g1"] }, + { id: "E", groupIds: ["g2", "g1"] }, + { id: "F" }, + ], + operations: [ + [actionBringForward, ["A", "C", "D", "E", "B", "F"]], + [actionBringForward, ["A", "C", "D", "E", "F", "B"]], + // noop + [actionBringForward, ["A", "C", "D", "E", "F", "B"]], + ], + }); + + // in-group siblings + // ------------------------------------------------------------------------- + + assertZindex({ + elements: [ + { id: "A" }, + { id: "B", groupIds: ["g2", "g1"], isSelected: true }, + { id: "C", groupIds: ["g2", "g1"] }, + { id: "D", groupIds: ["g1"] }, + ], + appState: { + editingGroupId: "g2", + }, + operations: [ + [actionBringForward, ["A", "C", "B", "D"]], + // noop (prevented) + [actionBringForward, ["A", "C", "B", "D"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A", groupIds: ["g1"], isSelected: true }, + { id: "B", groupIds: ["g2", "g1"] }, + { id: "C", groupIds: ["g2", "g1"] }, + { id: "D" }, + ], + appState: { + editingGroupId: "g1", + }, + operations: [ + [actionBringForward, ["B", "C", "A", "D"]], + // noop (prevented) + [actionBringForward, ["B", "C", "A", "D"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A", groupIds: ["g2", "g1"], isSelected: true }, + { id: "B", groupIds: ["g2", "g1"], isSelected: true }, + { id: "C", groupIds: ["g1"] }, + { id: "D" }, + ], + appState: { + editingGroupId: "g1", + }, + operations: [ + [actionBringForward, ["C", "A", "B", "D"]], + // noop (prevented) + [actionBringForward, ["C", "A", "B", "D"]], + ], + }); + + // invalid z-indexes across groups (legacy) → allow to sort to next sibling + assertZindex({ + elements: [ + { id: "A", groupIds: ["g2"], isSelected: true }, + { id: "B", groupIds: ["g2"], isSelected: true }, + { id: "C", groupIds: ["g1"] }, + { id: "D", groupIds: ["g2"] }, + { id: "E", groupIds: ["g1"] }, + ], + appState: { + editingGroupId: "g2", + }, + operations: [ + [actionBringForward, ["C", "D", "A", "B", "E"]], + // noop + [actionBringForward, ["C", "D", "A", "B", "E"]], + ], + }); + + // invalid z-indexes across groups (legacy) → allow to sort to next sibling + assertZindex({ + elements: [ + { id: "A", groupIds: ["g2"], isSelected: true }, + { id: "B" }, + { id: "C", groupIds: ["g2"], isSelected: true }, + { id: "D", groupIds: ["g1"] }, + { id: "E", groupIds: ["g2"] }, + { id: "F", groupIds: ["g1"] }, + ], + appState: { + editingGroupId: "g2", + }, + operations: [ + [actionBringForward, ["B", "D", "E", "A", "C", "F"]], + // noop + [actionBringForward, ["B", "D", "E", "A", "C", "F"]], + ], + }); + }); + + it("bring to front", () => { + assertZindex({ + elements: [ + { id: "0" }, + { id: "A", isSelected: true }, + { id: "B", isDeleted: true }, + { id: "C", isDeleted: true }, + { id: "D" }, + { id: "E", isSelected: true }, + { id: "F", isDeleted: true }, + { id: "G" }, + ], + operations: [ + [actionBringToFront, ["0", "B", "C", "D", "F", "G", "A", "E"]], + // noop + [actionBringToFront, ["0", "B", "C", "D", "F", "G", "A", "E"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A", isSelected: true }, + { id: "B", isSelected: true }, + { id: "C", isSelected: true }, + ], + operations: [ + // noop + [actionBringToFront, ["A", "B", "C"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A" }, + { id: "B", isSelected: true }, + { id: "C", isSelected: true }, + ], + operations: [ + // noop + [actionBringToFront, ["A", "B", "C"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A", isSelected: true }, + { id: "B", isSelected: true }, + { id: "C" }, + ], + operations: [ + [actionBringToFront, ["C", "A", "B"]], + // noop + [actionBringToFront, ["C", "A", "B"]], + ], + }); + + // in-group sorting + // ------------------------------------------------------------------------- + + assertZindex({ + elements: [ + { id: "A" }, + { id: "B", groupIds: ["g1"] }, + { id: "C", groupIds: ["g1"], isSelected: true }, + { id: "D", groupIds: ["g1"] }, + { id: "E", groupIds: ["g1"], isSelected: true }, + { id: "F", groupIds: ["g2", "g1"] }, + { id: "G", groupIds: ["g2", "g1"] }, + { id: "H", groupIds: ["g3", "g1"] }, + { id: "I", groupIds: ["g3", "g1"] }, + ], + appState: { + editingGroupId: "g1", + }, + operations: [ + [actionBringToFront, ["A", "B", "D", "F", "G", "H", "I", "C", "E"]], + // noop (prevented) + [actionBringToFront, ["A", "B", "D", "F", "G", "H", "I", "C", "E"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A" }, + { id: "B", groupIds: ["g2", "g1"], isSelected: true }, + { id: "D", groupIds: ["g2", "g1"] }, + { id: "C", groupIds: ["g1"] }, + ], + appState: { + editingGroupId: "g2", + }, + operations: [ + [actionBringToFront, ["A", "D", "B", "C"]], + // noop (prevented) + [actionBringToFront, ["A", "D", "B", "C"]], + ], + }); + + // invalid z-indexes across groups (legacy) → allow to sort to next sibling + assertZindex({ + elements: [ + { id: "A", groupIds: ["g2", "g3"], isSelected: true }, + { id: "B", groupIds: ["g1", "g3"] }, + { id: "C", groupIds: ["g2", "g3"] }, + { id: "D", groupIds: ["g1", "g3"] }, + ], + appState: { + editingGroupId: "g2", + }, + operations: [ + [actionBringToFront, ["B", "C", "A", "D"]], + // noop + [actionBringToFront, ["B", "C", "A", "D"]], + ], + }); + + // invalid z-indexes across groups (legacy) → allow to sort to next sibling + assertZindex({ + elements: [ + { id: "A", groupIds: ["g2"], isSelected: true }, + { id: "B", groupIds: ["g1"] }, + { id: "C", groupIds: ["g2"] }, + { id: "D", groupIds: ["g1"] }, + ], + appState: { + editingGroupId: "g2", + }, + operations: [ + [actionBringToFront, ["B", "C", "A", "D"]], + // noop + [actionBringToFront, ["B", "C", "A", "D"]], + ], + }); + }); + + it("send to back", () => { + assertZindex({ + elements: [ + { id: "A" }, + { id: "B", isDeleted: true }, + { id: "C" }, + { id: "D", isDeleted: true }, + { id: "E", isSelected: true }, + { id: "F", isDeleted: true }, + { id: "G" }, + { id: "H", isSelected: true }, + { id: "I" }, + ], + operations: [ + [actionSendToBack, ["E", "H", "A", "B", "C", "D", "F", "G", "I"]], + // noop + [actionSendToBack, ["E", "H", "A", "B", "C", "D", "F", "G", "I"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A", isSelected: true }, + { id: "B", isSelected: true }, + { id: "C", isSelected: true }, + ], + operations: [ + // noop + [actionSendToBack, ["A", "B", "C"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A", isSelected: true }, + { id: "B", isSelected: true }, + { id: "C" }, + ], + operations: [ + // noop + [actionSendToBack, ["A", "B", "C"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A" }, + { id: "B", isSelected: true }, + { id: "C", isSelected: true }, + ], + operations: [ + [actionSendToBack, ["B", "C", "A"]], + // noop + [actionSendToBack, ["B", "C", "A"]], + ], + }); + + // in-group sorting + // ------------------------------------------------------------------------- + + assertZindex({ + elements: [ + { id: "A" }, + { id: "B", groupIds: ["g2", "g1"] }, + { id: "C", groupIds: ["g2", "g1"] }, + { id: "D", groupIds: ["g3", "g1"] }, + { id: "E", groupIds: ["g3", "g1"] }, + { id: "F", groupIds: ["g1"], isSelected: true }, + { id: "G", groupIds: ["g1"] }, + { id: "H", groupIds: ["g1"], isSelected: true }, + { id: "I", groupIds: ["g1"] }, + ], + appState: { + editingGroupId: "g1", + }, + operations: [ + [actionSendToBack, ["A", "F", "H", "B", "C", "D", "E", "G", "I"]], + // noop (prevented) + [actionSendToBack, ["A", "F", "H", "B", "C", "D", "E", "G", "I"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A" }, + { id: "B", groupIds: ["g1"] }, + { id: "C", groupIds: ["g2", "g1"] }, + { id: "D", groupIds: ["g2", "g1"], isSelected: true }, + ], + appState: { + editingGroupId: "g2", + }, + operations: [ + [actionSendToBack, ["A", "B", "D", "C"]], + // noop (prevented) + [actionSendToBack, ["A", "B", "D", "C"]], + ], + }); + + // invalid z-indexes across groups (legacy) → allow to sort to next sibling + assertZindex({ + elements: [ + { id: "A", groupIds: ["g1", "g3"] }, + { id: "B", groupIds: ["g2", "g3"] }, + { id: "C", groupIds: ["g1", "g3"] }, + { id: "D", groupIds: ["g2", "g3"], isSelected: true }, + ], + appState: { + editingGroupId: "g2", + }, + operations: [ + [actionSendToBack, ["A", "D", "B", "C"]], + // noop + [actionSendToBack, ["A", "D", "B", "C"]], + ], + }); + + // invalid z-indexes across groups (legacy) → allow to sort to next sibling + assertZindex({ + elements: [ + { id: "A", groupIds: ["g1"] }, + { id: "B", groupIds: ["g2"] }, + { id: "C", groupIds: ["g1"] }, + { id: "D", groupIds: ["g2"], isSelected: true }, + ], + appState: { + editingGroupId: "g2", + }, + operations: [ + [actionSendToBack, ["A", "D", "B", "C"]], + // noop + [actionSendToBack, ["A", "D", "B", "C"]], + ], + }); + }); + + it("duplicating elements should retain zindex integrity", () => { + populateElements([ + { id: "A", isSelected: true }, + { id: "B", isSelected: true }, + ]); + API.executeAction(actionDuplicateSelection); + expect(h.elements).toMatchObject([ + { id: "A" }, + { id: getCloneByOrigId("A").id }, + { id: "B" }, + { id: getCloneByOrigId("B").id }, + ]); + + populateElements([ + { id: "A", groupIds: ["g1"], isSelected: true }, + { id: "B", groupIds: ["g1"], isSelected: true }, + ]); + API.executeAction(actionDuplicateSelection); + expect(h.elements).toMatchObject([ + { id: "A" }, + { id: "B" }, + { + id: getCloneByOrigId("A").id, + + groupIds: [expect.stringMatching(/.{3,}/)], + }, + { + id: getCloneByOrigId("B").id, + + groupIds: [expect.stringMatching(/.{3,}/)], + }, + ]); + + populateElements([ + { id: "A", groupIds: ["g1"], isSelected: true }, + { id: "B", groupIds: ["g1"], isSelected: true }, + { id: "C" }, + ]); + API.executeAction(actionDuplicateSelection); + expect(h.elements).toMatchObject([ + { id: "A" }, + { id: "B" }, + { + id: getCloneByOrigId("A").id, + + groupIds: [expect.stringMatching(/.{3,}/)], + }, + { + id: getCloneByOrigId("B").id, + + groupIds: [expect.stringMatching(/.{3,}/)], + }, + { id: "C" }, + ]); + + populateElements([ + { id: "A", groupIds: ["g1"], isSelected: true }, + { id: "B", groupIds: ["g1"], isSelected: true }, + { id: "C", isSelected: true }, + ]); + API.executeAction(actionDuplicateSelection); + expect(h.elements.map((element) => element.id)).toEqual([ + "A", + "B", + getCloneByOrigId("A").id, + getCloneByOrigId("B").id, + "C", + getCloneByOrigId("C").id, + ]); + + populateElements([ + { id: "A", groupIds: ["g1"], isSelected: true }, + { id: "B", groupIds: ["g1"], isSelected: true }, + { id: "C", groupIds: ["g2"], isSelected: true }, + { id: "D", groupIds: ["g2"], isSelected: true }, + ]); + API.executeAction(actionDuplicateSelection); + expect(h.elements.map((element) => element.id)).toEqual([ + "A", + "B", + getCloneByOrigId("A").id, + getCloneByOrigId("B").id, + "C", + "D", + getCloneByOrigId("C").id, + getCloneByOrigId("D").id, + ]); + + populateElements( + [ + { id: "A", groupIds: ["g1", "g2"], isSelected: true }, + { id: "B", groupIds: ["g1", "g2"], isSelected: true }, + { id: "C", groupIds: ["g2"], isSelected: true }, + ], + { + selectedGroupIds: { g1: true }, + }, + ); + API.executeAction(actionDuplicateSelection); + expect(h.elements.map((element) => element.id)).toEqual([ + "A", + "B", + getCloneByOrigId("A").id, + getCloneByOrigId("B").id, + "C", + getCloneByOrigId("C").id, + ]); + + populateElements( + [ + { id: "A", groupIds: ["g1", "g2"], isSelected: true }, + { id: "B", groupIds: ["g1", "g2"], isSelected: true }, + { id: "C", groupIds: ["g2"], isSelected: true }, + ], + { + selectedGroupIds: { g2: true }, + }, + ); + API.executeAction(actionDuplicateSelection); + expect(h.elements.map((element) => element.id)).toEqual([ + "A", + "B", + "C", + getCloneByOrigId("A").id, + getCloneByOrigId("B").id, + getCloneByOrigId("C").id, + ]); + + populateElements( + [ + { id: "A", groupIds: ["g1", "g2"], isSelected: true }, + { id: "B", groupIds: ["g1", "g2"], isSelected: true }, + { id: "C", groupIds: ["g2"], isSelected: true }, + { id: "D", groupIds: ["g3", "g4"], isSelected: true }, + { id: "E", groupIds: ["g3", "g4"], isSelected: true }, + { id: "F", groupIds: ["g4"], isSelected: true }, + ], + { + selectedGroupIds: { g2: true, g4: true }, + }, + ); + API.executeAction(actionDuplicateSelection); + expect(h.elements.map((element) => element.id)).toEqual([ + "A", + "B", + "C", + getCloneByOrigId("A").id, + getCloneByOrigId("B").id, + getCloneByOrigId("C").id, + "D", + "E", + "F", + getCloneByOrigId("D").id, + getCloneByOrigId("E").id, + getCloneByOrigId("F").id, + ]); + + populateElements( + [ + { id: "A", groupIds: ["g1", "g2"], isSelected: true }, + { id: "B", groupIds: ["g1", "g2"] }, + { id: "C", groupIds: ["g2"] }, + ], + { editingGroupId: "g1" }, + ); + API.executeAction(actionDuplicateSelection); + expect(h.elements.map((element) => element.id)).toEqual([ + "A", + getCloneByOrigId("A").id, + "B", + "C", + ]); + + populateElements( + [ + { id: "A", groupIds: ["g1", "g2"] }, + { id: "B", groupIds: ["g1", "g2"], isSelected: true }, + { id: "C", groupIds: ["g2"] }, + ], + { editingGroupId: "g1" }, + ); + API.executeAction(actionDuplicateSelection); + expect(h.elements.map((element) => element.id)).toEqual([ + "A", + "B", + getCloneByOrigId("B").id, + "C", + ]); + + populateElements( + [ + { id: "A", groupIds: ["g1", "g2"], isSelected: true }, + { id: "B", groupIds: ["g1", "g2"], isSelected: true }, + { id: "C", groupIds: ["g2"] }, + ], + { editingGroupId: "g1" }, + ); + API.executeAction(actionDuplicateSelection); + expect(h.elements.map((element) => element.id)).toEqual([ + "A", + getCloneByOrigId("A").id, + "B", + getCloneByOrigId("B").id, + "C", + ]); + }); + + it("duplicating incorrectly interleaved elements (group elements should be together) should still produce reasonable result", () => { + populateElements([ + { id: "A", groupIds: ["g1"], isSelected: true }, + { id: "B" }, + { id: "C", groupIds: ["g1"], isSelected: true }, + ]); + API.executeAction(actionDuplicateSelection); + expect(h.elements.map((element) => element.id)).toEqual([ + "A", + "C", + getCloneByOrigId("A").id, + getCloneByOrigId("C").id, + "B", + ]); + }); + + it("group-selected duplication should includes deleted elements that weren't selected on account of being deleted", () => { + populateElements([ + { id: "A", groupIds: ["g1"], isDeleted: true }, + { id: "B", groupIds: ["g1"], isSelected: true }, + { id: "C", groupIds: ["g1"], isSelected: true }, + { id: "D" }, + ]); + expect(h.state.selectedGroupIds).toEqual({ g1: true }); + API.executeAction(actionDuplicateSelection); + expect(h.elements.map((element) => element.id)).toEqual([ + "A", + "B", + "C", + getCloneByOrigId("A").id, + getCloneByOrigId("B").id, + getCloneByOrigId("C").id, + "D", + ]); + }); + + it("text-container binding should be atomic", () => { + assertZindex({ + elements: [ + { id: "A", isSelected: true }, + { id: "B" }, + { id: "C", containerId: "B" }, + ], + operations: [ + [actionBringForward, ["B", "C", "A"]], + [actionSendBackward, ["A", "B", "C"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A" }, + { id: "B", isSelected: true }, + { id: "C", containerId: "B" }, + ], + operations: [ + [actionSendBackward, ["B", "C", "A"]], + [actionBringForward, ["A", "B", "C"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A", isSelected: true, groupIds: ["g1"] }, + { id: "B", groupIds: ["g1"] }, + { id: "C", containerId: "B", groupIds: ["g1"] }, + ], + appState: { + editingGroupId: "g1", + }, + operations: [ + [actionBringForward, ["B", "C", "A"]], + [actionSendBackward, ["A", "B", "C"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A", groupIds: ["g1"] }, + { id: "B", groupIds: ["g1"], isSelected: true }, + { id: "C", containerId: "B", groupIds: ["g1"] }, + ], + appState: { + editingGroupId: "g1", + }, + operations: [ + [actionSendBackward, ["B", "C", "A"]], + [actionBringForward, ["A", "B", "C"]], + ], + }); + + assertZindex({ + elements: [ + { id: "A", groupIds: ["g1"] }, + { id: "B", isSelected: true, groupIds: ["g1"] }, + { id: "C" }, + { id: "D", containerId: "C" }, + ], + appState: { + editingGroupId: "g1", + }, + operations: [[actionBringForward, ["A", "B", "C", "D"]]], + }); + }); +}); + +describe("z-indexing with frames", () => { + beforeEach(async () => { + await render(<Excalidraw />); + }); + + // naming scheme: + // F# ... frame element + // F#_# ... frame child of F# (rectangle) + // R# ... unrelated element (rectangle) + + it("moving whole frame by one (normalized)", () => { + // normalized frame order + assertZindex({ + elements: [ + { id: "F1_1", frameId: "F1" }, + { id: "F1_2", frameId: "F1" }, + { id: "F1", type: "frame", isSelected: true }, + { id: "R1" }, + { id: "R2" }, + ], + operations: [ + // +1 + [actionBringForward, ["R1", "F1_1", "F1_2", "F1", "R2"]], + // +1 + [actionBringForward, ["R1", "R2", "F1_1", "F1_2", "F1"]], + // noop + [actionBringForward, ["R1", "R2", "F1_1", "F1_2", "F1"]], + // -1 + [actionSendBackward, ["R1", "F1_1", "F1_2", "F1", "R2"]], + // -1 + [actionSendBackward, ["F1_1", "F1_2", "F1", "R1", "R2"]], + // noop + [actionSendBackward, ["F1_1", "F1_2", "F1", "R1", "R2"]], + ], + }); + }); + + it("moving whole frame by one (DENORMALIZED)", () => { + // DENORMALIZED FRAME ORDER + assertZindex({ + elements: [ + { id: "F1_1", frameId: "F1" }, + { id: "F1", type: "frame", isSelected: true }, + { id: "F1_2", frameId: "F1" }, + { id: "R1" }, + { id: "R2" }, + ], + operations: [ + // +1 + [actionBringForward, ["R1", "F1_1", "F1", "F1_2", "R2"]], + // +1 + [actionBringForward, ["R1", "R2", "F1_1", "F1", "F1_2"]], + // noop + [actionBringForward, ["R1", "R2", "F1_1", "F1", "F1_2"]], + ], + }); + + // DENORMALIZED FRAME ORDER + assertZindex({ + elements: [ + { id: "F1_1", frameId: "F1" }, + { id: "F1", type: "frame", isSelected: true }, + { id: "R1" }, + { id: "F1_2", frameId: "F1" }, + { id: "R2" }, + ], + operations: [ + // +1 + [actionBringForward, ["R1", "F1_1", "F1", "R2", "F1_2"]], + // +1 + [actionBringForward, ["R1", "R2", "F1_1", "F1", "F1_2"]], + // noop + [actionBringForward, ["R1", "R2", "F1_1", "F1", "F1_2"]], + ], + }); + + // DENORMALIZED FRAME ORDER + assertZindex({ + elements: [ + { id: "F1_1", frameId: "F1" }, + { id: "R1" }, + { id: "F1", type: "frame", isSelected: true }, + { id: "R2" }, + { id: "F1_2", frameId: "F1" }, + { id: "R3" }, + ], + operations: [ + // +1 + [actionBringForward, ["R1", "F1_1", "R2", "F1", "R3", "F1_2"]], + // +1 + // FIXME incorrect, should put F1_1 after R3 + [actionBringForward, ["R1", "R2", "F1_1", "R3", "F1", "F1_2"]], + // +1 + // FIXME should be noop from previous step after it's fixed + [actionBringForward, ["R1", "R2", "R3", "F1_1", "F1", "F1_2"]], + ], + }); + + // DENORMALIZED FRAME ORDER + assertZindex({ + elements: [ + { id: "F1_1", frameId: "F1" }, + { id: "R1" }, + { id: "F1", type: "frame", isSelected: true }, + { id: "R2" }, + { id: "F1_2", frameId: "F1" }, + { id: "R3" }, + ], + operations: [ + // -1 + [actionSendBackward, ["F1_1", "F1", "R1", "F1_2", "R2", "R3"]], + // -1 + [actionSendBackward, ["F1_1", "F1", "F1_2", "R1", "R2", "R3"]], + ], + }); + }); + + it("moving selected frame children by one (normalized)", () => { + // normalized frame order + assertZindex({ + elements: [ + { id: "F1_1", frameId: "F1", isSelected: true }, + { id: "F1_2", frameId: "F1" }, + { id: "F1", type: "frame" }, + { id: "R1" }, + ], + operations: [ + // +1 + [actionBringForward, ["F1_2", "F1_1", "F1", "R1"]], + // noop + [actionBringForward, ["F1_2", "F1_1", "F1", "R1"]], + ], + }); + + // normalized frame order, multiple frames + assertZindex({ + elements: [ + { id: "F1_1", frameId: "F1", isSelected: true }, + { id: "F1_2", frameId: "F1" }, + { id: "F1", type: "frame" }, + { id: "R1" }, + { id: "F2_1", frameId: "F2", isSelected: true }, + { id: "F2_2", frameId: "F2" }, + { id: "F2", type: "frame" }, + { id: "R2" }, + ], + operations: [ + // +1 + [ + actionBringForward, + ["F1_2", "F1_1", "F1", "R1", "F2_2", "F2_1", "F2", "R2"], + ], + // noop + [ + actionBringForward, + ["F1_2", "F1_1", "F1", "R1", "F2_2", "F2_1", "F2", "R2"], + ], + ], + }); + }); + + it("moving selected frame children by one (DENORMALIZED)", () => { + // DENORMALIZED FRAME ORDER + assertZindex({ + elements: [ + { id: "F1_1", frameId: "F1", isSelected: true }, + { id: "F1", type: "frame" }, + { id: "F1_2", frameId: "F1" }, + { id: "R1" }, + ], + operations: [ + // +1 + // NOTE not sure what we wanna do here + [actionBringForward, ["F1", "F1_2", "F1_1", "R1"]], + // noop + [actionBringForward, ["F1", "F1_2", "F1_1", "R1"]], + // -1 + [actionSendBackward, ["F1", "F1_1", "F1_2", "R1"]], + // noop + [actionSendBackward, ["F1", "F1_1", "F1_2", "R1"]], + ], + }); + + // DENORMALIZED FRAME ORDER + assertZindex({ + elements: [ + { id: "F1_1", frameId: "F1", isSelected: true }, + { id: "R1" }, + { id: "F1", type: "frame" }, + { id: "F1_2", frameId: "F1" }, + { id: "R2" }, + ], + operations: [ + // +1 + // NOTE not sure what we wanna do here + [actionBringForward, ["R1", "F1", "F1_2", "F1_1", "R2"]], + // noop + [actionBringForward, ["R1", "F1", "F1_2", "F1_1", "R2"]], + // -1 + [actionSendBackward, ["R1", "F1", "F1_1", "F1_2", "R2"]], + // noop + [actionSendBackward, ["R1", "F1", "F1_1", "F1_2", "R2"]], + ], + }); + }); + + it("moving whole frame to front/end", () => { + // normalized frame order + assertZindex({ + elements: [ + { id: "F1_1", frameId: "F1" }, + { id: "F1_2", frameId: "F1" }, + { id: "F1", type: "frame", isSelected: true }, + { id: "R1" }, + { id: "R2" }, + ], + operations: [ + // +∞ + [actionBringToFront, ["R1", "R2", "F1_1", "F1_2", "F1"]], + // noop + [actionBringToFront, ["R1", "R2", "F1_1", "F1_2", "F1"]], + // -∞ + [actionSendToBack, ["F1_1", "F1_2", "F1", "R1", "R2"]], + // noop + [actionSendToBack, ["F1_1", "F1_2", "F1", "R1", "R2"]], + ], + }); + + // DENORMALIZED FRAME ORDER + assertZindex({ + elements: [ + { id: "F1_1", frameId: "F1" }, + { id: "F1", type: "frame", isSelected: true }, + { id: "F1_2", frameId: "F1" }, + { id: "R1" }, + { id: "R2" }, + ], + operations: [ + // +∞ + [actionBringToFront, ["R1", "R2", "F1_1", "F1", "F1_2"]], + // noop + [actionBringToFront, ["R1", "R2", "F1_1", "F1", "F1_2"]], + // -∞ + [actionSendToBack, ["F1_1", "F1", "F1_2", "R1", "R2"]], + // noop + [actionSendToBack, ["F1_1", "F1", "F1_2", "R1", "R2"]], + ], + }); + + // DENORMALIZED FRAME ORDER + assertZindex({ + elements: [ + { id: "F1_1", frameId: "F1" }, + { id: "F1", type: "frame", isSelected: true }, + { id: "R1" }, + { id: "F1_2", frameId: "F1" }, + { id: "R2" }, + ], + operations: [ + // +∞ + [actionBringToFront, ["R1", "R2", "F1_1", "F1", "F1_2"]], + ], + }); + + // DENORMALIZED FRAME ORDER + assertZindex({ + elements: [ + { id: "F1_1", frameId: "F1" }, + { id: "R1" }, + { id: "F1", type: "frame", isSelected: true }, + { id: "R2" }, + { id: "F1_2", frameId: "F1" }, + { id: "R3" }, + ], + operations: [ + // +1 + [actionBringToFront, ["R1", "R2", "R3", "F1_1", "F1", "F1_2"]], + ], + }); + }); +}); |
