diff --git a/package-lock.json b/package-lock.json index 45464109..97b3eb46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1292,6 +1292,12 @@ "any-observable": "^0.3.0" } }, + "@sheerun/mutationobserver-shim": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz", + "integrity": "sha512-vTCdPp/T/Q3oSqwHmZ5Kpa9oI7iLtGl3RQaA/NyLHikvcrPxACkkKVr/XzkSPJWXHRhKGzVvb0urJsbMlRxi1Q==", + "dev": true + }, "@svgr/babel-plugin-add-jsx-attribute": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz", @@ -1401,6 +1407,189 @@ "loader-utils": "^1.2.3" } }, + "@testing-library/dom": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-6.12.2.tgz", + "integrity": "sha512-KCnvHra5fV+wDxg3wJObGvZFxq7v1DJt829GNFLuRDjKxVNc/B5AdsylNF5PMHFbWMXDsHwM26d2NZcZO9KjbQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.6.2", + "@sheerun/mutationobserver-shim": "^0.3.2", + "@types/testing-library__dom": "^6.0.0", + "aria-query": "3.0.0", + "pretty-format": "^24.9.0", + "wait-for-expect": "^3.0.0" + } + }, + "@testing-library/jest-dom": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.1.1.tgz", + "integrity": "sha512-7xnmBFcUmmUVAUhFiZ/u3CxFh1e46THAwra4SiiKNCW4By26RedCRwEk0rtleFPZG0wlTSNOKDvJjWYy93dp0w==", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.3", + "@types/testing-library__jest-dom": "^5.0.0", + "chalk": "^3.0.0", + "css": "^2.2.4", + "css.escape": "^1.5.1", + "jest-diff": "^25.1.0", + "jest-matcher-utils": "^25.1.0", + "lodash": "^4.17.15", + "pretty-format": "^25.1.0", + "redent": "^3.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.1.0.tgz", + "integrity": "sha512-VpOtt7tCrgvamWZh1reVsGADujKigBUFTi19mlRjqEGsE8qH4r3s+skY33dNdXOwyZIvuftZ5tqdF1IgsMejMA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/yargs": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.3.tgz", + "integrity": "sha512-XCMQRK6kfpNBixHLyHUsGmXrpEmFFxzMrcnSXFMziHd8CoNJo8l16FkHyQq4x+xbM7E2XL83/O78OD8u+iZTdQ==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "diff-sequences": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.1.0.tgz", + "integrity": "sha512-nFIfVk5B/NStCsJ+zaPO4vYuLjlzQ6uFvPxzYyHlejNZ/UGa7G/n7peOXVrVNvRuyfstt+mZQYGpjxg9Z6N8Kw==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "jest-diff": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.1.0.tgz", + "integrity": "sha512-nepXgajT+h017APJTreSieh4zCqnSHEJ1iT8HDlewu630lSJ4Kjjr9KNzm+kzGwwcpsDE6Snx1GJGzzsefaEHw==", + "dev": true, + "requires": { + "chalk": "^3.0.0", + "diff-sequences": "^25.1.0", + "jest-get-type": "^25.1.0", + "pretty-format": "^25.1.0" + } + }, + "jest-get-type": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.1.0.tgz", + "integrity": "sha512-yWkBnT+5tMr8ANB6V+OjmrIJufHtCAqI5ic2H40v+tRqxDmE0PGnIiTyvRWFOMtmVHYpwRqyazDbTnhpjsGvLw==", + "dev": true + }, + "jest-matcher-utils": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-25.1.0.tgz", + "integrity": "sha512-KGOAFcSFbclXIFE7bS4C53iYobKI20ZWleAdAFun4W1Wz1Kkej8Ng6RRbhL8leaEvIOjGXhGf/a1JjO8bkxIWQ==", + "dev": true, + "requires": { + "chalk": "^3.0.0", + "jest-diff": "^25.1.0", + "jest-get-type": "^25.1.0", + "pretty-format": "^25.1.0" + } + }, + "pretty-format": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.1.0.tgz", + "integrity": "sha512-46zLRSGLd02Rp+Lhad9zzuNZ+swunitn8zIpfD2B4OPCRLXbM87RJT2aBLBWYOznNUML/2l/ReMyWNC80PJBUQ==", + "dev": true, + "requires": { + "@jest/types": "^25.1.0", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" + } + }, + "redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "requires": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + } + }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "requires": { + "min-indent": "^1.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@testing-library/react": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-9.4.0.tgz", + "integrity": "sha512-XdhDWkI4GktUPsz0AYyeQ8M9qS/JFie06kcSnUVcpgOwFjAu9vhwR83qBl+lw9yZWkbECjL8Hd+n5hH6C0oWqg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.7.6", + "@testing-library/dom": "^6.11.0", + "@types/testing-library__react": "^9.1.2" + } + }, "@types/babel__core": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.3.tgz", @@ -1668,6 +1857,34 @@ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==" }, + "@types/testing-library__dom": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@types/testing-library__dom/-/testing-library__dom-6.12.0.tgz", + "integrity": "sha512-PQ/gzABzc53T68RldZ/sJHKCihtP9ofU8XIgOk+H7tlfoCRdg9mqICio5Fo8j3Z8wo+pOfuDsuPprWsn3YtVmA==", + "dev": true, + "requires": { + "pretty-format": "^24.3.0" + } + }, + "@types/testing-library__jest-dom": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.0.1.tgz", + "integrity": "sha512-GiPXQBVF9O4DG9cssD2d266vozBJvC5Tnv6aeH5ujgYJgys1DYm9AFCz7YC+STR5ksGxq3zCt+yP8T1wbk2DFg==", + "dev": true, + "requires": { + "@types/jest": "*" + } + }, + "@types/testing-library__react": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@types/testing-library__react/-/testing-library__react-9.1.2.tgz", + "integrity": "sha512-CYaMqrswQ+cJACy268jsLAw355DZtPZGt3Jwmmotlcu8O/tkoXBI6AeZ84oZBJsIsesozPKzWzmv/0TIU+1E9Q==", + "dev": true, + "requires": { + "@types/react-dom": "*", + "@types/testing-library__dom": "*" + } + }, "@types/yargs": { "version": "13.0.8", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.8.tgz", @@ -3863,6 +4080,12 @@ "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.2.1.tgz", "integrity": "sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw==" }, + "css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=", + "dev": true + }, "cssdb": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-4.4.0.tgz", @@ -3873,6 +4096,12 @@ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" }, + "cssfontparser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/cssfontparser/-/cssfontparser-1.2.1.tgz", + "integrity": "sha1-9AIvyPlwDGgCnVQghK+69CWj8+M=", + "dev": true + }, "cssnano": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz", @@ -7154,6 +7383,16 @@ } } }, + "jest-canvas-mock": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-2.2.0.tgz", + "integrity": "sha512-DcJdchb7eWFZkt6pvyceWWnu3lsp5QWbUeXiKgEMhwB3sMm5qHM1GQhDajvJgBeiYpgKcojbzZ53d/nz6tXvJw==", + "dev": true, + "requires": { + "cssfontparser": "^1.2.1", + "parse-color": "^1.0.0" + } + }, "jest-changed-files": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-24.9.0.tgz", @@ -9390,6 +9629,12 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" }, + "min-indent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.0.tgz", + "integrity": "sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY=", + "dev": true + }, "mini-css-extract-plugin": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz", @@ -10359,6 +10604,23 @@ "safe-buffer": "^5.1.1" } }, + "parse-color": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-color/-/parse-color-1.0.0.tgz", + "integrity": "sha1-e3SLlag/A/FqlPU15S1/PZRlhhk=", + "dev": true, + "requires": { + "color-convert": "~0.5.0" + }, + "dependencies": { + "color-convert": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", + "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=", + "dev": true + } + } + }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -14952,6 +15214,12 @@ "xml-name-validator": "^3.0.0" } }, + "wait-for-expect": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/wait-for-expect/-/wait-for-expect-3.0.1.tgz", + "integrity": "sha512-3Ha7lu+zshEG/CeHdcpmQsZnnZpPj/UsG3DuKO8FskjuDbkx3jE3845H+CuwZjA2YWYDfKMU2KhnCaXMLd3wVw==", + "dev": true + }, "walker": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", diff --git a/package.json b/package.json index 211a2258..313e3b89 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ }, "description": "", "devDependencies": { + "@testing-library/jest-dom": "5.1.1", + "@testing-library/react": "9.4.0", "@types/jest": "25.1.2", "@types/nanoid": "2.1.0", "@types/react": "16.9.19", @@ -24,12 +26,18 @@ "eslint-config-prettier": "6.10.0", "eslint-plugin-prettier": "3.1.2", "husky": "4.2.1", + "jest-canvas-mock": "2.2.0", "lint-staged": "10.0.7", "node-sass": "4.13.1", "prettier": "1.19.1", "rewire": "4.0.1", "typescript": "3.7.5" }, + "jest": { + "transformIgnorePatterns": [ + "node_modules/(?!(roughjs|browser-nativefs)/)" + ] + }, "eslintConfig": { "extends": [ "prettier", diff --git a/src/index.tsx b/src/index.tsx index 19133e38..c74ad1f7 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -475,6 +475,9 @@ export class App extends React.Component { res: ActionResult, commitToHistory: boolean = true, ) => { + if (this.unmounted) { + return; + } if (res.elements) { elements = res.elements; if (commitToHistory) { @@ -515,6 +518,11 @@ export class App extends React.Component { this.saveDebounced.flush(); }; + private disableEvent: EventHandlerNonNull = e => { + e.preventDefault(); + }; + + private unmounted = false; public async componentDidMount() { document.addEventListener("copy", this.onCopy); document.addEventListener("paste", this.pasteFromClipboard); @@ -526,28 +534,32 @@ export class App extends React.Component { window.addEventListener("resize", this.onResize, false); window.addEventListener("unload", this.onUnload, false); window.addEventListener("blur", this.onUnload, false); - window.addEventListener("dragover", e => e.preventDefault(), false); - window.addEventListener("drop", e => e.preventDefault(), false); + window.addEventListener("dragover", this.disableEvent, false); + window.addEventListener("drop", this.disableEvent, false); const searchParams = new URLSearchParams(window.location.search); const id = searchParams.get("id"); if (id) { // Backwards compatibility with legacy url format - this.syncActionResult(await loadScene(id)); + const scene = await loadScene(id); + this.syncActionResult(scene); } else { const match = window.location.hash.match( /^#json=([0-9]+),([a-zA-Z0-9_-]+)$/, ); if (match) { - this.syncActionResult(await loadScene(match[1], match[2])); + const scene = await loadScene(match[1], match[2]); + this.syncActionResult(scene); } else { - this.syncActionResult(await loadScene(null)); + const scene = await loadScene(null); + this.syncActionResult(scene); } } } public componentWillUnmount() { + this.unmounted = true; document.removeEventListener("copy", this.onCopy); document.removeEventListener("paste", this.pasteFromClipboard); document.removeEventListener("cut", this.onCut); @@ -558,9 +570,12 @@ export class App extends React.Component { this.updateCurrentCursorPosition, false, ); + document.removeEventListener("keyup", this.onKeyUp); window.removeEventListener("resize", this.onResize, false); window.removeEventListener("unload", this.onUnload, false); window.removeEventListener("blur", this.onUnload, false); + window.removeEventListener("dragover", this.disableEvent, false); + window.removeEventListener("drop", this.disableEvent, false); } public state: AppState = getDefaultAppState(); diff --git a/src/setupTests.ts b/src/setupTests.ts new file mode 100644 index 00000000..fe48d2c4 --- /dev/null +++ b/src/setupTests.ts @@ -0,0 +1,8 @@ +import "@testing-library/jest-dom"; +import "jest-canvas-mock"; + +// ReactDOM is located inside index.tsx file +// as a result, we need a place for it to render into +const element = document.createElement("div"); +element.id = "root"; +document.body.appendChild(element); diff --git a/src/tests/dragCreate.test.tsx b/src/tests/dragCreate.test.tsx new file mode 100644 index 00000000..adcf4af6 --- /dev/null +++ b/src/tests/dragCreate.test.tsx @@ -0,0 +1,277 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import { App } from "../index"; +import * as Renderer from "../renderer/renderScene"; +import { KEYS } from "../keys"; +import { render, fireEvent } from "./test-utils"; + +// Unmount ReactDOM from root +ReactDOM.unmountComponentAtNode(document.getElementById("root")!); + +const renderScene = jest.spyOn(Renderer, "renderScene"); +beforeEach(() => { + localStorage.clear(); + renderScene.mockClear(); +}); + +describe("add element to the scene when mouse dragging long enough", () => { + it("rectangle", () => { + const { getByToolName, container } = render(); + // select tool + const tool = getByToolName("rectangle"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas")!; + + // start from (30, 20) + fireEvent.mouseDown(canvas, { clientX: 30, clientY: 20 }); + + // move to (60,70) + fireEvent.mouseMove(canvas, { clientX: 60, clientY: 70 }); + + // finish (position does not matter) + fireEvent.mouseUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(4); + expect(renderScene.mock.calls[3][1]).toBeNull(); + const elements = renderScene.mock.calls[3][0]; + + expect(elements.length).toEqual(1); + expect(elements[0].type).toEqual("rectangle"); + expect(elements[0].x).toEqual(30); + expect(elements[0].y).toEqual(20); + expect(elements[0].width).toEqual(30); // 60 - 30 + expect(elements[0].height).toEqual(50); // 70 - 20 + }); + + it("ellipse", () => { + const { getByToolName, container } = render(); + // select tool + const tool = getByToolName("ellipse"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas")!; + + // start from (30, 20) + fireEvent.mouseDown(canvas, { clientX: 30, clientY: 20 }); + + // move to (60,70) + fireEvent.mouseMove(canvas, { clientX: 60, clientY: 70 }); + + // finish (position does not matter) + fireEvent.mouseUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(4); + expect(renderScene.mock.calls[3][1]).toBeNull(); + + const elements = renderScene.mock.calls[3][0]; + expect(elements.length).toEqual(1); + expect(elements[0].type).toEqual("ellipse"); + expect(elements[0].x).toEqual(30); + expect(elements[0].y).toEqual(20); + expect(elements[0].width).toEqual(30); // 60 - 30 + expect(elements[0].height).toEqual(50); // 70 - 20 + }); + + it("diamond", () => { + const { getByToolName, container } = render(); + // select tool + const tool = getByToolName("diamond"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas")!; + + // start from (30, 20) + fireEvent.mouseDown(canvas, { clientX: 30, clientY: 20 }); + + // move to (60,70) + fireEvent.mouseMove(canvas, { clientX: 60, clientY: 70 }); + + // finish (position does not matter) + fireEvent.mouseUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(4); + expect(renderScene.mock.calls[3][1]).toBeNull(); + + const elements = renderScene.mock.calls[3][0]; + expect(elements.length).toEqual(1); + expect(elements[0].type).toEqual("diamond"); + expect(elements[0].x).toEqual(30); + expect(elements[0].y).toEqual(20); + expect(elements[0].width).toEqual(30); // 60 - 30 + expect(elements[0].height).toEqual(50); // 70 - 20 + }); + + it("arrow", () => { + const { getByToolName, container } = render(); + // select tool + const tool = getByToolName("arrow"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas")!; + + // start from (30, 20) + fireEvent.mouseDown(canvas, { clientX: 30, clientY: 20 }); + + // move to (60,70) + fireEvent.mouseMove(canvas, { clientX: 60, clientY: 70 }); + + // finish (position does not matter) + fireEvent.mouseUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(4); + expect(renderScene.mock.calls[3][1]).toBeNull(); + const elements = renderScene.mock.calls[3][0]; + + expect(elements.length).toEqual(1); + expect(elements[0].type).toEqual("arrow"); + expect(elements[0].x).toEqual(30); + expect(elements[0].y).toEqual(20); + expect(elements[0].points.length).toEqual(2); + expect(elements[0].points[0]).toEqual([0, 0]); + expect(elements[0].points[1]).toEqual([30, 50]); // (60 - 30, 70 - 20) + }); + + it("line", () => { + const { getByToolName, container } = render(); + // select tool + const tool = getByToolName("line"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas")!; + + // start from (30, 20) + fireEvent.mouseDown(canvas, { clientX: 30, clientY: 20 }); + + // move to (60,70) + fireEvent.mouseMove(canvas, { clientX: 60, clientY: 70 }); + + // finish (position does not matter) + fireEvent.mouseUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(4); + expect(renderScene.mock.calls[3][1]).toBeNull(); + const elements = renderScene.mock.calls[3][0]; + + expect(elements.length).toEqual(1); + expect(elements[0].type).toEqual("line"); + expect(elements[0].x).toEqual(30); + expect(elements[0].y).toEqual(20); + expect(elements[0].points.length).toEqual(2); + expect(elements[0].points[0]).toEqual([0, 0]); + expect(elements[0].points[1]).toEqual([30, 50]); // (60 - 30, 70 - 20) + }); +}); + +describe("do not add element to the scene if size is too small", () => { + it("rectangle", () => { + const { getByToolName, container } = render(); + // select tool + const tool = getByToolName("rectangle"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas")!; + + // start from (30, 20) + fireEvent.mouseDown(canvas, { clientX: 30, clientY: 20 }); + + // finish (position does not matter) + fireEvent.mouseUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(3); + expect(renderScene.mock.calls[2][1]).toBeNull(); + const elements = renderScene.mock.calls[2][0]; + + expect(elements.length).toEqual(0); + }); + + it("ellipse", () => { + const { getByToolName, container } = render(); + // select tool + const tool = getByToolName("ellipse"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas")!; + + // start from (30, 20) + fireEvent.mouseDown(canvas, { clientX: 30, clientY: 20 }); + + // finish (position does not matter) + fireEvent.mouseUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(3); + expect(renderScene.mock.calls[2][1]).toBeNull(); + const elements = renderScene.mock.calls[2][0]; + + expect(elements.length).toEqual(0); + }); + + it("diamond", () => { + const { getByToolName, container } = render(); + // select tool + const tool = getByToolName("diamond"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas")!; + + // start from (30, 20) + fireEvent.mouseDown(canvas, { clientX: 30, clientY: 20 }); + + // finish (position does not matter) + fireEvent.mouseUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(3); + expect(renderScene.mock.calls[2][1]).toBeNull(); + const elements = renderScene.mock.calls[2][0]; + + expect(elements.length).toEqual(0); + }); + + it("arrow", () => { + const { getByToolName, container } = render(); + // select tool + const tool = getByToolName("arrow"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas")!; + + // start from (30, 20) + fireEvent.mouseDown(canvas, { clientX: 30, clientY: 20 }); + + // finish (position does not matter) + fireEvent.mouseUp(canvas); + + // we need to finalize it because arrows and lines enter multi-mode + fireEvent.keyDown(document, { key: KEYS.ENTER }); + + expect(renderScene).toHaveBeenCalledTimes(4); + expect(renderScene.mock.calls[3][1]).toBeNull(); + const elements = renderScene.mock.calls[3][0]; + + expect(elements.length).toEqual(0); + }); + + it("line", () => { + const { getByToolName, container } = render(); + // select tool + const tool = getByToolName("line"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas")!; + + // start from (30, 20) + fireEvent.mouseDown(canvas, { clientX: 30, clientY: 20 }); + + // finish (position does not matter) + fireEvent.mouseUp(canvas); + + // we need to finalize it because arrows and lines enter multi-mode + fireEvent.keyDown(document, { key: KEYS.ENTER }); + + expect(renderScene).toHaveBeenCalledTimes(4); + expect(renderScene.mock.calls[3][1]).toBeNull(); + const elements = renderScene.mock.calls[3][0]; + + expect(elements.length).toEqual(0); + }); +}); diff --git a/src/tests/move.test.tsx b/src/tests/move.test.tsx new file mode 100644 index 00000000..df8b5252 --- /dev/null +++ b/src/tests/move.test.tsx @@ -0,0 +1,88 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import { render, fireEvent } from "./test-utils"; +import { App } from "../index"; +import * as Renderer from "../renderer/renderScene"; + +// Unmount ReactDOM from root +ReactDOM.unmountComponentAtNode(document.getElementById("root")!); + +const renderScene = jest.spyOn(Renderer, "renderScene"); +beforeEach(() => { + localStorage.clear(); + renderScene.mockClear(); +}); + +describe("move element", () => { + it("rectangle", () => { + const { getByToolName, container } = render(); + const canvas = container.querySelector("canvas")!; + + { + // create element + const tool = getByToolName("rectangle"); + fireEvent.click(tool); + fireEvent.mouseDown(canvas, { clientX: 30, clientY: 20 }); + fireEvent.mouseMove(canvas, { clientX: 60, clientY: 70 }); + fireEvent.mouseUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(4); + const elements = renderScene.mock.calls[3][0]; + const selectionElement = renderScene.mock.calls[3][1]; + expect(selectionElement).toBeNull(); + expect(elements.length).toEqual(1); + expect(elements[0].isSelected).toBeTruthy(); + expect([elements[0].x, elements[0].y]).toEqual([30, 20]); + + renderScene.mockClear(); + } + + fireEvent.mouseDown(canvas, { clientX: 50, clientY: 20 }); + fireEvent.mouseMove(canvas, { clientX: 20, clientY: 40 }); + fireEvent.mouseUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(3); + const elements = renderScene.mock.calls[2][0]; + expect(renderScene.mock.calls[2][1]).toBeNull(); + expect(elements.length).toEqual(1); + expect([elements[0].x, elements[0].y]).toEqual([0, 40]); + }); +}); + +describe("duplicate element on move when ALT is clicked", () => { + it("rectangle", () => { + const { getByToolName, container } = render(); + const canvas = container.querySelector("canvas")!; + + { + // create element + const tool = getByToolName("rectangle"); + fireEvent.click(tool); + fireEvent.mouseDown(canvas, { clientX: 30, clientY: 20 }); + fireEvent.mouseMove(canvas, { clientX: 60, clientY: 70 }); + fireEvent.mouseUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(4); + const elements = renderScene.mock.calls[3][0]; + const selectionElement = renderScene.mock.calls[3][1]; + expect(selectionElement).toBeNull(); + expect(elements.length).toEqual(1); + expect(elements[0].isSelected).toBeTruthy(); + expect([elements[0].x, elements[0].y]).toEqual([30, 20]); + + renderScene.mockClear(); + } + + fireEvent.mouseDown(canvas, { clientX: 50, clientY: 20, altKey: true }); + fireEvent.mouseMove(canvas, { clientX: 20, clientY: 40 }); + fireEvent.mouseUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(3); + const elements = renderScene.mock.calls[2][0]; + expect(renderScene.mock.calls[2][1]).toBeNull(); + expect(elements.length).toEqual(2); + // previous element should stay intact + expect([elements[0].x, elements[0].y]).toEqual([30, 20]); + expect([elements[1].x, elements[1].y]).toEqual([0, 40]); + }); +}); diff --git a/src/tests/multiPointCreate.test.tsx b/src/tests/multiPointCreate.test.tsx new file mode 100644 index 00000000..e9db36dc --- /dev/null +++ b/src/tests/multiPointCreate.test.tsx @@ -0,0 +1,140 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import { render, fireEvent } from "./test-utils"; +import { App } from "../index"; +import * as Renderer from "../renderer/renderScene"; +import { KEYS } from "../keys"; + +// Unmount ReactDOM from root +ReactDOM.unmountComponentAtNode(document.getElementById("root")!); + +const renderScene = jest.spyOn(Renderer, "renderScene"); +beforeEach(() => { + localStorage.clear(); + renderScene.mockClear(); +}); + +describe("remove shape in non linear elements", () => { + it("rectangle", () => { + const { getByToolName, container } = render(); + // select tool + const tool = getByToolName("rectangle"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas")!; + fireEvent.mouseDown(canvas, { clientX: 30, clientY: 20 }); + fireEvent.mouseUp(canvas, { clientX: 30, clientY: 30 }); + + expect(renderScene).toHaveBeenCalledTimes(3); + const elements = renderScene.mock.calls[2][0]; + expect(elements.length).toEqual(0); + }); + + it("ellipse", () => { + const { getByToolName, container } = render(); + // select tool + const tool = getByToolName("ellipse"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas")!; + fireEvent.mouseDown(canvas, { clientX: 30, clientY: 20 }); + fireEvent.mouseUp(canvas, { clientX: 30, clientY: 30 }); + + expect(renderScene).toHaveBeenCalledTimes(3); + const elements = renderScene.mock.calls[2][0]; + expect(elements.length).toEqual(0); + }); + + it("diamond", () => { + const { getByToolName, container } = render(); + // select tool + const tool = getByToolName("diamond"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas")!; + fireEvent.mouseDown(canvas, { clientX: 30, clientY: 20 }); + fireEvent.mouseUp(canvas, { clientX: 30, clientY: 30 }); + + expect(renderScene).toHaveBeenCalledTimes(3); + const elements = renderScene.mock.calls[2][0]; + expect(elements.length).toEqual(0); + }); +}); + +describe("multi point mode in linear elements", () => { + it("arrow", () => { + const { getByToolName, container } = render(); + // select tool + const tool = getByToolName("arrow"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas")!; + // first point is added on mouse down + fireEvent.mouseDown(canvas, { clientX: 30, clientY: 30 }); + + // second point, enable multi point + fireEvent.mouseUp(canvas, { clientX: 30, clientY: 30 }); + fireEvent.mouseMove(canvas, { clientX: 50, clientY: 60 }); + + // third point + fireEvent.mouseDown(canvas, { clientX: 50, clientY: 60 }); + fireEvent.mouseUp(canvas); + fireEvent.mouseMove(canvas, { clientX: 100, clientY: 140 }); + + // done + fireEvent.mouseDown(canvas); + fireEvent.mouseUp(canvas); + fireEvent.keyDown(document, { key: KEYS.ENTER }); + + expect(renderScene).toHaveBeenCalledTimes(8); + const elements = renderScene.mock.calls[7][0]; + expect(elements.length).toEqual(1); + + expect(elements[0].type).toEqual("arrow"); + expect(elements[0].x).toEqual(30); + expect(elements[0].y).toEqual(30); + expect(elements[0].points).toEqual([ + [0, 0], + [20, 30], + [70, 110], + ]); + }); + + it("line", () => { + const { getByToolName, container } = render(); + // select tool + const tool = getByToolName("line"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas")!; + // first point is added on mouse down + fireEvent.mouseDown(canvas, { clientX: 30, clientY: 30 }); + + // second point, enable multi point + fireEvent.mouseUp(canvas, { clientX: 30, clientY: 30 }); + fireEvent.mouseMove(canvas, { clientX: 50, clientY: 60 }); + + // third point + fireEvent.mouseDown(canvas, { clientX: 50, clientY: 60 }); + fireEvent.mouseUp(canvas); + fireEvent.mouseMove(canvas, { clientX: 100, clientY: 140 }); + + // done + fireEvent.mouseDown(canvas); + fireEvent.mouseUp(canvas); + fireEvent.keyDown(document, { key: KEYS.ENTER }); + + expect(renderScene).toHaveBeenCalledTimes(8); + const elements = renderScene.mock.calls[7][0]; + expect(elements.length).toEqual(1); + + expect(elements[0].type).toEqual("line"); + expect(elements[0].x).toEqual(30); + expect(elements[0].y).toEqual(30); + expect(elements[0].points).toEqual([ + [0, 0], + [20, 30], + [70, 110], + ]); + }); +}); diff --git a/src/tests/queries/toolQueries.ts b/src/tests/queries/toolQueries.ts new file mode 100644 index 00000000..92c174ce --- /dev/null +++ b/src/tests/queries/toolQueries.ts @@ -0,0 +1,32 @@ +import { queries, buildQueries } from "@testing-library/react"; + +const _getAllByToolName = (container: HTMLElement, tool: string) => { + const toolMap: { [propKey: string]: string } = { + selection: "Selection — S, 1", + rectangle: "Rectangle — R, 2", + diamond: "Diamond — D, 3", + ellipse: "Ellipse — E, 4", + arrow: "Arrow — A, 5", + line: "Line — L, 6", + }; + + const toolTitle = toolMap[tool as string]; + return queries.getAllByTitle(container, toolTitle); +}; + +const getMultipleError = (c: any, tool: any) => + `Found multiple elements with tool name: ${tool}`; +const getMissingError = (c: any, tool: any) => + `Unable to find an element with tool name: ${tool}`; + +export const [ + queryByToolName, + getAllByToolName, + getByToolName, + findAllByToolName, + findByToolName, +] = buildQueries( + _getAllByToolName, + getMultipleError, + getMissingError, +); diff --git a/src/tests/resize.test.tsx b/src/tests/resize.test.tsx new file mode 100644 index 00000000..41355929 --- /dev/null +++ b/src/tests/resize.test.tsx @@ -0,0 +1,98 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import { render, fireEvent } from "./test-utils"; +import { App } from "../index"; +import * as Renderer from "../renderer/renderScene"; + +// Unmount ReactDOM from root +ReactDOM.unmountComponentAtNode(document.getElementById("root")!); + +const renderScene = jest.spyOn(Renderer, "renderScene"); +beforeEach(() => { + localStorage.clear(); + renderScene.mockClear(); +}); + +describe("resize element", () => { + it("rectangle", () => { + const { getByToolName, container } = render(); + const canvas = container.querySelector("canvas")!; + + { + // create element + const tool = getByToolName("rectangle"); + fireEvent.click(tool); + fireEvent.mouseDown(canvas, { clientX: 30, clientY: 20 }); + fireEvent.mouseMove(canvas, { clientX: 60, clientY: 70 }); + fireEvent.mouseUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(4); + const elements = renderScene.mock.calls[3][0]; + const selectionElement = renderScene.mock.calls[3][1]; + expect(selectionElement).toBeNull(); + expect(elements.length).toEqual(1); + expect(elements[0].isSelected).toBeTruthy(); + expect([elements[0].x, elements[0].y]).toEqual([30, 20]); + + renderScene.mockClear(); + } + + // select the element first + fireEvent.mouseDown(canvas, { clientX: 50, clientY: 20 }); + fireEvent.mouseUp(canvas); + + // select a handler rectangle (top-left) + fireEvent.mouseDown(canvas, { clientX: 21, clientY: 13 }); + fireEvent.mouseMove(canvas, { clientX: 20, clientY: 40 }); + fireEvent.mouseUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(5); + const elements = renderScene.mock.calls[4][0]; + expect(renderScene.mock.calls[4][1]).toBeNull(); + expect(elements.length).toEqual(1); + expect([elements[0].x, elements[0].y]).toEqual([29, 47]); + expect([elements[0].width, elements[0].height]).toEqual([31, 23]); + }); +}); + +describe("resize element with aspect ratio when SHIFT is clicked", () => { + it("rectangle", () => { + const { getByToolName, container } = render(); + const canvas = container.querySelector("canvas")!; + + { + // create element + const tool = getByToolName("rectangle"); + fireEvent.click(tool); + fireEvent.mouseDown(canvas, { clientX: 30, clientY: 20 }); + fireEvent.mouseMove(canvas, { clientX: 60, clientY: 70 }); + fireEvent.mouseUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(4); + const elements = renderScene.mock.calls[3][0]; + const selectionElement = renderScene.mock.calls[3][1]; + expect(selectionElement).toBeNull(); + expect(elements.length).toEqual(1); + expect(elements[0].isSelected).toBeTruthy(); + expect([elements[0].x, elements[0].y]).toEqual([30, 20]); + + renderScene.mockClear(); + } + + // select the element first + fireEvent.mouseDown(canvas, { clientX: 50, clientY: 20 }); + fireEvent.mouseUp(canvas); + + // select a handler rectangle (top-left) + fireEvent.mouseDown(canvas, { clientX: 21, clientY: 13 }); + fireEvent.mouseMove(canvas, { clientX: 20, clientY: 40, shiftKey: true }); + fireEvent.mouseUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(5); + const elements = renderScene.mock.calls[4][0]; + expect(renderScene.mock.calls[4][1]).toBeNull(); + expect(elements.length).toEqual(1); + expect([elements[0].x, elements[0].y]).toEqual([29, 39]); + expect([elements[0].width, elements[0].height]).toEqual([31, 31]); + }); +}); diff --git a/src/tests/selection.test.tsx b/src/tests/selection.test.tsx new file mode 100644 index 00000000..b8838851 --- /dev/null +++ b/src/tests/selection.test.tsx @@ -0,0 +1,211 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import { render, fireEvent } from "./test-utils"; +import { App } from "../index"; +import * as Renderer from "../renderer/renderScene"; +import { KEYS } from "../keys"; + +// Unmount ReactDOM from root +ReactDOM.unmountComponentAtNode(document.getElementById("root")!); + +const renderScene = jest.spyOn(Renderer, "renderScene"); +beforeEach(() => { + localStorage.clear(); + renderScene.mockClear(); +}); + +describe("selection element", () => { + it("create selection element on mouse down", () => { + const { getByToolName, container } = render(); + // select tool + const tool = getByToolName("selection"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas")!; + fireEvent.mouseDown(canvas, { clientX: 60, clientY: 100 }); + + expect(renderScene).toHaveBeenCalledTimes(1); + const selectionElement = renderScene.mock.calls[0][1]!; + 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 mouse up is not triggered + fireEvent.mouseUp(canvas); + }); + + it("resize selection element on mouse move", () => { + const { getByToolName, container } = render(); + // select tool + const tool = getByToolName("selection"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas")!; + fireEvent.mouseDown(canvas, { clientX: 60, clientY: 100 }); + fireEvent.mouseMove(canvas, { clientX: 150, clientY: 30 }); + + expect(renderScene).toHaveBeenCalledTimes(2); + const selectionElement = renderScene.mock.calls[1][1]!; + 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 mouse up is not triggered + fireEvent.mouseUp(canvas); + }); + + it("remove selection element on mouse up", () => { + const { getByToolName, container } = render(); + // select tool + const tool = getByToolName("selection"); + fireEvent.click(tool); + + const canvas = container.querySelector("canvas")!; + fireEvent.mouseDown(canvas, { clientX: 60, clientY: 100 }); + fireEvent.mouseMove(canvas, { clientX: 150, clientY: 30 }); + fireEvent.mouseUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(3); + const selectionElement = renderScene.mock.calls[2][1]; + expect(selectionElement).toBeNull(); + }); +}); + +describe("select single element on the scene", () => { + it("rectangle", () => { + const { getByToolName, container } = render(); + const canvas = container.querySelector("canvas")!; + { + // create element + const tool = getByToolName("rectangle"); + fireEvent.click(tool); + fireEvent.mouseDown(canvas, { clientX: 30, clientY: 20 }); + fireEvent.mouseMove(canvas, { clientX: 60, clientY: 70 }); + fireEvent.mouseUp(canvas); + fireEvent.keyDown(document, { key: KEYS.ESCAPE }); + } + + const tool = getByToolName("selection"); + fireEvent.click(tool); + // click on a line on the rectangle + fireEvent.mouseDown(canvas, { clientX: 45, clientY: 20 }); + fireEvent.mouseUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(7); + const elements = renderScene.mock.calls[6][0]; + const selectionElement = renderScene.mock.calls[6][1]; + expect(selectionElement).toBeNull(); + expect(elements.length).toEqual(1); + expect(elements[0].isSelected).toBeTruthy(); + }); + + it("diamond", () => { + const { getByToolName, container } = render(); + const canvas = container.querySelector("canvas")!; + { + // create element + const tool = getByToolName("diamond"); + fireEvent.click(tool); + fireEvent.mouseDown(canvas, { clientX: 30, clientY: 20 }); + fireEvent.mouseMove(canvas, { clientX: 60, clientY: 70 }); + fireEvent.mouseUp(canvas); + fireEvent.keyDown(document, { key: KEYS.ESCAPE }); + } + + const tool = getByToolName("selection"); + fireEvent.click(tool); + // click on a line on the rectangle + fireEvent.mouseDown(canvas, { clientX: 45, clientY: 20 }); + fireEvent.mouseUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(7); + const elements = renderScene.mock.calls[6][0]; + const selectionElement = renderScene.mock.calls[6][1]; + expect(selectionElement).toBeNull(); + expect(elements.length).toEqual(1); + expect(elements[0].isSelected).toBeTruthy(); + }); + + it("ellipse", () => { + const { getByToolName, container } = render(); + const canvas = container.querySelector("canvas")!; + { + // create element + const tool = getByToolName("ellipse"); + fireEvent.click(tool); + fireEvent.mouseDown(canvas, { clientX: 30, clientY: 20 }); + fireEvent.mouseMove(canvas, { clientX: 60, clientY: 70 }); + fireEvent.mouseUp(canvas); + fireEvent.keyDown(document, { key: KEYS.ESCAPE }); + } + + const tool = getByToolName("selection"); + fireEvent.click(tool); + // click on a line on the rectangle + fireEvent.mouseDown(canvas, { clientX: 45, clientY: 20 }); + fireEvent.mouseUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(7); + const elements = renderScene.mock.calls[6][0]; + const selectionElement = renderScene.mock.calls[6][1]; + expect(selectionElement).toBeNull(); + expect(elements.length).toEqual(1); + expect(elements[0].isSelected).toBeTruthy(); + }); + + it("arrow", () => { + const { getByToolName, container } = render(); + const canvas = container.querySelector("canvas")!; + { + // create element + const tool = getByToolName("arrow"); + fireEvent.click(tool); + fireEvent.mouseDown(canvas, { clientX: 30, clientY: 20 }); + fireEvent.mouseMove(canvas, { clientX: 60, clientY: 70 }); + fireEvent.mouseUp(canvas); + fireEvent.keyDown(document, { key: KEYS.ESCAPE }); + } + + const tool = getByToolName("selection"); + fireEvent.click(tool); + // click on a line on the rectangle + fireEvent.mouseDown(canvas, { clientX: 45, clientY: 20 }); + fireEvent.mouseUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(7); + const elements = renderScene.mock.calls[6][0]; + const selectionElement = renderScene.mock.calls[6][1]; + expect(selectionElement).toBeNull(); + expect(elements.length).toEqual(1); + expect(elements[0].isSelected).toBeTruthy(); + }); + + it("arrow", () => { + const { getByToolName, container } = render(); + const canvas = container.querySelector("canvas")!; + { + // create element + const tool = getByToolName("line"); + fireEvent.click(tool); + fireEvent.mouseDown(canvas, { clientX: 30, clientY: 20 }); + fireEvent.mouseMove(canvas, { clientX: 60, clientY: 70 }); + fireEvent.mouseUp(canvas); + fireEvent.keyDown(document, { key: KEYS.ESCAPE }); + } + + const tool = getByToolName("selection"); + fireEvent.click(tool); + // click on a line on the rectangle + fireEvent.mouseDown(canvas, { clientX: 45, clientY: 20 }); + fireEvent.mouseUp(canvas); + + expect(renderScene).toHaveBeenCalledTimes(7); + const elements = renderScene.mock.calls[6][0]; + const selectionElement = renderScene.mock.calls[6][1]; + expect(selectionElement).toBeNull(); + expect(elements.length).toEqual(1); + expect(elements[0].isSelected).toBeTruthy(); + }); +}); diff --git a/src/tests/test-utils.ts b/src/tests/test-utils.ts new file mode 100644 index 00000000..476ea1bd --- /dev/null +++ b/src/tests/test-utils.ts @@ -0,0 +1,30 @@ +import { + render, + queries, + RenderResult, + RenderOptions, +} from "@testing-library/react"; + +import * as toolQueries from "./queries/toolQueries"; + +const customQueries = { + ...queries, + ...toolQueries, +}; + +type TestRenderFn = ( + ui: React.ReactElement, + options?: Omit, +) => RenderResult; + +const renderApp: TestRenderFn = (ui, options) => + render(ui, { + queries: customQueries, + ...options, + }); + +// re-export everything +export * from "@testing-library/react"; + +// override render method +export { renderApp as render };