From f960e32aae9cc96518c6ef5d78a18ce953ae4223 Mon Sep 17 00:00:00 2001 From: Marley Alford Date: Wed, 8 Apr 2026 15:37:34 +0200 Subject: [PATCH 01/10] feat: install vitest --- package-lock.json | 1508 ++++++++++++++++++++++++++++++++++++++++++++- package.json | 11 +- test/setup.js | 121 ++++ vitest.config.js | 27 + 4 files changed, 1662 insertions(+), 5 deletions(-) create mode 100644 test/setup.js create mode 100644 vitest.config.js diff --git a/package-lock.json b/package-lock.json index 0f66f2d..46e96b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,12 +12,31 @@ }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.0", + "@vitest/coverage-v8": "^3.2.4", + "@vitest/ui": "^3.2.4", + "@vue/test-utils": "^2.4.6", + "happy-dom": "^17.4.4", "vite": "^6.0.0", "vite-plugin-static-copy": "^2.1.0", "vite-plugin-web-extension": "^4.5.0", + "vitest": "^3.2.4", "vue": "^3.5.0" } }, + "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.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -89,6 +108,16 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@devicefarmer/adbkit": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/@devicefarmer/adbkit/-/adbkit-3.3.8.tgz", @@ -617,12 +646,115 @@ "url": "https://github.com/sponsors/kazupon" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -661,6 +793,24 @@ "node": ">= 8" } }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -706,6 +856,13 @@ "node": ">=12" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", @@ -1056,6 +1213,24 @@ "win32" ] }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1095,6 +1270,206 @@ "vue": "^3.2.25" } }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/coverage-v8/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.2.4.tgz", + "integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/utils": "3.2.4", + "fflate": "^0.8.2", + "flatted": "^3.3.3", + "pathe": "^2.0.3", + "sirv": "^3.0.1", + "tinyglobby": "^0.2.14", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "3.2.4" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@vue/compiler-core": { "version": "3.5.30", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz", @@ -1213,6 +1588,27 @@ "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", "license": "MIT" }, + "node_modules/@vue/test-utils": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.6.tgz", + "integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-beautify": "^1.14.9", + "vue-component-type-helpers": "^2.0.0" + } + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/adm-zip": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", @@ -1374,6 +1770,45 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -1510,6 +1945,16 @@ "dev": true, "license": "MIT" }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/camelcase": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", @@ -1523,6 +1968,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -1546,6 +2008,16 @@ "node": "*" } }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1603,8 +2075,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/commander": { - "version": "9.5.0", + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "9.5.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", "dev": true, @@ -1680,6 +2172,44 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/crypt": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", @@ -1758,6 +2288,16 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -1856,6 +2396,68 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/editorconfig": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz", + "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "^9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/emoji-regex": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", @@ -1886,6 +2488,13 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", @@ -1967,6 +2576,16 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "license": "MIT" }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.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", @@ -2046,6 +2665,13 @@ } } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -2079,6 +2705,30 @@ "node": ">=18" } }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fs-extra": { "version": "11.3.4", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", @@ -2153,6 +2803,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -2173,6 +2845,32 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/global-directory": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", @@ -2220,6 +2918,31 @@ "dev": true, "license": "MIT" }, + "node_modules/happy-dom": { + "version": "17.6.3", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-17.6.3.tgz", + "integrity": "sha512-UVIHeVhxmxedbWPCfgS55Jg2rDfwf2BCKeylcPSqazLz5w3Kri7Q4xdBJubsr/+VUzFLh0VjIvh13RaDA2/Xug==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "webidl-conversions": "^7.0.0", + "whatwg-mimetype": "^3.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/html-escaper": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", @@ -2498,6 +3221,115 @@ "node": ">=0.10.0" } }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-reports/node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2667,6 +3499,20 @@ "dev": true, "license": "MIT" }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -2676,6 +3522,34 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -2762,6 +3636,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2854,6 +3748,22 @@ "node": ">= 8" } }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -2928,7 +3838,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pako": { + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", @@ -2955,6 +3872,50 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2967,7 +3928,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3401,6 +4361,29 @@ "dev": true, "license": "MIT" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/shell-quote": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", @@ -3415,6 +4398,41 @@ "dev": true, "license": "MIT" }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -3490,6 +4508,20 @@ "node": ">= 10.x" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -3518,6 +4550,52 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", @@ -3534,6 +4612,30 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-5.0.0.tgz", @@ -3560,6 +4662,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/stubborn-fs": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", @@ -3577,6 +4699,73 @@ "dev": true, "license": "MIT" }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/thread-stream": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", @@ -3594,6 +4783,20 @@ "dev": true, "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -3611,6 +4814,36 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmp": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", @@ -3634,6 +4867,16 @@ "node": ">=8.0" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/type-fest": { "version": "3.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", @@ -3796,6 +5039,47 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/vite-plugin-static-copy": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.3.2.tgz", @@ -3856,6 +5140,98 @@ "node": ">=12" } }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/vue": { "version": "3.5.30", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz", @@ -3878,6 +5254,13 @@ } } }, + "node_modules/vue-component-type-helpers": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.2.12.tgz", + "integrity": "sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==", + "dev": true, + "license": "MIT" + }, "node_modules/vue-i18n": { "version": "9.14.5", "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.5.tgz", @@ -3960,6 +5343,26 @@ "dev": true, "license": "MPL-2.0" }, + "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-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/when": { "version": "3.7.7", "resolved": "https://registry.npmjs.org/when/-/when-3.7.7.tgz", @@ -3988,6 +5391,23 @@ "which": "bin/which" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/widest-line": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", @@ -4029,6 +5449,86 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/xdg-basedir": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", diff --git a/package.json b/package.json index 735c05a..5647fd8 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,20 @@ "scripts": { "dev": "vite build --watch", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest", + "test:ui": "vitest --ui", + "test:coverage": "vitest run --coverage" }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.0", + "@vitest/coverage-v8": "^3.2.4", + "@vitest/ui": "^3.2.4", + "@vue/test-utils": "^2.4.6", + "happy-dom": "^17.4.4", "vite": "^6.0.0", + "vitest": "^3.2.4", "vite-plugin-static-copy": "^2.1.0", "vite-plugin-web-extension": "^4.5.0", "vue": "^3.5.0" diff --git a/test/setup.js b/test/setup.js new file mode 100644 index 0000000..f227277 --- /dev/null +++ b/test/setup.js @@ -0,0 +1,121 @@ +import { afterEach, beforeEach, vi } from 'vitest' + +/** In-memory chrome.storage.local for the default global mock. */ +function createChromeMock() { + const localStore = Object.create(null) + + const storageOnChanged = { + listeners: [], + addListener(fn) { + this.listeners.push(fn) + }, + removeListener(fn) { + const i = this.listeners.indexOf(fn) + if (i !== -1) this.listeners.splice(i, 1) + }, + _emit(changes, areaName = 'local') { + for (const fn of this.listeners) { + try { + fn(changes, areaName) + } catch { + /* ignore listener errors in tests */ + } + } + }, + } + + return { + storage: { + onChanged: storageOnChanged, + local: { + get(keys, cb) { + const result = {} + if (keys == null) { + Object.assign(result, localStore) + } else if (typeof keys === 'string') { + if (localStore[keys] !== undefined) result[keys] = localStore[keys] + } else if (Array.isArray(keys)) { + for (const k of keys) { + if (localStore[k] !== undefined) result[k] = localStore[k] + } + } + const callback = typeof cb === 'function' ? cb : () => {} + queueMicrotask(() => callback(result)) + }, + set(items, cb) { + if (items && typeof items === 'object') { + const changes = {} + for (const [k, v] of Object.entries(items)) { + const oldValue = localStore[k] + localStore[k] = v + changes[k] = { oldValue, newValue: v } + } + queueMicrotask(() => { + storageOnChanged._emit(changes, 'local') + if (typeof cb === 'function') cb() + }) + } else if (typeof cb === 'function') { + queueMicrotask(() => cb()) + } + }, + remove(keys, cb) { + const keyList = typeof keys === 'string' ? [keys] : Array.isArray(keys) ? keys : [] + const changes = {} + for (const k of keyList) { + if (k in localStore) { + changes[k] = { oldValue: localStore[k], newValue: undefined } + delete localStore[k] + } + } + queueMicrotask(() => { + if (Object.keys(changes).length) storageOnChanged._emit(changes, 'local') + if (typeof cb === 'function') cb() + }) + }, + clear(cb) { + const changes = {} + for (const k of Object.keys(localStore)) { + changes[k] = { oldValue: localStore[k], newValue: undefined } + } + for (const k of Object.keys(localStore)) delete localStore[k] + queueMicrotask(() => { + storageOnChanged._emit(changes, 'local') + if (typeof cb === 'function') cb() + }) + }, + }, + }, + runtime: { + sendMessage: vi.fn(), + lastError: null, + getManifest: vi.fn(() => ({ manifest_version: 3 })), + id: 'test-extension-id', + onMessage: { + addListener: vi.fn(), + removeListener: vi.fn(), + }, + }, + tabs: { + query: vi.fn((_q, cb) => queueMicrotask(() => cb?.([]))), + get: vi.fn(), + update: vi.fn(), + onUpdated: { addListener: vi.fn(), removeListener: vi.fn() }, + onRemoved: { addListener: vi.fn(), removeListener: vi.fn() }, + }, + scripting: { + insertCSS: vi.fn(), + executeScript: vi.fn(), + }, + management: { + getSelf: vi.fn(), + }, + } +} + +beforeEach(() => { + globalThis.chrome = createChromeMock() +}) + +afterEach(() => { + vi.clearAllMocks() +}) diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 0000000..bdb67a7 --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,27 @@ +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + // Same as vite.config.js — vue-i18n JIT avoids new Function (MV3 CSP). + define: { + __INTLIFY_JIT_COMPILATION__: true, + }, + plugins: [vue()], + test: { + environment: 'happy-dom', + globals: true, + setupFiles: ['./test/setup.js'], + include: ['src/**/*.{test,spec}.{js,mjs}', 'test/**/*.{test,spec}.{js,mjs}'], + coverage: { + provider: 'v8', + reporter: ['text', 'html'], + include: ['src/**/*.{js,vue}'], + exclude: [ + '**/*.spec.js', + '**/*.test.js', + '**/node_modules/**', + 'dist/**', + ], + }, + }, +}) From 9389118f55e8869b07c8cabcc3658f87094bb355 Mon Sep 17 00:00:00 2001 From: Marley Alford Date: Wed, 8 Apr 2026 15:39:24 +0200 Subject: [PATCH 02/10] feat: creat mock utilities --- test/mocks/chrome.js | 175 +++++++++++++++++++++++++++++++++++++++++++ test/setup.js | 113 +--------------------------- 2 files changed, 176 insertions(+), 112 deletions(-) create mode 100644 test/mocks/chrome.js diff --git a/test/mocks/chrome.js b/test/mocks/chrome.js new file mode 100644 index 0000000..c12a54a --- /dev/null +++ b/test/mocks/chrome.js @@ -0,0 +1,175 @@ +import { vi } from 'vitest' + +/** + * `chrome.storage.onChanged` — matches MV3: listeners receive (changes, areaName). + * Use `_emit(changes, areaName)` in tests to simulate external updates. + */ +export function createStorageOnChangedMock() { + return { + listeners: [], + addListener(fn) { + this.listeners.push(fn) + }, + removeListener(fn) { + const i = this.listeners.indexOf(fn) + if (i !== -1) this.listeners.splice(i, 1) + }, + _emit(changes, areaName = 'local') { + for (const fn of this.listeners) { + try { + fn(changes, areaName) + } catch { + /* ignore listener errors in tests */ + } + } + }, + } +} + +/** + * In-memory `chrome.storage.local` that notifies `onChanged` on set/remove/clear. + * @param {Record} backing — mutable key bag (use Object.create(null)) + * @param {ReturnType} onChangedApi + */ +export function createStorageLocalMock(backing, onChangedApi) { + const emit = onChangedApi._emit.bind(onChangedApi) + + return { + get(keys, cb) { + const result = {} + if (keys == null) { + Object.assign(result, backing) + } else if (typeof keys === 'string') { + if (backing[keys] !== undefined) result[keys] = backing[keys] + } else if (Array.isArray(keys)) { + for (const k of keys) { + if (backing[k] !== undefined) result[k] = backing[k] + } + } + const callback = typeof cb === 'function' ? cb : () => {} + queueMicrotask(() => callback(result)) + }, + set(items, cb) { + if (items && typeof items === 'object') { + const changes = {} + for (const [k, v] of Object.entries(items)) { + const oldValue = backing[k] + backing[k] = v + changes[k] = { oldValue, newValue: v } + } + queueMicrotask(() => { + emit(changes, 'local') + if (typeof cb === 'function') cb() + }) + } else if (typeof cb === 'function') { + queueMicrotask(() => cb()) + } + }, + remove(keys, cb) { + const keyList = typeof keys === 'string' ? [keys] : Array.isArray(keys) ? keys : [] + const changes = {} + for (const k of keyList) { + if (k in backing) { + changes[k] = { oldValue: backing[k], newValue: undefined } + delete backing[k] + } + } + queueMicrotask(() => { + if (Object.keys(changes).length) emit(changes, 'local') + if (typeof cb === 'function') cb() + }) + }, + clear(cb) { + const changes = {} + for (const k of Object.keys(backing)) { + changes[k] = { oldValue: backing[k], newValue: undefined } + } + for (const k of Object.keys(backing)) delete backing[k] + queueMicrotask(() => { + emit(changes, 'local') + if (typeof cb === 'function') cb() + }) + }, + } +} + +/** + * Spy-friendly storage.local: same callbacks as the real API, but `get`/`set` are `vi.fn`. + * Backing store is shared with the stub implementation. + */ +export function createSpiedStorageLocalMock(backing, onChangedApi) { + const impl = createStorageLocalMock(backing, onChangedApi) + return { + get: vi.fn(impl.get), + set: vi.fn(impl.set), + remove: vi.fn(impl.remove), + clear: vi.fn(impl.clear), + } +} + +export function createRuntimeMock() { + return { + sendMessage: vi.fn(), + lastError: null, + getManifest: vi.fn(() => ({ manifest_version: 3 })), + id: 'test-extension-id', + onMessage: { + addListener: vi.fn(), + removeListener: vi.fn(), + }, + } +} + +export function createTabsMock() { + return { + query: vi.fn((_q, cb) => queueMicrotask(() => cb?.([]))), + get: vi.fn(), + update: vi.fn(), + onUpdated: { addListener: vi.fn(), removeListener: vi.fn() }, + onRemoved: { addListener: vi.fn(), removeListener: vi.fn() }, + } +} + +export function createScriptingMock() { + return { + insertCSS: vi.fn(), + executeScript: vi.fn(), + } +} + +export function createManagementMock() { + return { + getSelf: vi.fn(), + } +} + +/** + * Full default `chrome` tree for unit tests (popup / composables / background-style code). + * + * @param {object} [options] + * @param {Record} [options.backing] — shared in-memory bag for `storage.local` (default: fresh object) + * @param {ReturnType} [options.storageOnChanged] + * @param {boolean} [options.spyStorage] — wrap local get/set/remove/clear with `vi.fn` + * @param {ReturnType} [options.runtime] + * @param {ReturnType} [options.tabs] + */ +export function createChromeMock(options = {}) { + const backing = options.backing ?? Object.create(null) + const onChanged = + options.storageOnChanged ?? createStorageOnChangedMock() + const local = + options.spyStorage === true + ? createSpiedStorageLocalMock(backing, onChanged) + : createStorageLocalMock(backing, onChanged) + + return { + storage: { + onChanged, + local, + }, + runtime: options.runtime ?? createRuntimeMock(), + tabs: options.tabs ?? createTabsMock(), + scripting: options.scripting ?? createScriptingMock(), + management: options.management ?? createManagementMock(), + } +} diff --git a/test/setup.js b/test/setup.js index f227277..7916e2f 100644 --- a/test/setup.js +++ b/test/setup.js @@ -1,116 +1,5 @@ import { afterEach, beforeEach, vi } from 'vitest' - -/** In-memory chrome.storage.local for the default global mock. */ -function createChromeMock() { - const localStore = Object.create(null) - - const storageOnChanged = { - listeners: [], - addListener(fn) { - this.listeners.push(fn) - }, - removeListener(fn) { - const i = this.listeners.indexOf(fn) - if (i !== -1) this.listeners.splice(i, 1) - }, - _emit(changes, areaName = 'local') { - for (const fn of this.listeners) { - try { - fn(changes, areaName) - } catch { - /* ignore listener errors in tests */ - } - } - }, - } - - return { - storage: { - onChanged: storageOnChanged, - local: { - get(keys, cb) { - const result = {} - if (keys == null) { - Object.assign(result, localStore) - } else if (typeof keys === 'string') { - if (localStore[keys] !== undefined) result[keys] = localStore[keys] - } else if (Array.isArray(keys)) { - for (const k of keys) { - if (localStore[k] !== undefined) result[k] = localStore[k] - } - } - const callback = typeof cb === 'function' ? cb : () => {} - queueMicrotask(() => callback(result)) - }, - set(items, cb) { - if (items && typeof items === 'object') { - const changes = {} - for (const [k, v] of Object.entries(items)) { - const oldValue = localStore[k] - localStore[k] = v - changes[k] = { oldValue, newValue: v } - } - queueMicrotask(() => { - storageOnChanged._emit(changes, 'local') - if (typeof cb === 'function') cb() - }) - } else if (typeof cb === 'function') { - queueMicrotask(() => cb()) - } - }, - remove(keys, cb) { - const keyList = typeof keys === 'string' ? [keys] : Array.isArray(keys) ? keys : [] - const changes = {} - for (const k of keyList) { - if (k in localStore) { - changes[k] = { oldValue: localStore[k], newValue: undefined } - delete localStore[k] - } - } - queueMicrotask(() => { - if (Object.keys(changes).length) storageOnChanged._emit(changes, 'local') - if (typeof cb === 'function') cb() - }) - }, - clear(cb) { - const changes = {} - for (const k of Object.keys(localStore)) { - changes[k] = { oldValue: localStore[k], newValue: undefined } - } - for (const k of Object.keys(localStore)) delete localStore[k] - queueMicrotask(() => { - storageOnChanged._emit(changes, 'local') - if (typeof cb === 'function') cb() - }) - }, - }, - }, - runtime: { - sendMessage: vi.fn(), - lastError: null, - getManifest: vi.fn(() => ({ manifest_version: 3 })), - id: 'test-extension-id', - onMessage: { - addListener: vi.fn(), - removeListener: vi.fn(), - }, - }, - tabs: { - query: vi.fn((_q, cb) => queueMicrotask(() => cb?.([]))), - get: vi.fn(), - update: vi.fn(), - onUpdated: { addListener: vi.fn(), removeListener: vi.fn() }, - onRemoved: { addListener: vi.fn(), removeListener: vi.fn() }, - }, - scripting: { - insertCSS: vi.fn(), - executeScript: vi.fn(), - }, - management: { - getSelf: vi.fn(), - }, - } -} +import { createChromeMock } from './mocks/chrome.js' beforeEach(() => { globalThis.chrome = createChromeMock() From d0cb1f46356b3268796220564fc2b401d8ab02ac Mon Sep 17 00:00:00 2001 From: Marley Alford Date: Wed, 8 Apr 2026 15:42:48 +0200 Subject: [PATCH 03/10] feat: add test for use useStats --- src/composables/useStats.js | 4 +- src/composables/useStats.spec.js | 167 +++++++++++++++++++++++++++++++ vitest.config.js | 3 + 3 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 src/composables/useStats.spec.js diff --git a/src/composables/useStats.js b/src/composables/useStats.js index acc084b..40e948f 100644 --- a/src/composables/useStats.js +++ b/src/composables/useStats.js @@ -18,7 +18,7 @@ function calendarDaysDiff(earlierStr, laterStr) { } /** Days without unblocking avoid sites: current and longest (calendar-day based). */ -function computeStreaks(events) { +export function computeStreaks(events) { const today = todayStr() if (!events.length) return { current: 0, longest: 0 } @@ -48,7 +48,7 @@ function computeStreaks(events) { * Consecutive calendar days with at least one visit_site_clicked for this host. * Current streak is 0 if neither today nor yesterday was visited (missed a day). */ -function computeVisitStreakForHost(events, host) { +export function computeVisitStreakForHost(events, host) { const today = todayStr() const yesterday = toDateStr(Date.now() - 24 * 60 * 60 * 1000) const visitDays = [ diff --git a/src/composables/useStats.spec.js b/src/composables/useStats.spec.js new file mode 100644 index 0000000..56556fe --- /dev/null +++ b/src/composables/useStats.spec.js @@ -0,0 +1,167 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' +import { + computeStreaks, + computeVisitStreakForHost, + computeExportSummary, +} from './useStats.js' + +/** Fixed instant so `toLocaleDateString('en-CA')` is stable with `TZ=UTC` in vitest.config. */ +function utcNoon(y, month, day) { + return new Date(Date.UTC(y, month - 1, day, 12, 0, 0)) +} + +describe('computeStreaks', () => { + afterEach(() => { + vi.useRealTimers() + }) + + it('returns 0 for empty events', () => { + vi.useFakeTimers({ toFake: ['Date'] }) + vi.setSystemTime(utcNoon(2026, 4, 8)) + expect(computeStreaks([])).toEqual({ current: 0, longest: 0 }) + }) + + it('current streak is 0 if unblocked today', () => { + vi.useFakeTimers({ toFake: ['Date'] }) + vi.setSystemTime(utcNoon(2026, 4, 8)) + const events = [ + { type: 'unblock', ts: utcNoon(2026, 4, 8).getTime(), host: 'example.com' }, + ] + expect(computeStreaks(events).current).toBe(0) + }) + + it('counts calendar days since first event when there is no unblock', () => { + vi.useFakeTimers({ toFake: ['Date'] }) + vi.setSystemTime(utcNoon(2026, 4, 10)) + const events = [ + { type: 'overlay_shown', ts: utcNoon(2026, 4, 8).getTime(), host: 'example.com' }, + ] + const { current, longest } = computeStreaks(events) + expect(current).toBe(2) + expect(longest).toBe(2) + }) + + it('current streak is days since last unblock when last unblock is not today', () => { + vi.useFakeTimers({ toFake: ['Date'] }) + vi.setSystemTime(utcNoon(2026, 4, 10)) + const events = [ + { type: 'unblock', ts: utcNoon(2026, 4, 6).getTime(), host: 'example.com' }, + { type: 'overlay_shown', ts: utcNoon(2026, 4, 10).getTime(), host: 'example.com' }, + ] + expect(computeStreaks(events).current).toBe(4) + }) + + it('longest run is max gap between unblocks and trailing streak', () => { + vi.useFakeTimers({ toFake: ['Date'] }) + vi.setSystemTime(utcNoon(2026, 4, 20)) + const events = [ + { type: 'unblock', ts: utcNoon(2026, 4, 10).getTime(), host: 'a.com' }, + { type: 'unblock', ts: utcNoon(2026, 4, 15).getTime(), host: 'a.com' }, + { type: 'overlay_shown', ts: utcNoon(2026, 4, 20).getTime(), host: 'a.com' }, + ] + const { current, longest } = computeStreaks(events) + expect(current).toBe(5) + expect(longest).toBe(5) + }) + + it('treats multiple unblocks on the same calendar day as one unblock day', () => { + vi.useFakeTimers({ toFake: ['Date'] }) + vi.setSystemTime(utcNoon(2026, 4, 10)) + const morning = utcNoon(2026, 4, 8).getTime() + const evening = morning + 6 * 60 * 60 * 1000 + const events = [ + { type: 'unblock', ts: morning, host: 'x.com' }, + { type: 'unblock', ts: evening, host: 'x.com' }, + ] + expect(computeStreaks(events).current).toBe(2) + }) +}) + +describe('computeVisitStreakForHost', () => { + afterEach(() => { + vi.useRealTimers() + }) + + it('returns 0 for no visits', () => { + vi.useFakeTimers({ toFake: ['Date'] }) + vi.setSystemTime(utcNoon(2026, 4, 8)) + expect(computeVisitStreakForHost([], 'example.com')).toEqual({ + current: 0, + longest: 0, + }) + }) + + it('counts consecutive days with visits ending today', () => { + vi.useFakeTimers({ toFake: ['Date'] }) + vi.setSystemTime(utcNoon(2026, 4, 10)) + const events = [ + { type: 'visit_site_clicked', ts: utcNoon(2026, 4, 10).getTime(), host: 'good.com' }, + { type: 'visit_site_clicked', ts: utcNoon(2026, 4, 9).getTime(), host: 'good.com' }, + ] + expect(computeVisitStreakForHost(events, 'good.com').current).toBe(2) + }) + + it('anchors on yesterday when today has no visit but yesterday does', () => { + vi.useFakeTimers({ toFake: ['Date'] }) + vi.setSystemTime(utcNoon(2026, 4, 10)) + const events = [ + { type: 'visit_site_clicked', ts: utcNoon(2026, 4, 9).getTime(), host: 'good.com' }, + { type: 'visit_site_clicked', ts: utcNoon(2026, 4, 8).getTime(), host: 'good.com' }, + ] + expect(computeVisitStreakForHost(events, 'good.com').current).toBe(2) + }) + + it('current is 0 when neither today nor yesterday was visited', () => { + vi.useFakeTimers({ toFake: ['Date'] }) + vi.setSystemTime(utcNoon(2026, 4, 10)) + const events = [ + { type: 'visit_site_clicked', ts: utcNoon(2026, 4, 5).getTime(), host: 'good.com' }, + ] + expect(computeVisitStreakForHost(events, 'good.com').current).toBe(0) + }) + + it('breaks current run when a day in the streak is missing', () => { + vi.useFakeTimers({ toFake: ['Date'] }) + vi.setSystemTime(utcNoon(2026, 4, 10)) + const events = [ + { type: 'visit_site_clicked', ts: utcNoon(2026, 4, 10).getTime(), host: 'good.com' }, + { type: 'visit_site_clicked', ts: utcNoon(2026, 4, 8).getTime(), host: 'good.com' }, + ] + expect(computeVisitStreakForHost(events, 'good.com').current).toBe(1) + }) + + it('computes longest consecutive visit run', () => { + vi.useFakeTimers({ toFake: ['Date'] }) + vi.setSystemTime(utcNoon(2026, 4, 20)) + const events = [ + { type: 'visit_site_clicked', ts: utcNoon(2026, 4, 1).getTime(), host: 'good.com' }, + { type: 'visit_site_clicked', ts: utcNoon(2026, 4, 2).getTime(), host: 'good.com' }, + { type: 'visit_site_clicked', ts: utcNoon(2026, 4, 3).getTime(), host: 'good.com' }, + { type: 'visit_site_clicked', ts: utcNoon(2026, 4, 10).getTime(), host: 'good.com' }, + ] + expect(computeVisitStreakForHost(events, 'good.com').longest).toBe(3) + }) +}) + +describe('computeExportSummary', () => { + afterEach(() => { + vi.useRealTimers() + }) + + it('aggregates global and per-list streaks', () => { + vi.useFakeTimers({ toFake: ['Date'] }) + vi.setSystemTime(utcNoon(2026, 4, 10)) + const events = [ + { type: 'unblock', ts: utcNoon(2026, 4, 6).getTime(), host: 'bad.com' }, + { type: 'visit_site_clicked', ts: utcNoon(2026, 4, 9).getTime(), host: 'good.com' }, + { type: 'visit_site_clicked', ts: utcNoon(2026, 4, 10).getTime(), host: 'good.com' }, + ] + const summary = computeExportSummary(events, ['bad.com'], ['good.com']) + expect(summary.current_streak).toBe(4) + expect(summary.by_url_avoid).toHaveLength(1) + expect(summary.by_url_avoid[0].site).toBe('bad.com') + expect(summary.by_url_visit).toHaveLength(1) + expect(summary.by_url_visit[0].site).toBe('good.com') + expect(summary.by_url_visit[0].current_streak).toBe(2) + }) +}) diff --git a/vitest.config.js b/vitest.config.js index bdb67a7..a0c2848 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -11,6 +11,9 @@ export default defineConfig({ environment: 'happy-dom', globals: true, setupFiles: ['./test/setup.js'], + env: { + TZ: 'UTC', + }, include: ['src/**/*.{test,spec}.{js,mjs}', 'test/**/*.{test,spec}.{js,mjs}'], coverage: { provider: 'v8', From f46ec4f0be0b252704ee55fdc71166d6a53ab0d6 Mon Sep 17 00:00:00 2001 From: Marley Alford Date: Wed, 8 Apr 2026 15:44:35 +0200 Subject: [PATCH 04/10] featuse: add tests for useStorage --- src/composables/useStorage.spec.js | 116 +++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 src/composables/useStorage.spec.js diff --git a/src/composables/useStorage.spec.js b/src/composables/useStorage.spec.js new file mode 100644 index 0000000..be3b7f5 --- /dev/null +++ b/src/composables/useStorage.spec.js @@ -0,0 +1,116 @@ +import { describe, it, expect, vi } from 'vitest' +import { defineComponent } from 'vue' +import { mount, flushPromises } from '@vue/test-utils' +import { useStorage, DEFAULTS } from './useStorage.js' +import { createChromeMock } from '../../test/mocks/chrome.js' + +function mountUseStorage(keys) { + return mount( + defineComponent({ + setup() { + return useStorage(keys) + }, + template: '
', + }), + ) +} + +describe('useStorage', () => { + it('loads default values when storage has no keys', async () => { + const wrapper = mountUseStorage(['avoid', 'visit', 'theme']) + await flushPromises() + expect(wrapper.vm.avoid).toEqual(DEFAULTS.avoid) + expect(wrapper.vm.visit).toEqual(DEFAULTS.visit) + expect(wrapper.vm.theme).toBe(DEFAULTS.theme) + wrapper.unmount() + }) + + it('loads persisted values from chrome.storage.local', async () => { + const backing = Object.create(null) + backing.avoid = ['twitter.com'] + backing.theme = 'orange' + globalThis.chrome = createChromeMock({ backing }) + + const wrapper = mountUseStorage(['avoid', 'theme']) + await flushPromises() + expect(wrapper.vm.avoid).toEqual(['twitter.com']) + expect(wrapper.vm.theme).toBe('orange') + wrapper.unmount() + }) + + it('set() persists and updates refs via onChanged', async () => { + const wrapper = mountUseStorage(['avoid']) + await flushPromises() + + await wrapper.vm.set({ avoid: ['a.com', 'b.com'] }) + await flushPromises() + + expect(wrapper.vm.avoid).toEqual(['a.com', 'b.com']) + wrapper.unmount() + }) + + it('reflects updates made only through storage.local.set', async () => { + const wrapper = mountUseStorage(['visit']) + await flushPromises() + + await new Promise((resolve) => { + chrome.storage.local.set({ visit: ['good.com'] }, resolve) + }) + await flushPromises() + + expect(wrapper.vm.visit).toEqual(['good.com']) + wrapper.unmount() + }) + + it('remove() clears keys and refs fall back to defaults', async () => { + const wrapper = mountUseStorage(['avoid', 'visit']) + await flushPromises() + await wrapper.vm.set({ avoid: ['x.com'], visit: ['y.com'] }) + await flushPromises() + + await wrapper.vm.remove(['avoid', 'visit']) + await flushPromises() + + expect(wrapper.vm.avoid).toEqual([]) + expect(wrapper.vm.visit).toEqual([]) + wrapper.unmount() + }) + + it('refresh() reloads tracked keys from storage', async () => { + const backing = Object.create(null) + globalThis.chrome = createChromeMock({ backing }) + const wrapper = mountUseStorage(['avoid']) + await flushPromises() + expect(wrapper.vm.avoid).toEqual([]) + + backing.avoid = ['injected.com'] + wrapper.vm.refresh() + await flushPromises() + expect(wrapper.vm.avoid).toEqual(['injected.com']) + wrapper.unmount() + }) + + it('ignores storage.onChanged when areaName is not local', async () => { + const wrapper = mountUseStorage(['avoid']) + await flushPromises() + await wrapper.vm.set({ avoid: ['keep.com'] }) + await flushPromises() + + chrome.storage.onChanged._emit( + { avoid: { oldValue: ['keep.com'], newValue: ['other.com'] } }, + 'sync', + ) + await flushPromises() + + expect(wrapper.vm.avoid).toEqual(['keep.com']) + wrapper.unmount() + }) + + it('removeListener is called on unmount', async () => { + const removeListener = vi.spyOn(chrome.storage.onChanged, 'removeListener') + const wrapper = mountUseStorage(['avoid']) + await flushPromises() + wrapper.unmount() + expect(removeListener).toHaveBeenCalled() + }) +}) From b66eca687a33aa7ad078e99c3d93fc2306c82a53 Mon Sep 17 00:00:00 2001 From: Marley Alford Date: Wed, 8 Apr 2026 15:46:37 +0200 Subject: [PATCH 05/10] feat: add tests for useTracker --- src/composables/useTracker.spec.js | 97 ++++++++++++++++++++++++++++++ test/mocks/chrome.js | 68 ++++++++++++++------- 2 files changed, 143 insertions(+), 22 deletions(-) create mode 100644 src/composables/useTracker.spec.js diff --git a/src/composables/useTracker.spec.js b/src/composables/useTracker.spec.js new file mode 100644 index 0000000..b4d7096 --- /dev/null +++ b/src/composables/useTracker.spec.js @@ -0,0 +1,97 @@ +import { describe, it, expect, afterEach, vi } from 'vitest' +import { useTracker } from './useTracker.js' +import { createChromeMock } from '../../test/mocks/chrome.js' + +const STORAGE_KEY = 'foqusEvents' + +describe('useTracker', () => { + afterEach(() => { + vi.useRealTimers() + }) + + it('getEvents resolves to empty array when log is missing', async () => { + const tracker = useTracker() + const events = await tracker.getEvents() + expect(events).toEqual([]) + }) + + it('overlayShown appends overlay_shown with host and ts', async () => { + vi.useFakeTimers({ toFake: ['Date'] }) + vi.setSystemTime(new Date('2026-05-01T12:00:00Z')) + const tracker = useTracker() + await tracker.overlayShown('example.com') + + const events = await tracker.getEvents() + expect(events).toHaveLength(1) + expect(events[0]).toMatchObject({ + type: 'overlay_shown', + host: 'example.com', + ts: new Date('2026-05-01T12:00:00Z').getTime(), + }) + expect(events[0].meta).toBeUndefined() + }) + + it('intentionKept appends intention_kept', async () => { + const tracker = useTracker() + await tracker.intentionKept('a.com') + + const events = await tracker.getEvents() + expect(events[0].type).toBe('intention_kept') + expect(events[0].host).toBe('a.com') + }) + + it('unblock appends unblock with meta.minutes', async () => { + const tracker = useTracker() + await tracker.unblock('distraction.com', 15) + + const events = await tracker.getEvents() + expect(events[0]).toMatchObject({ + type: 'unblock', + host: 'distraction.com', + meta: { minutes: 15 }, + }) + }) + + it('visitSiteClicked appends visit_site_clicked', async () => { + const tracker = useTracker() + await tracker.visitSiteClicked('good.com') + + const events = await tracker.getEvents() + expect(events[0].type).toBe('visit_site_clicked') + expect(events[0].host).toBe('good.com') + }) + + it('records events in order when awaited sequentially', async () => { + const tracker = useTracker() + await tracker.overlayShown('host') + await tracker.intentionKept('host') + + const events = await tracker.getEvents() + expect(events.map((e) => e.type)).toEqual(['overlay_shown', 'intention_kept']) + }) + + it('drops events older than the retention window on write', async () => { + vi.useFakeTimers({ toFake: ['Date'] }) + const now = new Date('2026-06-01T12:00:00Z').getTime() + vi.setSystemTime(now) + + const ninetyOneDays = 91 * 24 * 60 * 60 * 1000 + const oldTs = now - ninetyOneDays + const recentTs = now - 24 * 60 * 60 * 1000 + + const backing = Object.create(null) + backing[STORAGE_KEY] = [ + { type: 'unblock', ts: oldTs, host: 'ancient.com' }, + { type: 'overlay_shown', ts: recentTs, host: 'keep.com' }, + ] + globalThis.chrome = createChromeMock({ backing }) + + const tracker = useTracker() + await tracker.visitSiteClicked('new.com') + + const events = await tracker.getEvents() + expect(events.find((e) => e.host === 'ancient.com')).toBeUndefined() + expect(events.find((e) => e.host === 'keep.com')).toBeTruthy() + expect(events.find((e) => e.host === 'new.com')).toBeTruthy() + }) +}) diff --git a/test/mocks/chrome.js b/test/mocks/chrome.js index c12a54a..41c39a8 100644 --- a/test/mocks/chrome.js +++ b/test/mocks/chrome.js @@ -34,20 +34,28 @@ export function createStorageOnChangedMock() { export function createStorageLocalMock(backing, onChangedApi) { const emit = onChangedApi._emit.bind(onChangedApi) + function buildGetResult(keys) { + const result = {} + if (keys == null) { + Object.assign(result, backing) + } else if (typeof keys === 'string') { + if (backing[keys] !== undefined) result[keys] = backing[keys] + } else if (Array.isArray(keys)) { + for (const k of keys) { + if (backing[k] !== undefined) result[k] = backing[k] + } + } + return result + } + return { get(keys, cb) { - const result = {} - if (keys == null) { - Object.assign(result, backing) - } else if (typeof keys === 'string') { - if (backing[keys] !== undefined) result[keys] = backing[keys] - } else if (Array.isArray(keys)) { - for (const k of keys) { - if (backing[k] !== undefined) result[k] = backing[k] - } + const result = buildGetResult(keys) + if (typeof cb === 'function') { + queueMicrotask(() => cb(result)) + return undefined } - const callback = typeof cb === 'function' ? cb : () => {} - queueMicrotask(() => callback(result)) + return Promise.resolve(result) }, set(items, cb) { if (items && typeof items === 'object') { @@ -57,13 +65,23 @@ export function createStorageLocalMock(backing, onChangedApi) { backing[k] = v changes[k] = { oldValue, newValue: v } } - queueMicrotask(() => { - emit(changes, 'local') - if (typeof cb === 'function') cb() + return new Promise((resolve) => { + queueMicrotask(() => { + emit(changes, 'local') + if (typeof cb === 'function') cb() + resolve() + }) + }) + } + if (typeof cb === 'function') { + return new Promise((resolve) => { + queueMicrotask(() => { + cb() + resolve() + }) }) - } else if (typeof cb === 'function') { - queueMicrotask(() => cb()) } + return Promise.resolve() }, remove(keys, cb) { const keyList = typeof keys === 'string' ? [keys] : Array.isArray(keys) ? keys : [] @@ -74,9 +92,12 @@ export function createStorageLocalMock(backing, onChangedApi) { delete backing[k] } } - queueMicrotask(() => { - if (Object.keys(changes).length) emit(changes, 'local') - if (typeof cb === 'function') cb() + return new Promise((resolve) => { + queueMicrotask(() => { + if (Object.keys(changes).length) emit(changes, 'local') + if (typeof cb === 'function') cb() + resolve() + }) }) }, clear(cb) { @@ -85,9 +106,12 @@ export function createStorageLocalMock(backing, onChangedApi) { changes[k] = { oldValue: backing[k], newValue: undefined } } for (const k of Object.keys(backing)) delete backing[k] - queueMicrotask(() => { - emit(changes, 'local') - if (typeof cb === 'function') cb() + return new Promise((resolve) => { + queueMicrotask(() => { + emit(changes, 'local') + if (typeof cb === 'function') cb() + resolve() + }) }) }, } From 187c93a59d05486ae35a2fabf7063d1baf60dd9a Mon Sep 17 00:00:00 2001 From: Marley Alford Date: Wed, 8 Apr 2026 15:49:38 +0200 Subject: [PATCH 06/10] feat: add tests for App component --- src/popup/App.spec.js | 113 ++++++++++++++++++++++++++++++++++++++++++ src/popup/App.vue | 2 + 2 files changed, 115 insertions(+) create mode 100644 src/popup/App.spec.js diff --git a/src/popup/App.spec.js b/src/popup/App.spec.js new file mode 100644 index 0000000..900cd84 --- /dev/null +++ b/src/popup/App.spec.js @@ -0,0 +1,113 @@ +import { describe, it, expect, vi } from 'vitest' +import { mount, flushPromises } from '@vue/test-utils' +import App from './App.vue' +import { i18n } from '../i18n/index.js' +import { createChromeMock } from '../../test/mocks/chrome.js' + +function mountPopup(options = {}) { + return mount(App, { + attachTo: document.body, + global: { + plugins: [i18n], + ...options.global, + }, + ...options, + }) +} + +describe('popup App', () => { + it('shows onboarding hint for new users', async () => { + const wrapper = mountPopup() + await flushPromises() + + const hints = wrapper.findAll('.popup-onboarding-hint') + expect(hints.length).toBeGreaterThan(0) + expect(wrapper.text()).toContain('Add sites to') + expect(wrapper.text()).toContain('Avoid') + expect(wrapper.text()).toContain('Visit') + + wrapper.unmount() + }) + + it('hides onboarding hint after dismiss', async () => { + const wrapper = mountPopup() + await flushPromises() + + await wrapper.find('.popup-onboarding-hint-dismiss').trigger('click') + await flushPromises() + + expect(wrapper.find('.popup-onboarding-hint').exists()).toBe(false) + wrapper.unmount() + }) + + it('does not show onboarding hint when already dismissed', async () => { + const backing = Object.create(null) + backing.descriptionBannerDismissed = true + globalThis.chrome = createChromeMock({ backing }) + + const wrapper = mountPopup() + await flushPromises() + + expect(wrapper.find('.popup-onboarding-hint').exists()).toBe(false) + wrapper.unmount() + }) + + it('adds a site to the avoid list from the form', async () => { + const backing = Object.create(null) + backing.descriptionBannerDismissed = true + globalThis.chrome = createChromeMock({ backing }) + + const wrapper = mountPopup() + await flushPromises() + + const input = wrapper.find('#site-input-avoid') + await input.setValue('twitter.com') + await wrapper.find('.popup-tab-panel--avoid form').trigger('submit') + await flushPromises() + + expect(wrapper.text()).toContain('twitter.com') + const items = wrapper.findAll('#avoid-sites-list .popup-list-item-text') + expect(items.length).toBe(1) + expect(items[0].text()).toBe('twitter.com') + + wrapper.unmount() + }) + + it('does not duplicate an avoid site already on the list', async () => { + const backing = Object.create(null) + backing.descriptionBannerDismissed = true + backing.avoid = ['twitter.com'] + globalThis.chrome = createChromeMock({ backing }) + + const wrapper = mountPopup() + await flushPromises() + + const input = wrapper.find('#site-input-avoid') + await input.setValue('twitter.com') + await wrapper.find('.popup-tab-panel--avoid form').trigger('submit') + await flushPromises() + + const items = wrapper.findAll('#avoid-sites-list .popup-list-item-text') + expect(items.length).toBe(1) + + wrapper.unmount() + }) + + it('prefills avoid input from the active tab hostname', async () => { + const backing = Object.create(null) + backing.descriptionBannerDismissed = true + globalThis.chrome = createChromeMock({ backing }) + chrome.tabs.query = vi.fn((_q, cb) => { + queueMicrotask(() => cb([{ url: 'https://facebook.com/newsfeed' }])) + }) + + const wrapper = mountPopup() + await flushPromises() + await flushPromises() + + const input = wrapper.find('#site-input-avoid') + expect(input.element.value).toBe('facebook.com') + + wrapper.unmount() + }) +}) diff --git a/src/popup/App.vue b/src/popup/App.vue index 85c2bfc..b1354a8 100644 --- a/src/popup/App.vue +++ b/src/popup/App.vue @@ -174,6 +174,7 @@ onMounted(() => { keypath="popup.onboardingHint" tag="p" class="popup-onboarding-hint-text" + scope="global" >