diff --git a/.eslintignore b/.eslintignore index ff6eb7f9..d73e77e4 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,4 @@ build/ dist/ -vite.config.js -public \ No newline at end of file +public/ +vite.config.js \ No newline at end of file diff --git a/.github/workflows/runTests.yml b/.github/workflows/runTests.yml new file mode 100644 index 00000000..7a0191e6 --- /dev/null +++ b/.github/workflows/runTests.yml @@ -0,0 +1,18 @@ +name: Run all tests + +on: + pull_request: + branches: [ master ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Install NPM dependencies + run: npm install + + - name: Run all vitest tests + run: npm run test diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..3598c300 --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +tests \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 8d7ffd13..f6374188 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,6 +13,7 @@ "styleguidelines", "Subrina", "Tiwari", + "vite", "xshift", "yshift" ], diff --git a/README.md b/README.md index e364591c..77176294 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Sponsored by James Oswald (RAIR Lab) ## Development -**All listed commands should be run in the project root** +**All listed commands should be run in the project root (Peirce-My-Heart)** ### Documentation @@ -35,6 +35,7 @@ We also use it to invoke all core development tools via `npm run`. 2. [**Vite**](https://vitejs.dev/) : Vite is an asset bundler and build system for large web projects with many types of resources such as typescript. Additionally, it provides excellent debugging features. We use it as a build system for Typescript, Asset Compressor, and Live Debugger. + * **Vitest** : a testing framework native to Vite with support for TypeScript. It allows for github workflow compatibility and running numerous tests with one command. 3. [**gts**](https://github.com/google/gts) : gts (Google TypeScript Style) is a set of style guidelines and tools for typescript consistent and readable formatting. It provides defaults for the following tools we use: @@ -97,18 +98,31 @@ open attached to the Vite server, any vs code breakpoints triggered in chrome will be jumped to in VSC. +## Testing + +Test your changes! Vitest will help here by looking for all .test.ts files in /tests/ and running them. +To perform this locally in a terminal, run the following: +```bash +npm run test +``` + + ### Root Files and Folders Overview ``` /.github/ : The code for github workflows this project uses, used for automatically deploying. /src/ : source code for the application +/tests/ : vitest compatible .test.ts files that are run on commits and must be passed before all merges to master + /.eslintignore : list of .js and .ts files the linter shouldn't look at /.eslintrc.json : config for the linter, which catches semantic errors in your typescript code. /.gitignore : list of files and folders shouldn't be uploaded with git version control +/.npmignore : list of the files NPM shouldn't peep + /.prettierrc.js : config for prettier, which catches syntactic errors in your typescript code. /LICENSE : The legal jargon for who can use and sell the project. diff --git a/package-lock.json b/package-lock.json index bddc1d7e..6e5e19d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,8 @@ "gts": "^5.2.0", "typedoc": "^0.25.2", "typescript": "~5.2.2", - "vite": "^4.4.11" + "vite": "^4.4.11", + "vitest": "^0.34.6" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -647,6 +648,18 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", @@ -701,9 +714,7 @@ "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.19", @@ -772,6 +783,27 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@types/chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==", + "dev": true + }, + "node_modules/@types/chai-subset": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", + "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, "node_modules/@types/eslint": { "version": "8.44.2", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz", @@ -1010,6 +1042,101 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@vitest/expect": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", + "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", + "dev": true, + "dependencies": { + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", + "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", + "dev": true, + "dependencies": { + "@vitest/utils": "0.34.6", + "p-limit": "^4.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/runner/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/snapshot": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", + "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "pretty-format": "^29.5.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", + "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", + "dev": true, + "dependencies": { + "tinyspy": "^2.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", + "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", + "dev": true, + "dependencies": { + "diff-sequences": "^29.4.3", + "loupe": "^2.3.6", + "pretty-format": "^29.5.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -1031,6 +1158,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1128,6 +1264,15 @@ "node": ">=0.10.0" } }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1200,6 +1345,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1235,6 +1389,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/chai": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1269,6 +1441,18 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -1387,6 +1571,18 @@ "node": ">=0.10.0" } }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1549,6 +1745,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2197,6 +2402,15 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -2719,6 +2933,18 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -2743,6 +2969,15 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/loupe": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.0" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -2761,6 +2996,18 @@ "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", "dev": true }, + "node_modules/magic-string": { + "version": "0.30.4", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.4.tgz", + "integrity": "sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/map-obj": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", @@ -2895,6 +3142,18 @@ "node": ">= 6" } }, + "node_modules/mlly": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", + "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==", + "dev": true, + "dependencies": { + "acorn": "^8.10.0", + "pathe": "^1.1.1", + "pkg-types": "^1.0.3", + "ufo": "^1.3.0" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -3154,6 +3413,21 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", + "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", + "dev": true + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -3172,10 +3446,21 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", + "dev": true, + "dependencies": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.2.0", + "pathe": "^1.1.0" + } + }, "node_modules/postcss": { - "version": "8.4.29", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz", - "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "funding": [ { @@ -3236,6 +3521,32 @@ "node": ">=6.0.0" } }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -3274,6 +3585,12 @@ "node": ">=8" } }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -3566,6 +3883,12 @@ "vscode-textmate": "^8.0.0" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -3645,6 +3968,18 @@ "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", "dev": true }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.4.3.tgz", + "integrity": "sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==", + "dev": true + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -3704,6 +4039,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", + "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", + "dev": true, + "dependencies": { + "acorn": "^8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -3764,6 +4111,30 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, + "node_modules/tinybench": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.1.tgz", + "integrity": "sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==", + "dev": true + }, + "node_modules/tinypool": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", + "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", + "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/titleize": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", @@ -3848,6 +4219,15 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -3918,6 +4298,12 @@ "node": ">=14.17" } }, + "node_modules/ufo": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.1.tgz", + "integrity": "sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==", + "dev": true + }, "node_modules/untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", @@ -4001,6 +4387,106 @@ } } }, + "node_modules/vite-node": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", + "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "mlly": "^1.4.0", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", + "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", + "dev": true, + "dependencies": { + "@types/chai": "^4.3.5", + "@types/chai-subset": "^1.3.3", + "@types/node": "*", + "@vitest/expect": "0.34.6", + "@vitest/runner": "0.34.6", + "@vitest/snapshot": "0.34.6", + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "acorn": "^8.9.0", + "acorn-walk": "^8.2.0", + "cac": "^6.7.14", + "chai": "^4.3.10", + "debug": "^4.3.4", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.3.3", + "strip-literal": "^1.0.1", + "tinybench": "^2.5.0", + "tinypool": "^0.7.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", + "vite-node": "0.34.6", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@vitest/browser": "*", + "@vitest/ui": "*", + "happy-dom": "*", + "jsdom": "*", + "playwright": "*", + "safaridriver": "*", + "webdriverio": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "playwright": { + "optional": true + }, + "safaridriver": { + "optional": true + }, + "webdriverio": { + "optional": true + } + } + }, "node_modules/vscode-oniguruma": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", @@ -4028,6 +4514,22 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", + "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 2d99fe75..830bc730 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ "preview": "vite preview --open", "live-edit": "vite serve --open", "build-docs": "typedoc", - "vsc-debug": "vite serve" + "vsc-debug": "vite serve", + "live-test": "vitest", + "test" : "vitest run" }, "repository": { "type": "git", @@ -33,7 +35,8 @@ "gts": "^5.2.0", "typedoc": "^0.25.2", "typescript": "~5.2.2", - "vite": "^4.4.11" + "vite": "^4.4.11", + "vitest": "^0.34.6" }, "dependencies": { "nomial": "^1.0.11" diff --git a/src/AEG/AEGTree.ts b/src/AEG/AEGTree.ts index 9fef23a4..886cc403 100644 --- a/src/AEG/AEGTree.ts +++ b/src/AEG/AEGTree.ts @@ -4,6 +4,11 @@ import {Point} from "./Point"; import {Ellipse} from "./Ellipse"; import {shapesOverlap} from "./AEGUtils"; +/** + * Represents the background AEG tree structure. + * @author Ryan Reilly + * @author Anusha Tiwari + */ export class AEGTree { sheet: CutNode; @@ -55,7 +60,7 @@ export class AEGTree { } /** - * Method that checks whether the given node can be inserted into this tree + * Checks whether the given node can be inserted into this tree * at a given point without overlapping any bounding boxes. * @param incomingNode The node to be inserted. * @returns True, if the node can be inserted. Else, false diff --git a/src/AEG/AtomNode.ts b/src/AEG/AtomNode.ts index efac190e..50ef6119 100644 --- a/src/AEG/AtomNode.ts +++ b/src/AEG/AtomNode.ts @@ -4,7 +4,7 @@ import {Ellipse} from "./Ellipse"; import {Point} from "./Point"; /** - * Class that defines an Atom. + * Defines an Atom. * @author Anusha Tiwari * @author Ryan Reilly */ diff --git a/src/AEG/CutNode.ts b/src/AEG/CutNode.ts index 4f759ede..60223ffe 100644 --- a/src/AEG/CutNode.ts +++ b/src/AEG/CutNode.ts @@ -174,6 +174,14 @@ export class CutNode { return str; } + /** + * Constructs a string representation of an AEGTree or a subtree. + * () - cut + * char - atom + * (char char ()) - valid nesting of two chars and a cut inside another cut + * @returns an accurate string representation of the AEGTree or a subtree + * @author James Oswald + */ public toFormulaString(): string { let formulaString = ""; for (const child of this.internalChildren) { diff --git a/src/AEG/Ellipse.ts b/src/AEG/Ellipse.ts index b3a4bfe4..b580d056 100644 --- a/src/AEG/Ellipse.ts +++ b/src/AEG/Ellipse.ts @@ -34,12 +34,21 @@ export class Ellipse { * @param center The center point of the ellipse. * @param radX The horizontal radius of the ellipse. * @param radY The vertical radius of the ellipse. + * @throws Errors on NaN, Infinity, and negative radii lengths. */ public constructor(center?: Point, radX?: number, radY?: number) { this.center = center ?? new Point(); this.radiusX = radX ?? 0; this.radiusY = radY ?? 0; + if (!Number.isFinite(this.radiusX) || !Number.isFinite(this.radiusY)) { + throw new Error("A radius passed into an Ellipse construction was NaN or Infinity."); + } else if (this.radiusX !== undefined && this.radiusX < 0) { + throw new Error("Horizontal radius in an Ellipse construction was negative."); + } else if (this.radiusY !== undefined && this.radiusY < 0) { + throw new Error("Vertical radius in an Ellipse construction was negative."); + } + const boundingVertex: Point = new Point( this.center.x - this.radiusX, this.center.y - this.radiusY @@ -47,26 +56,6 @@ export class Ellipse { this.boundingBox = new Rectangle(boundingVertex, this.radiusX * 2, this.radiusY * 2); } - /** - * Method that returns the string representation of an ellipse. - * @returns The coordinates and radii for the ellipse. - */ - public toString(): string { - return ( - "An ellipse with Center at: " + - this.center.toString() + - ", \n" + - "Horizontal Radius of: " + - this.radiusX + - ", \n" + - "Vertical Radius of: " + - this.radiusY + - ", \n" + - "Bounding box: " + - this.boundingBox.toString() - ); - } - /** * Method that checks whether a point is within the given ellipse. * @param otherPoint The point that might be inside this ellipse. @@ -93,4 +82,21 @@ export class Ellipse { public contains(otherShape: Rectangle | Ellipse): boolean { return shapeContains(this, otherShape); } + + /** + * Method that returns the string representation of an ellipse. + * @returns The coordinates and radii for the ellipse. + */ + public toString(): string { + return ( + "An ellipse with Center at: " + + this.center.toString() + + ", Horizontal radius: " + + this.radiusX + + ", Vertical radius: " + + this.radiusY + + ", Bounding box: " + + this.boundingBox.toString() + ); + } } diff --git a/src/AEG/Point.ts b/src/AEG/Point.ts index 3aafc6b5..461845ce 100644 --- a/src/AEG/Point.ts +++ b/src/AEG/Point.ts @@ -1,5 +1,5 @@ /** - * Class that defines a Point. + * Defines a Point. * @author Anusha Tiwari * @author Ryan Reilly */ @@ -15,17 +15,32 @@ export class Point { y: number; /** - * Construct a new point at the given coordinates. - * If no coordinates specified, default them to 0. + * Constructs a new point at the given coordinates. + * If no coordinates are specified, default them to 0. * @param coordX The specified x coordinate. * @param coordY The specified y coordinate. + * @throws Error on receiving NaN or Infinity values as coordinates. */ public constructor(coordX?: number, coordY?: number) { this.x = coordX ?? 0; this.y = coordY ?? 0; + if (!Number.isFinite(this.x) || !Number.isFinite(this.y)) { + throw new Error("NaN or Infinity value(s) were passed in constructing a Point."); + } } + /** + * Sets this Point's coordinates according to the incoming numbers. + * @param coordX the incoming value for x + * @param coordY the incoming value for y + * @throws Error on receiving NaN or Infinity values as coordinates. + */ public set(coordX: number, coordY: number) { + if (!Number.isFinite(coordX) || !Number.isFinite(coordY)) { + throw new Error( + "NaN or Infinity value(s) were passed in setting " + this + "'s coords." + ); + } this.x = coordX; this.y = coordY; } @@ -42,10 +57,10 @@ export class Point { } /** - * Method that returns a string representation of the point. - * @returns The coordinates of the point. + * Returns a string representation of the point. + * @returns The coordinates of the point in string form. */ public toString(): string { - return "X: " + this.x + ", Y: " + this.y; + return "(" + this.x + ", " + this.y + ")"; } } diff --git a/src/AEG/Rectangle.ts b/src/AEG/Rectangle.ts index aa04d47a..23e6820d 100644 --- a/src/AEG/Rectangle.ts +++ b/src/AEG/Rectangle.ts @@ -3,71 +3,76 @@ import {Point} from "./Point"; import {shapesOverlap, shapeContains, pointInRect} from "./AEGUtils"; /** - * Class that defines a Rectangle. + * Defines a rectangle. * @author Anusha Tiwari * @author Ryan Reilly */ export class Rectangle { /** - * The starting (top left) vertex of the rectangle. + * The starting (top left) vertex of this Rectangle. */ startVertex: Point; /** - * The width of the rectangle. + * The width of this Rectangle. */ width: number; /** - * The height of the rectangle. + * The height of this Rectangle. */ height: number; /** - * Construct a rectangle using the given points and lengths. - * If no values specified, default them to 0. + * Constructs a rectangle using the given points and lengths. + * If no Point specified, default startVertex to (0, 0). * @param vertex The starting point of the rectangle. * @param w The width of the rectangle. * @param h The height of the rectangle. */ public constructor(vertex?: Point, w?: number, h?: number) { - this.startVertex = vertex ?? new Point(); + if (!Number.isFinite(w) || !Number.isFinite(h)) { + throw new Error( + "Infinity/NaN passed in for width/height while constructing a Rectangle." + ); + } else if (w !== undefined && w < 0) { + throw new Error("Negative value passed for width while constructing a Rectangle."); + } else if (h !== undefined && h < 0) { + throw new Error("Negative value passed for height while constructing a Rectangle."); + } + this.startVertex = vertex ?? new Point(0, 0); this.width = w ?? 0; this.height = h ?? 0; } /** - * The corners of the rectangle in clockwise order, starting from the top left. - * 0 = Top left vertex. - * 1 = Top Right vertex. - * 2 = Bottom Right vertex. - * 3 = Bottom Left vertex. - * @returns The bounding box of the rectangle. + * Creates a Point array of + * the corners of the rectangle in clockwise order, starting from the top left. + * vertices[0] = Top left vertex. + * vertices[1] = Top Right vertex. + * vertices[2] = Bottom Right vertex. + * vertices[3] = Bottom Left vertex. + * @returns The bounding box of the rectangle in Point array form. */ public getCorners(): Point[] { - //0 = top left vertex const vertices: Point[] = [this.startVertex]; - //1 = top right vertex vertices.push(new Point(this.startVertex.x + this.width, this.startVertex.y)); - //2 = bottom right vertex vertices.push(new Point(this.startVertex.x + this.width, this.startVertex.y + this.height)); - //3 = bottom left vertex vertices.push(new Point(this.startVertex.x, this.startVertex.y + this.height)); return vertices; } /** - * Method that checks whether there is a point inside the given rectangle. - * @param otherPoint The point that might be inside this rectangle. - * @returns True, if the point is completely inside the rectangle. Else, false. + * Checks whether the incoming Point is inside this Rectangle. + * @param otherPoint Point that may be inside this Rectangle. */ public containsPoint(point: Point): boolean { return pointInRect(this, point); } /** - * Method that checks whether there is an overlap between this rectangle and another shape. + * Checks whether there is an overlap between this rectangle and another shape. * @param otherShape The other shape that might be overlapping this rectangle. * @returns True, if there is an overlap. Else, false. */ @@ -76,27 +81,25 @@ export class Rectangle { } /** - * Method that checks whether another shape is within this rectangle. - * @param otherShape The shape that might be within this rectangle. - * @returns True, if the shape is within this rectangle. Else, false. + * Checks whether another shape is contained within this Rectangle. + * @param otherShape The shape that may be within this Rectangle. + * @returns True, if the shape is within this Rectangle. */ public contains(otherShape: Rectangle | Ellipse): boolean { return shapeContains(this, otherShape); } /** - * Method that returns a string representation of the rectangle. - * @returns The coordinates and lengths for the rectangle. + * Returns a string representation of this Rectangle. + * @returns This Rectangle in string form. */ public toString(): string { return ( - "A rectangle with\nTop Left Vertex at: " + + "Rectangle with top left vertex at: " + this.startVertex.toString() + - ", " + - "Width: " + + ", w: " + this.width + - ", " + - "Height: " + + ", h: " + this.height ); } diff --git a/src/EllipseCreation.ts b/src/EllipseCreation.ts index b648846c..495a69bf 100644 --- a/src/EllipseCreation.ts +++ b/src/EllipseCreation.ts @@ -20,12 +20,6 @@ const ctx: CanvasRenderingContext2D = res; let startingPoint: Point; let currentEllipse: Ellipse; -function distance(p1: Point, p2: Point): number { - const dx = p1.x - p2.x; - const dy = p1.y - p2.y; - return Math.sqrt(dx * dx + dy * dy); -} - /** * A function to draw an ellipse between two points designated by the user. * @param original the point where the user originally clicked @@ -46,7 +40,7 @@ export function createEllipse(original: Point, current: Point): Ellipse { if (modeElm.value === "circumscribed") { //This inscribed ellipse solution is inspired by the discussion of radius ratios in //https://stackoverflow.com/a/433426/6342516 - const rv: number = Math.floor(distance(center, current)); + const rv: number = Math.floor(center.distance(current)); ry = Math.floor(rv * (dy / dx)); rx = Math.floor(rv * (dx / dy)); } else { diff --git a/tests/Ellipse.test.ts b/tests/Ellipse.test.ts new file mode 100644 index 00000000..889d3f05 --- /dev/null +++ b/tests/Ellipse.test.ts @@ -0,0 +1,147 @@ +import {describe, expect, test} from "vitest"; + +import {Ellipse} from "../src/AEG/Ellipse"; +import {Rectangle} from "../src/AEG/Rectangle"; +import {Point} from "../src/AEG/Point"; + +describe("Ellipse constructor soliloquy:", () => { + const ell: Ellipse = new Ellipse(); + const center: Point = new Point(10, 10); + + test("Default Ellipse constructor should produce an Ellipse of center (0, 0), radX = 0, radY = 0.", () => { + expect(ell.center).toStrictEqual(new Point(0, 0)); + expect(ell.radiusX).toBe(0); + expect(ell.radiusY).toBe(0); + }); + + test.fails.each([ + [-1, 1], + [1, -1], + [NaN, NaN], + [Infinity, Infinity], + [-Infinity, Infinity], + [Infinity, -Infinity], + [-Infinity, -Infinity], + ])( + "Constructions with top center (10, 10), radX = %f, radY = %f should throw errors.", + (radX, radY) => { + new Ellipse(center, radX, radY); + } + ); +}); + +describe("Ellipse containsPoint soliloquy:", () => { + const ell: Ellipse = new Ellipse(new Point(5, 5), 5, 5); //diameters of 10 + + test.each([ + [0, 0], //we probably should not have our Ellipses be Rectangles. These would be the corners + [0, 10], + [10, 0], + [10, 10], + [10, 5], //farthest reaches of this Ellipse + [0, 5], + [5, 0], + [5, 10], + [100, 100], //arbitrary Points that just shouldn't be within + [200, 200], + ])("Ellipse of center (5, 5), {radX, radY} = 5 should not contain Point (%f, %f).", (x, y) => { + expect(ell.containsPoint(new Point(x, y))).toBeFalsy(); + }); + + test.each([ + [5, 5], //we want this Ellipse to contain its center + [6, 6], //arbitrary Points that should be contained within + [4.3, 4.4], + [9.9, 4.9], + [5.1, 5.1], + [4.9, 9.9], + ])("Ellipse of center (5, 5), {radX, radY} = 5 should contain Point (%f, %f).", (x, y) => { + expect(ell.containsPoint(new Point(x, y))).toBeTruthy(); + }); +}); + +describe("Ellipse-on-Rectangle overlaps soliloquy:", () => { + const ell: Ellipse = new Ellipse(new Point(5, 5), 5, 5); //diameters of 10 + + //same logic with Rectangle's overlaps(), touching points along the edge should be overlap + //set to skip until Ellipse's overlaps, at least from a design choice perspective, are + //hammered out + test.skip.each([ + [10, 5, 0, 0], //Rectangle touching the rightmost point of this Ellipse + [5, 0, 0, 0], //Rectangle touching the topmost point of this Ellipse + [5, 10, 10, 10], //Rectangle touching the bottommost point of this Ellipse + [-10, -5, 5, 10], //Rectangle touching the leftmost point of this Ellipse + ])( + "Ellipse of center (5, 5) and {radX, radY} = 5 should overlap with Rectangle of TL vertex (%f, %f) and w = %f, h = %f.", + (x, y, w, h) => { + expect(ell.overlaps(new Rectangle(new Point(x, y), w, h))).toBeTruthy(); + } + ); + + //this block should be combined with the above block when design decisions are hammered out + test.each([ + [5, 5, 20, 20], //begins inside the Ellipse but touches the Ellipse's bounds from inside + [-5, -5, 10, 10], //begins outside the Ellipse but touches the Ellipse's bounds from outside + ])( + "Ellipse of center (5, 5) and {radX, radY} = 5 should overlap with Rectangle of TL vertex (%f, %f) and w = %f, h = %f.", + (x, y, w, h) => { + expect(ell.overlaps(new Rectangle(new Point(x, y), w, h))).toBeTruthy(); + } + ); + + test.each([ + [5, 5, 2, 2], //tiny guys + [5, 5, 0, 0], + ])( + "Ellipse of center (5, 5) and {radX, radY} = 5 should not overlap with Rectangle of TL vertex (%f, %f) and w = %f, h = %f.", + (x, y, w, h) => { + expect(ell.overlaps(new Rectangle(new Point(x, y), w, h))).toBeFalsy(); + } + ); +}); + +describe("Ellipse-on-Ellipse overlaps soliloquy:", () => { + const ell: Ellipse = new Ellipse(new Point(5, 5), 5, 5); //diameters of 10 + + test("Any Ellipse should overlap an Ellipse with the same measurements.", () => { + expect(ell.overlaps(ell)).toBeTruthy(); + }); +}); + +describe("Ellipse-on-Rectangle contains soliloquy:", () => { + const ell: Ellipse = new Ellipse(new Point(5, 5), 5, 5); //diameters of 10 + + test.each([ + [5, 0, 10, 10], //begins at the Ellipse's topmost point but top right corner is outside + [5, 5, 10, 10], //begins at Ellipse's center but bottom right corner is outside + ])( + "Ellipse of center (5, 5) and {radX, radY} = 5 should not contain Rectangle of TL vertex (%f, %f) and w = %f, h = %f.", + (x, y, w, h) => { + expect(ell.contains(new Rectangle(new Point(x, y), w, h))).toBeFalsy(); + } + ); +}); + +describe("Ellipse-on-Ellipse contains soliloquy:", () => { + const ell: Ellipse = new Ellipse(new Point(5, 5), 5, 5); //diameters of 10 + + test("Any Ellipse should not contain an Ellipse with the same measurements.", () => { + expect(ell.contains(new Rectangle(new Point(0, 0), 5, 5))).toBeFalsy(); + }); +}); + +describe("Ellipse toString soliloquy:", () => { + const ell: Ellipse = new Ellipse(new Point(5, 5), 5, 5); + + const expectedString = + "An ellipse with Center at: (5, 5), Horizontal radius: 5, Vertical radius: 5, Bounding box: Rectangle with top left vertex at: (0, 0), w: 10, h: 10"; + + test( + "Ellipse with center (5, 5) and {radX, radY} = 5 should produce toString " + + expectedString + + ".", + () => { + expect(ell.toString()).toBe(expectedString); + } + ); +}); diff --git a/tests/Point.test.ts b/tests/Point.test.ts new file mode 100644 index 00000000..63aa9104 --- /dev/null +++ b/tests/Point.test.ts @@ -0,0 +1,90 @@ +import {describe, expect, test} from "vitest"; + +import {Point} from "../src/AEG/Point"; + +/** + * Contains comprehensive unit tests on the Point class. + * @author Ryan Reilly + */ +let point: Point; +describe("Point constructor soliloquy: ", () => { + point = new Point(); + test("Default constructor should create a Point with x = 0 and y = 0.", () => { + expect(point.toString()).toBe("(0, 0)"); + }); + + test.each([ + [10, 10, "(10, 10)"], + [-10, 10, "(-10, 10)"], + [10, -10, "(10, -10)"], + [-10, -10, "(-10, -10)"], + [10.1, 10.1, "(10.1, 10.1)"], + [-10.1, 10.1, "(-10.1, 10.1)"], + [10.1, -10.1, "(10.1, -10.1)"], + [-10.1, -10.1, "(-10.1, -10.1)"], + ])("Should create a Point with (%f, %f).", (x, y, expectedString) => { + point = new Point(x, y); + expect(point.toString()).toBe(expectedString); + }); + + test.fails.each([ + [NaN, NaN], + [Infinity, Infinity], + [-Infinity, Infinity], + [Infinity, -Infinity], + [-Infinity, -Infinity], + ])("All these constructions should throw an error.", (x, y) => { + point = new Point(x, y); + }); +}); + +describe("Point set soliloquy:", () => { + const point2 = new Point(); + test.each([ + [10, 10, "(10, 10)"], + [-10, 10, "(-10, 10)"], + [10, -10, "(10, -10)"], + [-10, -10, "(-10, -10)"], + [10.1, 10.1, "(10.1, 10.1)"], + [-10.1, 10.1, "(-10.1, 10.1)"], + [10.1, -10.1, "(10.1, -10.1)"], + [-10.1, -10.1, "(-10.1, -10.1)"], + ])("Should set the Point to (%f, %f).", (x, y, expectedString) => { + point2.set(x, y); + expect(point2.toString()).toBe(expectedString); + }); + + test.fails.each([ + [NaN, NaN], + [Infinity, Infinity], + [-Infinity, Infinity], + [Infinity, -Infinity], + [-Infinity, -Infinity], + ])("Set attempt on (%f, %f) should throw an error.", (x, y) => { + point2.set(x, y); + }); +}); + +describe("Point distance soliloquy:", () => { + point = new Point(); + test.each([ + [point, point, 0], + [point, new Point(10, 10), Math.sqrt(200)], + [point, new Point(-10, 10), Math.sqrt(200)], + [point, new Point(-10, -10), Math.sqrt(200)], + ])("Distance between %o, %o, should be %i", (p1, p2, expected) => { + expect(p1.distance(p2)).toBe(expected); + }); +}); + +describe("Point distance soliloquy (float):", () => { + point = new Point(); + test.each([ + [point, new Point(0.00000000000001, 0.000000000001), 0], + [point, new Point(-0.00000000001, -0.0000000000001), 0], + [point, new Point(-0.0000000000001, -0.0000000000001), 0], + [point, new Point(0.0000000000000000000000000000000000000000000001, 0.000000000000001), 0], + ])("Distance between %o, %o, should be nearly %i", (p1, p2, expected) => { + expect(p1.distance(p2)).toBeCloseTo(expected); + }); +}); diff --git a/tests/Rectangle.test.ts b/tests/Rectangle.test.ts new file mode 100644 index 00000000..262c9d06 --- /dev/null +++ b/tests/Rectangle.test.ts @@ -0,0 +1,254 @@ +import {describe, expect, test} from "vitest"; + +import {Rectangle} from "../src/AEG/Rectangle"; +import {Ellipse} from "../src/AEG/Ellipse"; +import {Point} from "../src/AEG/Point"; + +/** + * Contains comprehensive tests on the Rectangle class. + * @author Ryan Reilly + */ + +describe("Rectangle constructor soliloquy:", () => { + const expectedVertex = new Point(0, 0); + test.each([ + [new Rectangle(new Point(0, 0), 1, 1), expectedVertex], + [new Rectangle(new Point(0, 0), 5, 5), expectedVertex], + ])("These constructors should produce a startingVertex of (0, 0).", (r, expectedVertex) => { + expect(r.startVertex).toStrictEqual(expectedVertex); + }); + + test.each([ + [0, 0], + [1, 1], + ])("These constructors should produce a width of %i and height %i.", (w, h) => { + expect(new Rectangle(new Point(0, 0), w, h).width).toBe(w); + expect(new Rectangle(new Point(0, 0), w, h).height).toBe(h); + }); + + test.fails.each([ + [-1, 1], + [1, -1], + [NaN, NaN], + [Infinity, Infinity], + [-Infinity, Infinity], + [Infinity, -Infinity], + [-Infinity, -Infinity], + ])("Constructions with top left vertex (0, 0), w = %i, h = %i should throw errors.", (w, h) => { + new Rectangle(new Point(0, 0), w, h); + }); +}); + +describe("Rectangle getCorners soliloquy:", () => { + const rect1 = new Rectangle(new Point(0, 0), 0, 0); + + test("Rectangle of top left vertex (0, 0) and {w, h} = 0 should have all (0, 0) corners.", () => { + expect(rect1.getCorners()).toStrictEqual([ + new Point(0, 0), + new Point(0, 0), + new Point(0, 0), + new Point(0, 0), + ]); + }); + + const rect2 = new Rectangle(new Point(0, 0), 10, 10); + + test("Rectangle of top left vertex (0, 0) and {w, h} = 10 should have apt corners.", () => { + expect(rect2.getCorners()).toStrictEqual([ + new Point(0, 0), + new Point(10, 0), + new Point(10, 10), + new Point(0, 10), + ]); + }); +}); + +describe("Rectangle containsPoint soliloquy:", () => { + const rect: Rectangle = new Rectangle(new Point(0, 0), 10, 10); + + test.each([ + [2.5, 5], + [5, 2.5], + [7.561231231231213, 4.12783918264], + ])("Rectangle of TL vertex (0, 0), {w, h} = 10 should contain Point (%f, %f).", (x, y) => { + expect(rect.containsPoint(new Point(x, y))).toBeTruthy(); + }); + + test.each([ + [0, 0], + [0, 10], + [10, 0], + [10, 10], + [0, 5], + [5, 0], + [10, 5], + [5, 10], + [-1, 0], + [0, -1], + [0, 11], + [11, 0], + [11, 11], + [0, -0.000000001], + [-0.00000000001, 0], + [0, 10.1], + [10.1, 0], + [10.1, 10], + [10, 10.1], + [10.1, 10], + [10.1, 10.1], + ])("Rectangle of TL vertex (0, 0), {w, h} = 10 should not contain Point (%f, %f).", (x, y) => { + expect(rect.containsPoint(new Point(x, y))).toBeFalsy(); + }); +}); + +describe("Rectangle-on-Rectangle overlaps soliloquy:", () => { + const rect: Rectangle = new Rectangle(new Point(0, 0), 10, 10); + test("Rectangle of TL vertex (0, 0) and {w, h} = 10 should not overlap Rectangle of TL vertex (11, 11) and {w, h} = 10.", () => { + expect(rect.overlaps(new Rectangle(new Point(11, 11), 10, 10))).toBeFalsy(); + }); + + test.each([ + [0, 0, 10, 10], //Rectangle with the same measurements + [10, 10, 10, 10], //Rectangle sharing one point in common + [0, 0, 0, 10], //Rectangle sharing one edge in common + [5, 5, 5, 5], //Rectangle starting inside of and crossing through the existing one + [-5, -5, 5, 5], //Rectangle starting outside and crossing through the existing one + ])( + "Rectangle of TL vertex(0, 0) and {w, h} = 10 should overlap with Rectangle of TL vertex (%f, %f) and w = %f, h %f.", + (x, y, w, h) => { + expect(rect.overlaps(new Rectangle(new Point(x, y), w, h))).toBeTruthy(); + } + ); +}); + +//skipping until the code determining this is hammered out +describe.skip("Rectangle-on-Ellipse overlaps soliloquy:", () => { + const rect: Rectangle = new Rectangle(new Point(0, 0), 10, 10); + + //same logic with Rectangle's overlaps(), touching points along the edge should be overlap + //set to skip until Ellipse's overlaps, at least from a design choice perspective, are + //hammered out + test.skip.each([ + [15, 0, 5, 5], //Ellipse touching the top right vertex of this Rectangle + [-5, 0, 5, 5], //Ellipse touching the top left vertex of this Rectangle + [-5, 10, 5, 5], //Ellipse touching the bottom left vertex of this Rectangle + [15, 10, 5, 5], //Ellipse touching the bottom right vertex of this Rectangle + ])( + "Rectangle of TL vertex (0, 0) and {w, h} = 10 should overlap with Ellipse of center (%f, %f) and radX = %f, radY = %f.", + (x, y, radX, radY) => { + expect(rect.overlaps(new Ellipse(new Point(x, y), radX, radY))).toBeTruthy(); + } + ); + + //this block should be combined with the above block when design decisions are hammered out + test.skip.each([ + [5, 5, 20, 20], //begins inside the Rectangle but touches the Rectangle's bounds from inside + [-5, 0, 20, 20], //begins outside the Rectangle but touches the Rectangle's bounds from outside + ])( + "Rectangle of TL vertex (0, 0) and {w, h} = 10 should overlap with Ellipse of center (%f, %f) and radX = %f, radY = %f.", + (x, y, radX, radY) => { + expect(rect.overlaps(new Ellipse(new Point(x, y), radX, radY))).toBeTruthy(); + } + ); + + test.each([ + [5, 5, 1, 1], //tiny guy + [5, 5, 20, 20], //huge guy + ])( + "Ellipse of center (5, 5) and {radX, radY} = 5 should not overlap with Rectangle of TL vertex (%f, %f) and w = %f, h = %f.", + (x, y, radX, radY) => { + expect(rect.overlaps(new Rectangle(new Point(x, y), radX, radY))).toBeFalsy(); + } + ); +}); + +describe("Rectangle-on-Rectangle contains soliloquy:", () => { + const rect: Rectangle = new Rectangle(new Point(0, 0), 10, 10); + + test("A Rectangle of TL vertex (0, 0) and {w, h} = 10 should not contain a Rectangle with the same measurements.", () => { + expect(rect.contains(new Rectangle(new Point(0, 0), 10, 10))).toBeFalsy(); + }); + + test.each([ + [0, 0, 0, 0], //essentially just Points that exist on the existing Rectangle's corners + [10, 0, 0, 0], + [0, 10, 0, 0], + [10, 10, 0, 0], + [5, 5, 5, 5], //begins inside the existing Rectangle but touches that Rectangle's bounds from inside + [-5, -5, 5, 5], //begins outside the existing Rectangle but touches that Rectangle's bounds from outside + ])( + "Rectangle of TL vertex (0, 0) and {w, h} = 10 should not contain Rectangle of TL vertex (%f, %f) and w = %f, h = %f.", + (x, y, w, h) => { + expect(rect.contains(new Rectangle(new Point(x, y), w, h))).toBeFalsy(); + } + ); + + test.each([ + [1, 1, 1, 1], //standard cases + [9.9, 9.9, 0.05, 0.05], + [9.9, 1, 0.05, 0.05], + [1, 9.9, 0.05, 0.05], + ])( + "Rectangle of TL vertex (0, 0) and {w, h} = 10 should contain Rectangle of TL vertex (%f, %f) and w = %f, h = %f.", + (x, y, w, h) => { + expect(rect.contains(new Rectangle(new Point(x, y), w, h))).toBeTruthy(); + } + ); +}); + +describe("Rectangle-on-Ellipse contains soliloquy:", () => { + const rect: Rectangle = new Rectangle(new Point(0, 0), 10, 10); + + test.each([ + [0, 0, 5, 5], //should not contain Ellipses whose centers are on corners or edges + [10, 0, 5, 5], + [0, 10, 5, 5], + [10, 10, 5, 5], + [0, 5, 5, 5], + [5, 0, 5, 5], + [10, 5, 5, 5], + [5, 10, 5, 5], + //should not contain Ellipses whose farthest points touch corners or edges + [15, 10, 5, 5], //touches (10, 10) on the Rectangle + [15, 5, 5, 5], //touches (10, 5) on the Rectangle + [15, 0, 5, 5], //touches (10, 0) on the Rectangle + [5, -5, 5, 5], //touches (5, 0) on the Rectangle + [-5, 0, 5, 5], //touches (0, 0) on the Rectangle + [-5, 5, 5, 5], //touches (0, 5) on the Rectangle + [-5, 10, 5, 5], //touches (0, 10) on the Rectangle + [5, 15, 5, 5], //touches (5, 10) on the Rectangle + ])( + "Rectangle of TL vertex (0, 0) and {w, h} = 10 should not contain Ellipse of center (%f, %f) and radX = %f, radY = %f.", + (x, y, rx, ry) => { + expect(rect.contains(new Ellipse(new Point(x, y), rx, ry))).toBeFalsy(); + } + ); + + test.each([ + [5, 5, 2, 2], //arbitrary Ellipses that should be contained within the Rectangle + [9.9, 9.9, 0, 0], + [0.1, 0.1, 0.01, 0.01], + [9.9, 0.1, 0.001, 0.001], + [0.1, 9.9, 0.0001, 0.0001], + ])( + "Rectangle of TL vertex (0, 0) and {w, h} = 10 should not contain Ellipse of center (%f, %f) and radX = %f, radY = %f.", + (x, y, rx, ry) => { + expect(rect.contains(new Ellipse(new Point(x, y), rx, ry))).toBeTruthy(); + } + ); +}); + +describe("Rectangle toString soliloquy:", () => { + const rect: Rectangle = new Rectangle(new Point(0, 0), 10, 10); + + const properOutput = "Rectangle with top left vertex at: (0, 0), w: 10, h: 10"; + + test( + "Rectangle with TL vertex (0, 0) and {w, h} = 10 should produce a toString of the form " + + properOutput + + ".", + () => { + expect(rect.toString()).toStrictEqual(properOutput); + } + ); +}); diff --git a/tsconfig.json b/tsconfig.json index 92c4450f..47b52ae0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,6 @@ }, "include": [ "src/**/*.ts", - "test/**/*.ts" + "tests/**/*.ts" ] } diff --git a/vite.config.js b/vite.config.js index 9542f250..f8a69905 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,4 +1,4 @@ - +/// import {resolve} from 'path'; import {defineConfig} from 'vite' @@ -7,7 +7,7 @@ export default defineConfig(({command, mode}) => { return { root: root, base: "/Peirce-My-Heart/", - publicDir: "../public/", + publicDir: "../public/", build:{ //only minify if you're trying to debug in the chrome debugger, otherwise use vsc debug //minify: mode === "production", @@ -20,6 +20,9 @@ export default defineConfig(({command, mode}) => { homepage: resolve(__dirname, root, "aeg.html") } } + }, + test:{ + include:["../tests/*"] } }; }) \ No newline at end of file