diff --git a/.github/workflows/nodejs.yaml b/.github/workflows/nodejs.yaml index c338c45..e4e934e 100644 --- a/.github/workflows/nodejs.yaml +++ b/.github/workflows/nodejs.yaml @@ -25,6 +25,9 @@ jobs: - name: Install Node.js dependencies run: npm ci + - name: + run: npm test + - name: Build Project run: npm run build # Ensure this outputs to the `dist` directory diff --git a/.github/workflows/wrangler-action.yaml b/.github/workflows/wrangler-action.yaml index 0ee74ba..2cee8e2 100644 --- a/.github/workflows/wrangler-action.yaml +++ b/.github/workflows/wrangler-action.yaml @@ -29,6 +29,9 @@ jobs: - name: Install Node.js dependencies run: npm ci + - name: Install Node.js dependencies + run: npm test + - name: Install Node.js dependencies run: npm run build diff --git a/package-lock.json b/package-lock.json index ec6307c..5e24f4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,12 +22,14 @@ "react-dom": "^18.3.1", "react-router-dom": "^6.26.1", "three": "^0.167.1", - "three-stdlib": "^2.32.1", - "zustand": "^5.0.0" + "three-stdlib": "^2.32.1" }, "devDependencies": { "@cloudflare/workers-types": "^4.20240821.1", - "@testing-library/react": "^16.0.0", + "@testing-library/jest-dom": "^6.6.2", + "@testing-library/react": "^16.0.1", + "@types/jest": "^29.5.14", + "@types/mocha": "^10.0.9", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^7.2.0", @@ -38,15 +40,23 @@ "eslint": "^8.57.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.6", + "jsdom": "^25.0.1", "playwright": "^1.46.1", "postcss": "^8.4.44", "tailwindcss": "^3.4.10", "typescript": "^5.2.2", "vite": "^5.2.0", - "vitest": "^2.0.5", + "vitest": "^2.1.3", "wrangler": "^3.74.0" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", + "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -59,20 +69,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", @@ -1183,6 +1179,50 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -1860,6 +1900,13 @@ "win32" ] }, + "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, + "license": "MIT" + }, "node_modules/@swc/core": { "version": "1.7.23", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.23.tgz", @@ -2204,6 +2251,48 @@ "node": ">=18" } }, + "node_modules/@testing-library/jest-dom": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.2.tgz", + "integrity": "sha512-P6GJD4yqc9jZLbe98j/EkyQDTPgqftohZF5FBkHY5BUERZmcf4HeO2k0XaefEg329ux2p21i1A1DmyQ1kKw2Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, "node_modules/@testing-library/react": { "version": "16.0.1", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.0.1.tgz", @@ -2285,6 +2374,86 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest/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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/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, + "license": "MIT", + "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/@types/jest/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mocha": { + "version": "10.0.9", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.9.tgz", + "integrity": "sha512-sicdRoWtYevwxjOHNMPTl3vSfJM6oyW8o1wXeI7uww6b6xHg8eBznQDNSGBCDJmsE8UMxP05JgZRtsKbTqt//Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mute-stream": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", @@ -2356,6 +2525,13 @@ "@types/react": "*" } }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/stats.js": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.3.tgz", @@ -2402,6 +2578,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", @@ -2634,18 +2827,20 @@ } }, "node_modules/@vitest/browser": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-2.0.5.tgz", - "integrity": "sha512-VbOYtu/6R3d7ASZREcrJmRY/sQuRFO9wMVsEDqfYbWiJRh2fDNi8CL1Csn7Ux31pOcPmmM5QvzFCMpiojvVh8g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-2.1.3.tgz", + "integrity": "sha512-PQ2kLLc9q8ukJutuuYsynHSr31E78/dtYEvPy4jCHLht1LmITqXTVTqu7THWdZ1kXNGrWwtdMqtt3z2mvSKdIg==", "dev": true, "license": "MIT", "dependencies": { "@testing-library/dom": "^10.4.0", "@testing-library/user-event": "^14.5.2", - "@vitest/utils": "2.0.5", - "magic-string": "^0.30.10", - "msw": "^2.3.2", + "@vitest/mocker": "2.1.3", + "@vitest/utils": "2.1.3", + "magic-string": "^0.30.11", + "msw": "^2.3.5", "sirv": "^2.0.4", + "tinyrainbow": "^1.2.0", "ws": "^8.18.0" }, "funding": { @@ -2653,7 +2848,7 @@ }, "peerDependencies": { "playwright": "*", - "vitest": "2.0.5", + "vitest": "2.1.3", "webdriverio": "*" }, "peerDependenciesMeta": { @@ -2669,14 +2864,14 @@ } }, "node_modules/@vitest/expect": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", - "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.3.tgz", + "integrity": "sha512-SNBoPubeCJhZ48agjXruCI57DvxcsivVDdWz+SSsmjTT4QN/DfHk3zB/xKsJqMs26bLZ/pNRLnCf0j679i0uWQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.0.5", - "@vitest/utils": "2.0.5", + "@vitest/spy": "2.1.3", + "@vitest/utils": "2.1.3", "chai": "^5.1.1", "tinyrainbow": "^1.2.0" }, @@ -2684,10 +2879,38 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/mocker": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.3.tgz", + "integrity": "sha512-eSpdY/eJDuOvuTA3ASzCjdithHa+GIF1L4PqtEELl6Qa3XafdMLBpBlZCIUCX2J+Q6sNmjmxtosAG62fK4BlqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.3", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.11" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/spy": "2.1.3", + "msw": "^2.3.5", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, "node_modules/@vitest/pretty-format": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", - "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.3.tgz", + "integrity": "sha512-XH1XdtoLZCpqV59KRbPrIhFCOO0hErxrQCMcvnQete3Vibb9UeIOX02uFPfVn3Z9ZXsq78etlfyhnkmIZSzIwQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2698,13 +2921,13 @@ } }, "node_modules/@vitest/runner": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz", - "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.3.tgz", + "integrity": "sha512-JGzpWqmFJ4fq5ZKHtVO3Xuy1iF2rHGV4d/pdzgkYHm1+gOzNZtqjvyiaDGJytRyMU54qkxpNzCx+PErzJ1/JqQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.0.5", + "@vitest/utils": "2.1.3", "pathe": "^1.1.2" }, "funding": { @@ -2712,14 +2935,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz", - "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.3.tgz", + "integrity": "sha512-qWC2mWc7VAXmjAkEKxrScWHWFyCQx/cmiZtuGqMi+WwqQJ2iURsVY4ZfAK6dVo6K2smKRU6l3BPwqEBvhnpQGg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.0.5", - "magic-string": "^0.30.10", + "@vitest/pretty-format": "2.1.3", + "magic-string": "^0.30.11", "pathe": "^1.1.2" }, "funding": { @@ -2727,9 +2950,9 @@ } }, "node_modules/@vitest/spy": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", - "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.3.tgz", + "integrity": "sha512-Nb2UzbcUswzeSP7JksMDaqsI43Sj5+Kry6ry6jQJT4b5gAK+NS9NED6mDb8FlMRCX8m5guaHCDZmqYMMWRy5nQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2740,14 +2963,13 @@ } }, "node_modules/@vitest/utils": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz", - "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.3.tgz", + "integrity": "sha512-xpiVfDSg1RrYT0tX6czgerkpcKFmFOF/gCr30+Mve5V2kewCy4Prn1/NDMSRwaSmT7PRaOF83wu+bEtsY1wrvA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.0.5", - "estree-walker": "^3.0.3", + "@vitest/pretty-format": "2.1.3", "loupe": "^3.1.1", "tinyrainbow": "^1.2.0" }, @@ -2791,6 +3013,19 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2933,6 +3168,13 @@ "node": ">=12" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -3174,9 +3416,9 @@ } }, "node_modules/chai": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", - "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", + "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", "dev": true, "license": "MIT", "dependencies": { @@ -3253,6 +3495,22 @@ "node": ">= 6" } }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/cli-spinners": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", @@ -3336,6 +3594,19 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -3394,6 +3665,13 @@ "node": ">= 8" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -3406,6 +3684,19 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", + "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rrweb-cssom": "^0.7.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -3419,6 +3710,20 @@ "dev": true, "license": "MIT" }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/date-fns": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", @@ -3454,6 +3759,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -3478,6 +3790,16 @@ "dev": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -3503,6 +3825,16 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "license": "Apache-2.0" }, + "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, + "license": "MIT", + "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", @@ -3567,6 +3899,19 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -3836,30 +4181,6 @@ "node": ">=0.10.0" } }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, "node_modules/exit-hook": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", @@ -3873,6 +4194,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4017,6 +4355,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -4071,16 +4424,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "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, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/get-source": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", @@ -4092,19 +4435,6 @@ "source-map": "^0.6.1" } }, - "node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4213,6 +4543,13 @@ "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==", "license": "MIT" }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -4271,14 +4608,58 @@ "integrity": "sha512-uybAvKS6uDe0MnWNEPnO0krWVr+8m2R0hJ/viql8H3MVK+itq8gGQuIYoFHL3rECkIpNH98Lw8YuuWMKZxp3Ew==", "license": "Apache-2.0" }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, "engines": { - "node": ">=16.17.0" + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, "node_modules/ieee754": { @@ -4344,6 +4725,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -4446,25 +4837,19 @@ "node": ">=8" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-promise": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", "license": "MIT" }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4507,6 +4892,192 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/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, + "license": "MIT", + "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/jest-diff/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/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, + "license": "MIT", + "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/jest-matcher-utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/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, + "license": "MIT", + "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/jest-message-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/jiti": { "version": "1.21.6", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", @@ -4535,6 +5106,60 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/tough-cookie": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -4620,6 +5245,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.castarray": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", @@ -4651,14 +5283,11 @@ } }, "node_modules/loupe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", - "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", + "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.1" - } + "license": "MIT" }, "node_modules/lru-cache": { "version": "10.4.3", @@ -4696,13 +5325,6 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4753,17 +5375,37 @@ "node": ">=10.0.0" } }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" } }, "node_modules/mini-svg-data-uri": { @@ -4992,34 +5634,12 @@ "node": ">=0.10.0" } }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "node_modules/nwsapi": { + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz", + "integrity": "sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==", "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "MIT" }, "node_modules/object-assign": { "version": "4.1.1", @@ -5056,22 +5676,6 @@ "wrappy": "1" } }, - "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5148,6 +5752,19 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.0.tgz", + "integrity": "sha512-ZkDsAOcxsUMZ4Lz5fVciOehNcJ+Gb8gTzcA4yl3wnc273BAybYWrQ+Ks/OjCjSEpjvQkDSeZbybK9qj2VHHdGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5713,6 +6330,20 @@ "node": ">=8.10.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -5902,6 +6533,13 @@ "dev": true, "license": "MIT" }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5925,6 +6563,26 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.21.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", @@ -6053,6 +6711,29 @@ "dev": true, "license": "MIT" }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -6188,17 +6869,17 @@ "node": ">=8" } }, - "node_modules/strip-final-newline": { + "node_modules/strip-indent": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" + "dependencies": { + "min-indent": "^1.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=8" } }, "node_modules/strip-json-comments": { @@ -6290,6 +6971,13 @@ "react": ">=17.0" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tabbable": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", @@ -6419,6 +7107,13 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyexec": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz", + "integrity": "sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==", + "dev": true, + "license": "MIT" + }, "node_modules/tinypool": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz", @@ -6440,15 +7135,35 @@ } }, "node_modules/tinyspy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz", - "integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", "dev": true, "license": "MIT", "engines": { "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "6.1.56", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.56.tgz", + "integrity": "sha512-2PT1oRZCxtsbLi5R2SQjE/v4vvgRggAtVcYj+3Rrcnu2nPZvu7m64+gDa/EsVSWd3QzEc0U0xN+rbEKsJC47kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.56" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.56", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.56.tgz", + "integrity": "sha512-Ihxv/Bwiyj73icTYVgBUkQ3wstlCglLoegSgl64oSrGUBX1hc7Qmf/CnrnJLaQdZrCnTaLqMYOwKMKlkfkFrxQ==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6487,6 +7202,19 @@ "node": ">=6" } }, + "node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/troika-three-text": { "version": "0.49.1", "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.49.1.tgz", @@ -6820,16 +7548,15 @@ } }, "node_modules/vite-node": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz", - "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.3.tgz", + "integrity": "sha512-I1JadzO+xYX887S39Do+paRePCKoiDrWRRjp9kkG5he0t7RXNvPAJPCQSJqbGN4uCrFFeS3Kj3sLqY8NMYBEdA==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.5", + "debug": "^4.3.6", "pathe": "^1.1.2", - "tinyrainbow": "^1.2.0", "vite": "^5.0.0" }, "bin": { @@ -6858,30 +7585,30 @@ } }, "node_modules/vitest": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz", - "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.3.tgz", + "integrity": "sha512-Zrxbg/WiIvUP2uEzelDNTXmEMJXuzJ1kCpbDvaKByFA9MNeO95V+7r/3ti0qzJzrxdyuUw5VduN7k+D3VmVOSA==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@vitest/expect": "2.0.5", - "@vitest/pretty-format": "^2.0.5", - "@vitest/runner": "2.0.5", - "@vitest/snapshot": "2.0.5", - "@vitest/spy": "2.0.5", - "@vitest/utils": "2.0.5", + "@vitest/expect": "2.1.3", + "@vitest/mocker": "2.1.3", + "@vitest/pretty-format": "^2.1.3", + "@vitest/runner": "2.1.3", + "@vitest/snapshot": "2.1.3", + "@vitest/spy": "2.1.3", + "@vitest/utils": "2.1.3", "chai": "^5.1.1", - "debug": "^4.3.5", - "execa": "^8.0.1", - "magic-string": "^0.30.10", + "debug": "^4.3.6", + "magic-string": "^0.30.11", "pathe": "^1.1.2", "std-env": "^3.7.0", - "tinybench": "^2.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.0", "tinypool": "^1.0.0", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "2.0.5", + "vite-node": "2.1.3", "why-is-node-running": "^2.3.0" }, "bin": { @@ -6896,8 +7623,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.0.5", - "@vitest/ui": "2.0.5", + "@vitest/browser": "2.1.3", + "@vitest/ui": "2.1.3", "happy-dom": "*", "jsdom": "*" }, @@ -6922,6 +7649,19 @@ } } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/webgl-constants": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", @@ -6933,6 +7673,53 @@ "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==", "license": "MIT" }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -7515,6 +8302,23 @@ } } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/xxhash-wasm": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.0.2.tgz", @@ -7630,35 +8434,6 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zustand": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.0.tgz", - "integrity": "sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==", - "license": "MIT", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } - } } } } diff --git a/package.json b/package.json index bb7be0b..ee748bf 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,8 @@ "scripts": { "dev": "vite --port 3000", "build": "tsc && vite build", + "test": "vitest", + "test:watch": "vitest --watch", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "npm run build && wrangler pages dev ./dist", "deploy": "npm run build && wrangler pages deploy ./dist" @@ -25,12 +27,14 @@ "react-dom": "^18.3.1", "react-router-dom": "^6.26.1", "three": "^0.167.1", - "three-stdlib": "^2.32.1", - "zustand": "^5.0.0" + "three-stdlib": "^2.32.1" }, "devDependencies": { "@cloudflare/workers-types": "^4.20240821.1", - "@testing-library/react": "^16.0.0", + "@testing-library/jest-dom": "^6.6.2", + "@testing-library/react": "^16.0.1", + "@types/jest": "^29.5.14", + "@types/mocha": "^10.0.9", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^7.2.0", @@ -41,12 +45,13 @@ "eslint": "^8.57.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.6", + "jsdom": "^25.0.1", "playwright": "^1.46.1", "postcss": "^8.4.44", "tailwindcss": "^3.4.10", "typescript": "^5.2.2", "vite": "^5.2.0", - "vitest": "^2.0.5", + "vitest": "^2.1.3", "wrangler": "^3.74.0" } } diff --git a/src/App.tsx b/src/App.tsx index f467f90..fada790 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,17 +1,20 @@ import { Routes, Route, Outlet, Link } from "react-router-dom"; import ProductPage from './pages/Product'; import ProductList from './pages/ProductList'; +import { ColorProvider } from './context/ColorContext'; function App() { return ( <> - - } /> - } /> - } /> - + + + } /> + } /> + } /> + + ) diff --git a/src/components/ColorPicker.test.tsx b/src/components/ColorPicker.test.tsx new file mode 100644 index 0000000..ab3582d --- /dev/null +++ b/src/components/ColorPicker.test.tsx @@ -0,0 +1,81 @@ +import { render, screen, waitFor, act } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { ColorProvider } from "../context/ColorContext"; +import ColorPicker from "../components/ColorPicker"; +import { ColorsResponse } from "../interfaces"; +import { vi } from "vitest"; + +// Mock data for color options +const mockColors: ColorsResponse[] = [ + { filament: "PLA", hexColor: "FF5733", colorTag: "Red" }, + { filament: "PLA", hexColor: "33FF57", colorTag: "Green" }, + { filament: "PLA", hexColor: "3357FF", colorTag: "Blue" }, +]; + +// Utility to simulate a delay in the fetch response +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +describe("ColorPicker Component", () => { + beforeEach(() => { + vi.spyOn(global, "fetch").mockImplementation(async () => { + await delay(50); // Simulate a short network delay + return { + json: async () => mockColors, + } as Response; + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("displays loading state initially", async () => { + await act(async () => { + render( + + + + ); + }); + + expect(screen.getByText("Loading...")).toBeInTheDocument(); + }); + + it("selects the first color initially after loading", async () => { + await act(async () => { + render( + + + + ); + }); + + await waitFor(() => expect(screen.getByLabelText("Choose a color")).toBeInTheDocument()); + + const selectedColorButton = screen.getByRole("radio", { checked: true }); + const backgroundColor = selectedColorButton.querySelector("span")?.style.backgroundColor; + expect(backgroundColor).toBe("rgb(255, 87, 51)"); // Expected color from hex "FF5733" + }); + + it("updates color when a new color is selected", async () => { + await act(async () => { + render( + + + + ); + }); + + await waitFor(() => expect(screen.getByLabelText("Choose a color")).toBeInTheDocument()); + + const secondColorButton = screen.getByRole("radio", { name: /Green/i }); + + await act(async () => { + await userEvent.click(secondColorButton); + }); + + expect(secondColorButton).toBeChecked(); + const selectedBackgroundColor = secondColorButton.querySelector("span")?.style.backgroundColor; + expect(selectedBackgroundColor).toBe("rgb(51, 255, 87)"); // Expected color from hex "33FF57" + }); +}); diff --git a/src/components/ColorPicker.tsx b/src/components/ColorPicker.tsx new file mode 100644 index 0000000..144da40 --- /dev/null +++ b/src/components/ColorPicker.tsx @@ -0,0 +1,79 @@ +import { useEffect } from "react"; +import { Radio, RadioGroup } from "@headlessui/react"; +import { ColorsResponse } from "../interfaces"; +import { useColorContext } from "../context/ColorContext"; + +const BASE_URL = "https://3dprinter-web-api.benhalverson.workers.dev"; + +const ColorPicker: React.FC = ({ filamentType }) => { + const { state, dispatch } = useColorContext(); + const { colorOptions, isLoading, color } = state; + + useEffect(() => { + const fetchColors = async () => { + dispatch({ type: "SET_IS_LOADING", payload: true }); + try { + const url = new URL(`${BASE_URL}/colors`); + if (filamentType) url.searchParams.set("filamentType", filamentType); + + const response = await fetch(url.toString()); + const colors = await response.json() as ColorsResponse[]; + dispatch({ type: "SET_COLOR_OPTIONS", payload: colors }); + } catch (error) { + console.error("Failed to fetch colors:", error); + } finally { + dispatch({ type: "SET_IS_LOADING", payload: false }); + } + }; + + if (filamentType) fetchColors(); + }, [filamentType, dispatch]); + + if (isLoading) return
Loading...
; + + return ( +
+ dispatch({ type: "SET_COLOR", payload: newColor })} + className="flex items-center space-x-3" + > + {colorOptions?.map((colorOption, index) => ( + + `relative -m-0.5 flex cursor-pointer items-center justify-center rounded-full p-0.5 focus:outline-none ${ + checked ? "ring-2 ring-offset-1 ring-blue-500" : "" + }` + } + > + {({ checked }) => ( + <> + + ))} + +
+ ); +}; + +export default ColorPicker; + +interface Props { + filamentType: string; +} diff --git a/src/components/FilamentDropdown.test.tsx b/src/components/FilamentDropdown.test.tsx new file mode 100644 index 0000000..5fb9f46 --- /dev/null +++ b/src/components/FilamentDropdown.test.tsx @@ -0,0 +1,46 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, it, expect, vi } from "vitest"; +import FilamentDropdown from "../components/FilamentDropdown"; + +describe("FilamentDropdown Component", () => { + it("renders with the correct initial value", () => { + render( + {}} /> + ); + + const dropdown = screen.getByRole("combobox"); + expect(dropdown).toBeInTheDocument(); + expect(dropdown).toHaveValue("PLA"); + }); + + it("calls setSelectedFilament with the correct value on change", async () => { + const mockSetSelectedFilament = vi.fn(); + + render( + + ); + + const dropdown = screen.getByRole("combobox"); + + // Change the selected filament to "PETG" + await userEvent.selectOptions(dropdown, "PETG"); + + expect(mockSetSelectedFilament).toHaveBeenCalledWith("PETG"); + expect(mockSetSelectedFilament).toHaveBeenCalledTimes(1); + }); + + it("renders all dropdown options", () => { + render( + {}} /> + ); + + const options = screen.getAllByRole("option"); + expect(options).toHaveLength(2); + expect(options[0]).toHaveValue("PLA"); + expect(options[1]).toHaveValue("PETG"); + }); +}); diff --git a/src/components/filamentDropdown.tsx b/src/components/FilamentDropdown.tsx similarity index 100% rename from src/components/filamentDropdown.tsx rename to src/components/FilamentDropdown.tsx diff --git a/src/components/PreviewComponent.tsx b/src/components/PreviewComponent.tsx index 113c027..9d71e82 100644 --- a/src/components/PreviewComponent.tsx +++ b/src/components/PreviewComponent.tsx @@ -1,257 +1,249 @@ /* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable react-hooks/exhaustive-deps */ +/* eslint-disable @typescript-eslint/no-unused-vars */ import React, { useEffect, useRef, useState } from "react"; import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import { usePreviewService } from "../hooks/usePreview"; -import { useColorStore } from '../store/colorStore'; +import { useColorContext } from "../context/ColorContext"; const GRID_SIZE = 250; // in mm const LIMIT_DIMENSIONS_MM = { length: 250, width: 250, height: 310 }; // in mm interface PreviewComponentProps { - url: string; - onExceedsLimit: (limit: boolean) => void; - onError: (error: string) => void; + url: string; + onExceedsLimit: (limit: boolean) => void; + onError: (error: string) => void; } const PreviewComponent: React.FC = ({ - url, - onExceedsLimit, - onError, + url, + onExceedsLimit, + onError, }) => { - const {color} = useColorStore() - const previewRef = useRef(null); - const { loadModel } = usePreviewService(); - const scene = useRef(new THREE.Scene()).current; - const camera = useRef( - new THREE.PerspectiveCamera( - 75, - window.innerWidth / window.innerHeight, - 0.1, - 1000 - ) - ).current; - const renderer = useRef(new THREE.WebGLRenderer({ antialias: true })).current; - const meshRef = useRef(null); - const gridHelperRef = useRef(null); - const controlsRef = useRef(null); - const [_dimensions, setDimensions] = useState<{ - length: number; - width: number; - height: number; - }>({ length: 0, width: 0, height: 0 }); - const [modelLoaded, setModelLoaded] = useState(false); - const [_exceedsLimit, setExceedsLimit] = useState(false); - const [_errorMessage, setErrorMessage] = useState(null); - - useEffect(() => { - if (previewRef.current) { - initializeScene(); - } - }, []); - - useEffect(() => { - if (modelLoaded) { - fitCameraToObject(meshRef.current!); - updateGrid(); - updateDimensions(); - } - }, [modelLoaded]); - - useEffect(() => { - const handleResize = () => { - renderer.setSize(window.innerWidth, window.innerHeight); - camera.aspect = window.innerWidth / window.innerHeight; - camera.updateProjectionMatrix(); - }; - - window.addEventListener("resize", handleResize); - - return () => { - window.removeEventListener("resize", handleResize); - }; - }, []); - - // Whenever the URL or color changes, reload the model - useEffect(() => { - loadModelAndCheckDimensions(url); - }, [url]); - - const initializeScene = () => { - renderer.setSize(600, 400); // Fixed size - previewRef.current!.appendChild(renderer.domElement); - camera.position.z = 500; - scene.add(new THREE.AmbientLight(0xffffff, 0.5)); - scene.add(new THREE.DirectionalLight(0xffffff, 0.5)); - - // Set the background color to white - renderer.setClearColor(0xf0f0f0); // A slightly darker white color - - controlsRef.current = new OrbitControls(camera, renderer.domElement); - animate(); - }; - - const loadModelAndCheckDimensions = async (url: string) => { - try { - const geometry = await loadModel(url); - if (geometry) { - // Convert color string to hexadecimal number for Three.js - // Remove any `#` and convert the string to a number using base 16 - const hexColor = parseInt(color.replace("#", ""), 16); - console.log('hexColor', hexColor); - - const material = new THREE.MeshStandardMaterial({ color: hexColor }); // Apply the hex color - - meshRef.current = new THREE.Mesh(geometry, material); - - let boundingBox = new THREE.Box3().setFromObject(meshRef.current); - const center = boundingBox.getCenter(new THREE.Vector3()); - const size = boundingBox.getSize(new THREE.Vector3()); - - checkDimensions(size); - - meshRef.current.rotation.x = Math.PI / 2; - meshRef.current.updateMatrixWorld(); - const worldDir = new THREE.Vector3(); - meshRef.current.getWorldDirection(worldDir); - if (worldDir.y < 0) { - meshRef.current.rotation.x += Math.PI; - } - - boundingBox = new THREE.Box3().setFromObject(meshRef.current); - boundingBox.getCenter(center); - boundingBox.getSize(size); - - checkDimensions(size); - - meshRef.current.position.copy(center).multiplyScalar(-1); - meshRef.current.position.y += size.y / 2; - - scene.add(meshRef.current); - setModelLoaded(true); - } else { - throw new Error( - "Invalid file: Could not load the 3D model from the provided file." - ); - } - } catch (error: any) { - console.error(error); - setErrorMessage(error.message); - setExceedsLimit(true); - onError(error.message); - } - }; - - const fitCameraToObject = (object: THREE.Object3D, offset = 2) => { - const boundingBox = new THREE.Box3().setFromObject(object); - const center = boundingBox.getCenter(new THREE.Vector3()); - const size = boundingBox.getSize(new THREE.Vector3()); - - const maxDim = Math.max(size.x, size.y, size.z); - const fov = camera.fov * (Math.PI / 180); - const cameraZ = Math.abs(maxDim / (2 * Math.tan(fov / 2))); - - camera.position.set(center.x, size.y / 2, cameraZ * offset); - camera.lookAt(center.add(new THREE.Vector3(0, size.y / 2, 0))); - - if (controlsRef.current) { - controlsRef.current.target = center; - } - - camera.updateProjectionMatrix(); - }; - - const animate = () => { - requestAnimationFrame(animate); - controlsRef.current!.update(); - renderer.render(scene, camera); - }; - - const updateGrid = () => { - if (gridHelperRef.current) { - scene.remove(gridHelperRef.current); - } - - const gridHelper = new THREE.GridHelper(GRID_SIZE, GRID_SIZE); - scene.add(gridHelper); - gridHelperRef.current = gridHelper; - }; - - const updateMaterialColor = (hexColor: number) => { - if (meshRef.current) { - const material = new THREE.MeshStandardMaterial({ color: hexColor }); - meshRef.current.material = material; - } - }; - - useEffect(() => { - if (modelLoaded) { - updateMaterialColor(parseInt(color.replace("#", ""), 16)); - } - }, [color]); - - const updateDimensions = () => { - const boundingBox = new THREE.Box3().setFromObject(meshRef.current!); - const size = boundingBox.getSize(new THREE.Vector3()); - - if (size.x === 0 || size.y === 0 || size.z === 0) { - setError("Invalid model: The model dimensions are zero."); - return; - } - - setDimensions({ - length: parseFloat(size.x.toFixed(2)), - width: parseFloat(size.y.toFixed(2)), - height: parseFloat(size.z.toFixed(2)), - }); - - const modelExceedsLimit = - size.x > LIMIT_DIMENSIONS_MM.length || - size.y > LIMIT_DIMENSIONS_MM.width || - size.z > LIMIT_DIMENSIONS_MM.height; - - setExceedsLimit(modelExceedsLimit); - onExceedsLimit(modelExceedsLimit); - }; - - const checkDimensions = (size: THREE.Vector3) => { - if (size.x === 0 || size.y === 0 || size.z === 0) { - setError("Invalid model: The model dimensions are zero."); - return; - } - - setDimensions({ - length: parseFloat(size.x.toFixed(2)), - width: parseFloat(size.y.toFixed(2)), - height: parseFloat(size.z.toFixed(2)), - }); - - const modelExceedsLimit = - size.x > LIMIT_DIMENSIONS_MM.length || - size.y > LIMIT_DIMENSIONS_MM.width || - size.z > LIMIT_DIMENSIONS_MM.height; - - if (modelExceedsLimit) { - setError( - `Model dimensions exceed our limit of ${LIMIT_DIMENSIONS_MM.length} (L) x ${LIMIT_DIMENSIONS_MM.width} (W) x ${LIMIT_DIMENSIONS_MM.height} (H) mm. Please choose a smaller model.` - ); - } - }; - - const setError = (message: string) => { - setErrorMessage(message); - setExceedsLimit(true); - onError(message); - }; - - return ( -
-
-
-
-
- ); + const { state } = useColorContext(); + const { color } = state; + + const previewRef = useRef(null); + const { loadModel } = usePreviewService(); + const scene = useRef(new THREE.Scene()).current; + const camera = useRef( + new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000) + ).current; + const renderer = useRef(new THREE.WebGLRenderer({ antialias: true })).current; + const meshRef = useRef(null); + const gridHelperRef = useRef(null); + const controlsRef = useRef(null); + const animationFrameId = useRef(null); + + const [_dimensions, setDimensions] = useState<{ length: number; width: number; height: number }>({ + length: 0, + width: 0, + height: 0, + }); + const [modelLoaded, setModelLoaded] = useState(false); + const [_exceedsLimit, setExceedsLimit] = useState(false); + const [_errorMessage, setErrorMessage] = useState(null); + + useEffect(() => { + if (previewRef.current) { + initializeScene(); + } + return () => { + // Cancel animation on unmount + if (animationFrameId.current) { + cancelAnimationFrame(animationFrameId.current); + } + }; + }, []); + + useEffect(() => { + if (modelLoaded) { + fitCameraToObject(meshRef.current!); + updateGrid(); + updateDimensions(); + } + }, [modelLoaded]); + + useEffect(() => { + const handleResize = () => { + renderer.setSize(window.innerWidth, window.innerHeight); + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + }; + + window.addEventListener("resize", handleResize); + return () => { + window.removeEventListener("resize", handleResize); + }; + }, []); + + // Reload the model whenever the URL or color changes + useEffect(() => { + if (url && color) { + loadModelAndCheckDimensions(url); + } + }, [url, color]); + + const initializeScene = () => { + renderer.setSize(600, 400); // Fixed size + previewRef.current!.appendChild(renderer.domElement); + camera.position.z = 500; + scene.add(new THREE.AmbientLight(0xffffff, 0.5)); + scene.add(new THREE.DirectionalLight(0xffffff, 0.5)); + + renderer.setClearColor(0xf0f0f0); // A slightly darker white color + + controlsRef.current = new OrbitControls(camera, renderer.domElement); + animate(); + }; + + const loadModelAndCheckDimensions = async (url: string) => { + // Dispose of previous mesh, if any + if (meshRef.current) { + scene.remove(meshRef.current); + meshRef.current.geometry.dispose(); + (meshRef.current.material as THREE.Material).dispose(); + } + + try { + const geometry = await loadModel(url); + if (!geometry) throw new Error("Model loading failed. Geometry is undefined."); + + const hexColor = parseInt(color.replace("#", ""), 16); + const material = new THREE.MeshStandardMaterial({ color: hexColor }); + meshRef.current = new THREE.Mesh(geometry, material); + + const boundingBox = new THREE.Box3().setFromObject(meshRef.current); + const center = boundingBox.getCenter(new THREE.Vector3()); + const size = boundingBox.getSize(new THREE.Vector3()); + + checkDimensions(size); + + meshRef.current.rotation.x = Math.PI / 2; + meshRef.current.position.copy(center).multiplyScalar(-1); + meshRef.current.position.y += size.y / 2; + + scene.add(meshRef.current); + setModelLoaded(true); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : "Failed to load model"; + console.error(message); + setErrorMessage(message); + setExceedsLimit(true); + onError(message); + } + }; + + const fitCameraToObject = (object: THREE.Object3D, offset = 2) => { + const boundingBox = new THREE.Box3().setFromObject(object); + const center = boundingBox.getCenter(new THREE.Vector3()); + const size = boundingBox.getSize(new THREE.Vector3()); + + const maxDim = Math.max(size.x, size.y, size.z); + const fov = camera.fov * (Math.PI / 180); + const cameraZ = Math.abs(maxDim / (2 * Math.tan(fov / 2))); + + camera.position.set(center.x, size.y / 2, cameraZ * offset); + camera.lookAt(center.add(new THREE.Vector3(0, size.y / 2, 0))); + + if (controlsRef.current) { + controlsRef.current.target = center; + } + + camera.updateProjectionMatrix(); + }; + + const animate = () => { + animationFrameId.current = requestAnimationFrame(animate); + controlsRef.current!.update(); + renderer.render(scene, camera); + }; + + const updateGrid = () => { + if (gridHelperRef.current) { + scene.remove(gridHelperRef.current); + } + const gridHelper = new THREE.GridHelper(GRID_SIZE, GRID_SIZE); + scene.add(gridHelper); + gridHelperRef.current = gridHelper; + }; + + const updateMaterialColor = (hexColor: number) => { + if (meshRef.current) { + const material = new THREE.MeshStandardMaterial({ color: hexColor }); + meshRef.current.material = material; + } + }; + + useEffect(() => { + if (modelLoaded && color) { + updateMaterialColor(parseInt(color.replace("#", ""), 16)); + } + }, [color, modelLoaded]); + + const updateDimensions = () => { + const boundingBox = new THREE.Box3().setFromObject(meshRef.current!); + const size = boundingBox.getSize(new THREE.Vector3()); + + if (size.x === 0 || size.y === 0 || size.z === 0) { + setError("Invalid model: The model dimensions are zero."); + return; + } + + setDimensions({ + length: parseFloat(size.x.toFixed(2)), + width: parseFloat(size.y.toFixed(2)), + height: parseFloat(size.z.toFixed(2)), + }); + + const modelExceedsLimit = + size.x > LIMIT_DIMENSIONS_MM.length || + size.y > LIMIT_DIMENSIONS_MM.width || + size.z > LIMIT_DIMENSIONS_MM.height; + + setExceedsLimit(modelExceedsLimit); + onExceedsLimit(modelExceedsLimit); + }; + + const checkDimensions = (size: THREE.Vector3) => { + if (size.x === 0 || size.y === 0 || size.z === 0) { + setError("Invalid model: The model dimensions are zero."); + return; + } + + setDimensions({ + length: parseFloat(size.x.toFixed(2)), + width: parseFloat(size.y.toFixed(2)), + height: parseFloat(size.z.toFixed(2)), + }); + + const modelExceedsLimit = + size.x > LIMIT_DIMENSIONS_MM.length || + size.y > LIMIT_DIMENSIONS_MM.width || + size.z > LIMIT_DIMENSIONS_MM.height; + + if (modelExceedsLimit) { + setError( + `Model dimensions exceed our limit of ${LIMIT_DIMENSIONS_MM.length} (L) x ${LIMIT_DIMENSIONS_MM.width} (W) x ${LIMIT_DIMENSIONS_MM.height} (H) mm. Please choose a smaller model.` + ); + } + }; + + const setError = (message: string) => { + setErrorMessage(message); + setExceedsLimit(true); + onError(message); + }; + + return ( +
+
+
+
+
+ ); }; export default PreviewComponent; diff --git a/src/components/colorPicker.tsx b/src/components/colorPicker.tsx deleted file mode 100644 index 5ba7791..0000000 --- a/src/components/colorPicker.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { useEffect } from "react"; -import { Radio, RadioGroup } from "@headlessui/react"; -import { ColorsResponse } from "../interfaces"; -// const BASE_URL = import.meta.env.VITE_BASE_URL; -const BASE_URL = "https://3dprinter-web-api.benhalverson.workers.dev"; - -import { useColorStore } from "../store/colorStore"; - - -const ColorPicker: React.FC = ({filamentType}) => { - const url = new URL(`${BASE_URL}/colors`); - const {colorOptions, isLoading, setIsLoading, color, setColorOptions, setColor} = useColorStore(); - - const fetchColors = async (filamentType?: string) => { - - if(filamentType) { - url.searchParams.set("filamentType", filamentType); - } - const response = await fetch(url.toString()); - setColorOptions(await (response.json() as Promise)); - return response - - }; - - useEffect(() => { - if(filamentType) { - - setIsLoading(true); - fetchColors(filamentType); - } - setIsLoading(false); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [filamentType]); - - - if (isLoading) return
Loading...
; - // if (error) return
No data available
; - - console.log('colorOptions', colorOptions); - console.log('color', color) - return ( -
- - {colorOptions?.map((color, i) => ( - - `relative -m-0.5 flex cursor-pointer items-center justify-center rounded-full p-0.5 focus:outline-none ${ - checked ? "ring-2 ring-offset-1 ring-blue-500" : "" - }` - } - > - {({ checked }) => ( - <> - - ))} - -
- ); -} - -export default ColorPicker; - -interface Props { - filamentType: string; -} \ No newline at end of file diff --git a/src/context/ColorContext.tsx b/src/context/ColorContext.tsx new file mode 100644 index 0000000..7886a6f --- /dev/null +++ b/src/context/ColorContext.tsx @@ -0,0 +1,60 @@ +import React, { createContext, useContext, useReducer, ReactNode } from "react"; +import { ColorsResponse } from "../interfaces"; + +interface ColorState { + colorOptions: ColorsResponse[]; + color: string; + isLoading: boolean; + hasInitialized: boolean; // Track if the initial color has been set +} + +type ColorAction = + | { type: "SET_COLOR_OPTIONS"; payload: ColorsResponse[] } + | { type: "SET_COLOR"; payload: string } + | { type: "SET_IS_LOADING"; payload: boolean }; + +const initialState: ColorState = { + colorOptions: [], + color: "", + isLoading: false, + hasInitialized: false, // Initialize as false +}; + +const ColorContext = createContext<{ + state: ColorState; + dispatch: React.Dispatch; +}>({ + state: initialState, + dispatch: () => null, +}); + +const colorReducer = (state: ColorState, action: ColorAction): ColorState => { + switch (action.type) { + case "SET_COLOR_OPTIONS": + return { + ...state, + colorOptions: action.payload, + // Only set color initially if not already initialized + color: !state.hasInitialized && action.payload.length > 0 ? action.payload[0].hexColor : state.color, + hasInitialized: true, + }; + case "SET_COLOR": + return { ...state, color: action.payload }; + case "SET_IS_LOADING": + return { ...state, isLoading: action.payload }; + default: + return state; + } +}; + +export const ColorProvider = ({ children }: { children: ReactNode }) => { + const [state, dispatch] = useReducer(colorReducer, initialState); + + return ( + + {children} + + ); +}; + +export const useColorContext = () => useContext(ColorContext); diff --git a/src/data/test.ts b/src/data/test.ts deleted file mode 100644 index a8621ad..0000000 --- a/src/data/test.ts +++ /dev/null @@ -1,66 +0,0 @@ -// TODO: implent the transformData function that takes in an array of RawDataEntry objects and from manf and returns structured json for backend endpoint. -// Define the structure of a color entry -interface ColorEntry { - color: string; - hexColor: string; - colorTag: string; -} - -// Define the raw data structure -interface RawDataEntry { - filament: string; - hexColor: string; - colorTag: string; -} - -// Define the structure of the transformed output -interface TransformedData { - [key: string]: ColorEntry[]; -} - -export const rawData: RawDataEntry[] = [ - { filament: "PETG WHITE", hexColor: "f6efef", colorTag: "petgWhite" }, - { filament: "PETG BLACK", hexColor: "000000", colorTag: "petgBlack" }, - { filament: "PLA BLACK", hexColor: "000000", colorTag: "black" }, - { filament: "PLA GRAY", hexColor: "666666", colorTag: "gray" }, - { filament: "PLA WHITE", hexColor: "ffffff", colorTag: "white" }, - { filament: "PLA YELLOW", hexColor: "f5c211", colorTag: "yellow" }, - { filament: "PLA RED", hexColor: "f91010", colorTag: "red" }, - { filament: "PLA GOLD", hexColor: "d5b510", colorTag: "gold" }, - { filament: "PLA LUNAR REGOLITH", hexColor: "7d7e7e", colorTag: "lunarRegolith" }, - { filament: "PLA MATTE BLACK", hexColor: "000000", colorTag: "matteBlack" } -]; - -export const transformData = (data: RawDataEntry[]): TransformedData[] => { - const filamentMap: TransformedData = {}; - - data.forEach((item) => { - const [type, color] = item.filament.split(' '); // Split filament into type and color - - const colorEntry: ColorEntry = { - color: color.toUpperCase(), // Capitalize color name - hexColor: item.hexColor, - colorTag: item.colorTag, - }; - - // If the filament type doesn't exist, initialize it - if (!filamentMap[type]) { - filamentMap[type] = []; - } - - console.log(filamentMap); - // Push the color entry to the corresponding filament type - filamentMap[type].push(colorEntry); - }); - - // Convert the map into an array of objects, sorted by PLA first, PETG second - return Object.entries(filamentMap) - .sort(([keyA], [keyB]) => { - if (keyA === 'PLA') return -1; - if (keyB === 'PLA') return 1; - return keyA.localeCompare(keyB); // Sort alphabetically otherwise - }) - .map(([filamentType, colors]) => ({ - [filamentType]: colors, - })); -}; \ No newline at end of file diff --git a/src/hooks/usePreview.test.ts b/src/hooks/usePreview.test.ts new file mode 100644 index 0000000..982d761 --- /dev/null +++ b/src/hooks/usePreview.test.ts @@ -0,0 +1,86 @@ +import { renderHook } from "@testing-library/react"; +import { vi } from "vitest"; +import * as THREE from "three"; +import { usePreviewService } from './usePreview'; +import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js'; + +// Mock STLLoader directly +vi.mock("three/examples/jsm/loaders/STLLoader.js", () => { + return { + STLLoader: vi.fn().mockImplementation(() => ({ + load: vi.fn(), // Define load as a mock function + })), + }; +}); + +describe("usePreviewService", () => { + let mockGeometry: THREE.BufferGeometry; + + beforeEach(() => { + // Set up a mock geometry with a bounding box + mockGeometry = new THREE.BufferGeometry(); + mockGeometry.computeBoundingBox = vi.fn(() => { + mockGeometry.boundingBox = new THREE.Box3( + new THREE.Vector3(0, 0, 0), + new THREE.Vector3(10, 20, 30) + ); + }); + + // Ensure load is mocked for each test + STLLoader.prototype.load = vi.fn(); + }); + + it.skip( + "loads a model successfully using loadModel", + async () => { + const { result } = renderHook(() => usePreviewService()); + + // Mock load to immediately call onLoad with mockGeometry + (STLLoader.prototype.load as jest.Mock).mockImplementation((_, onLoad) => { + onLoad(mockGeometry); + }); + + const geometry = await result.current.loadModel("mockURL"); + expect(geometry).toBe(mockGeometry); + }, + 10000 + ); + + it.skip( + "handles loadModel error correctly", + async () => { + const { result } = renderHook(() => usePreviewService()); + + // Mock load to immediately call onError with an error + (STLLoader.prototype.load as jest.Mock).mockImplementation((_, __, ___, onError) => { + onError(new Error("Failed to load")); + }); + + await expect(result.current.loadModel("mockURL")).rejects.toThrow("Failed to load"); + }, + 10000 + ); + + it("calculates dimensions using getDimensions", () => { + const { result } = renderHook(() => usePreviewService()); + mockGeometry.computeBoundingBox(); + + const dimensions = result.current.getDimensions(mockGeometry); + expect(dimensions).toEqual({ + width: 10, + height: 20, + depth: 30, + }); + }); + + it("checks model dimensions using checkModelDimensions", () => { + const { result } = renderHook(() => usePreviewService()); + const dimensions = result.current.checkModelDimensions(mockGeometry); + + expect(dimensions).toEqual({ + length: 10, + width: 20, + height: 30, + }); + }); +}); diff --git a/src/pages/Product.test.tsx b/src/pages/Product.test.tsx new file mode 100644 index 0000000..4e16c09 --- /dev/null +++ b/src/pages/Product.test.tsx @@ -0,0 +1,93 @@ +import { render, screen, fireEvent, waitFor, act } from "@testing-library/react"; +import ProductPage from "./Product"; +import { vi } from "vitest"; +import { Suspense } from "react"; + +// Mock lazy-loaded component +vi.mock("../components/PreviewComponent", () => ({ + default: () =>
Preview Component
, +})); + +// Mock components +vi.mock("../components/ColorPicker", () => ({ + default: ({ filamentType }: {filamentType: string}) => ( +
+ Color Picker for {filamentType} +
+ ), +})); + +vi.mock("../components/FilamentDropdown", () => ({ + default: ({ selectedFilament, setSelectedFilament }: { selectedFilament: string; setSelectedFilament: (value: string) => void }) => ( + + ), +})); + +describe("ProductPage", () => { + beforeEach(async () => { + // Wrap render in act() to handle Suspense loading + await act(async () => { + render( + Loading...}> + + + ); + }); + }); + + it("renders ProductPage with product details", () => { + expect(screen.getByText("RC Wheels")).toBeInTheDocument(); + expect(screen.getByText("$35")).toBeInTheDocument(); + expect(screen.getByText("Description")).toBeInTheDocument(); + expect( + screen.getByText( + "This is a 12mm RC buggy wheel that will fit any modern buggy for 1/10 scale racing." + ) + ).toBeInTheDocument(); + }); + + it("renders PreviewComponent in a Suspense wrapper", async () => { + // Wait for PreviewComponent to load and check for its existence + await waitFor(() => { + expect(screen.getByTestId("preview-component")).toBeInTheDocument(); + }); + }); + + it("displays the initial filament type as PLA", () => { + const filamentDropdown = screen.getByTestId("filament-dropdown"); + expect(filamentDropdown).toHaveValue("PLA"); + }); + + it("updates filament selection when dropdown value changes", async () => { + const filamentDropdown = screen.getByTestId("filament-dropdown"); + fireEvent.change(filamentDropdown, { target: { value: "PETG" } }); + expect(filamentDropdown).toHaveValue("PETG"); + }); + + it("passes selected filament to ColorPicker", () => { + const colorPicker = screen.getByTestId("color-picker"); + expect(colorPicker).toHaveTextContent("Color Picker for PLA"); + }); + + it("updates ColorPicker filamentType when filament selection changes", async () => { + const filamentDropdown = screen.getByTestId("filament-dropdown"); + fireEvent.change(filamentDropdown, { target: { value: "PETG" } }); + + await waitFor(() => { + const colorPicker = screen.getByTestId("color-picker"); + expect(colorPicker).toHaveTextContent("Color Picker for PETG"); + }); + }); + + it("renders Add to cart button", () => { + const addToCartButton = screen.getByRole("button", { name: "Add to cart" }); + expect(addToCartButton).toBeInTheDocument(); + }); +}); diff --git a/src/pages/Product.tsx b/src/pages/Product.tsx index 7029aa2..81c2e59 100644 --- a/src/pages/Product.tsx +++ b/src/pages/Product.tsx @@ -1,8 +1,8 @@ /* eslint-disable react-hooks/exhaustive-deps */ import { lazy, Suspense, useState } from "react"; import { ShoppingBagIcon, UserIcon } from "@heroicons/react/24/outline"; -import ColorPicker from "../components/colorPicker"; -import FilamentDropdown from '../components/filamentDropdown'; +import ColorPicker from "../components/ColorPicker"; +import FilamentDropdown from '../components/FilamentDropdown'; const PreviewComponent = lazy(() => import("../components/PreviewComponent")); @@ -78,7 +78,7 @@ export default function ProductPage() {

Images

- Loading...
}> + Loading...}> false} diff --git a/src/setupTests.ts b/src/setupTests.ts new file mode 100644 index 0000000..7b0828b --- /dev/null +++ b/src/setupTests.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/src/store/colorStore.ts b/src/store/colorStore.ts deleted file mode 100644 index 806337f..0000000 --- a/src/store/colorStore.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { create } from 'zustand'; -import { ColorsResponse } from '../interfaces'; - -interface ColorState { - // State - colorOptions: ColorsResponse[]; - color: string; - isLoading: boolean; - - // Actions - setColorOptions: (colorOptions: ColorsResponse[]) => void; - setColor: (color: string) => void; - setIsLoading: (isLoading: boolean) => void; - -} - -export const useColorStore = create((set) => ({ - // State - colorOptions: [], - color: '000000', - isLoading: false, - - // Actions - setColorOptions: (colorOptions: ColorsResponse[]) => set({ colorOptions }), - setColor: (color: string) => set({ color }), - setIsLoading: (isLoading: boolean) => set({ isLoading }), -})); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 86819c7..f765df2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,10 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "types": [ - "@cloudflare/workers-types/2023-07-01" + "@cloudflare/workers-types/2023-07-01", + "vitest", + "mocha", + "jest" ] }, "include": ["src"], diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..9af2bfc --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": ["vitest", "node"] + }, + "include": ["src"] +} diff --git a/vite.config.ts b/vite.config.ts index 5934b75..e04107f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,10 +1,14 @@ -import { defineConfig } from 'vite' +import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react-swc' -// https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], - // define: { - // 'process.env.VITE_BASE_URL': JSON.stringify(process.env.VITE_BASE_URL) - // } -}) + test: { + globals: true, // Enable global test functions like describe, it, expect + environment: "jsdom", // Use jsdom for DOM-related tests + setupFiles: "./src/setupTests.ts", // Optional: Path to a setup file if needed + typecheck: { + tsconfig: "./tsconfig.test.json", // Specify the test-specific tsconfig + }, + }, +}) \ No newline at end of file