diff --git a/package-lock.json b/package-lock.json index 938600165..7ba5cd80c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -142,6 +142,7 @@ "terminal": "^0.1.4", "ts-loader": "^9.5.7", "typescript": "^5.9.3", + "vitest": "^4.1.10", "vscode-languageserver-types": "^3.17.5", "ws": "^8.21.0" } @@ -3801,6 +3802,16 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@parcel/watcher": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", @@ -4107,6 +4118,311 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rspack/binding": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-2.1.5.tgz", @@ -4450,6 +4766,13 @@ "integrity": "sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw==", "license": "ISC" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@szmarczak/http-timer": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", @@ -4513,6 +4836,27 @@ "dev": true, "license": "MIT" }, + "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/chai/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/@types/cordova": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-11.0.3.tgz", @@ -4773,13 +5117,19 @@ "@types/d3-selection": "*" } }, + "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.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/gensync": { "version": "1.0.5", @@ -4826,59 +5176,182 @@ "@types/mdurl": "^2" } }, - "node_modules/@types/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/url-parse": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@types/url-parse/-/url-parse-1.4.11.tgz", + "integrity": "sha512-FKvKIqRaykZtd4n47LbK/W/5fhQQ1X7cxxzG9A48h0BGN+S04NH7ervcCjM8tyR0lyGru83FAHSmw2ObgKoESg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/custom-elements": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/custom-elements/-/custom-elements-1.3.0.tgz", + "integrity": "sha512-f4q/s76+8nOy+fhrNHyetuoPDR01lmlZB5czfCG+OOnBw/Wf+x48DcCDPmMQY7oL8xYFL8qfenMoiS8DUkKBUw==", + "license": "ISC" + }, + "node_modules/@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/expect/node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, "license": "MIT", - "peer": true + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@types/node": { - "version": "26.1.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", - "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "undici-types": "~8.3.0" + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, "license": "MIT", - "optional": true + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@types/url-parse": { - "version": "1.4.11", - "resolved": "https://registry.npmjs.org/@types/url-parse/-/url-parse-1.4.11.tgz", - "integrity": "sha512-FKvKIqRaykZtd4n47LbK/W/5fhQQ1X7cxxzG9A48h0BGN+S04NH7ervcCjM8tyR0lyGru83FAHSmw2ObgKoESg==", + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", "dev": true, - "license": "MIT" - }, - "node_modules/@ungap/custom-elements": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/custom-elements/-/custom-elements-1.3.0.tgz", - "integrity": "sha512-f4q/s76+8nOy+fhrNHyetuoPDR01lmlZB5czfCG+OOnBw/Wf+x48DcCDPmMQY7oL8xYFL8qfenMoiS8DUkKBUw==", - "license": "ISC" + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@ungap/promise-all-settled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", - "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", "dev": true, - "license": "ISC" - }, - "node_modules/@upsetjs/venn.js": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", - "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", "license": "MIT", - "optionalDependencies": { - "d3-selection": "^3.0.0", - "d3-transition": "^3.0.1" + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, "node_modules/@vscode/l10n": { @@ -8313,7 +8786,6 @@ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -8662,8 +9134,7 @@ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.2", @@ -8791,6 +9262,16 @@ "node": ">=4.0" } }, + "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/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -8854,6 +9335,16 @@ "node": ">= 0.8.0" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -11020,68 +11511,341 @@ "https://opencollective.com/katex", "https://github.com/sponsors/katex" ], - "license": "MIT", - "dependencies": { - "commander": "^8.3.0" + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.0" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, + "node_modules/latest-version": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", + "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-json": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" }, - "bin": { - "katex": "cli.js" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/keyv": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", - "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.0" + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/khroma": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", - "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" - }, - "node_modules/latest-version": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", - "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "package-json": "^6.3.0" - }, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/layout-base": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", - "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", - "license": "MIT" - }, - "node_modules/lazystream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", - "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "^2.0.5" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.6.3" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lines-and-columns": { @@ -11268,6 +12032,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -13095,6 +13869,13 @@ "node": ">=8" } }, + "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": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", @@ -14317,6 +15098,40 @@ "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", "license": "Unlicense" }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, "node_modules/roughjs": { "version": "4.6.6", "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", @@ -14738,6 +15553,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -14944,6 +15766,13 @@ "node": "^20.17.0 || >=22.9.0" } }, + "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/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -14954,6 +15783,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "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", @@ -15232,6 +16068,13 @@ "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": "1.2.4", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", @@ -15257,6 +16100,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmp": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", @@ -15837,6 +16690,174 @@ "dev": true, "license": "MIT" }, + "node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, "node_modules/vscode-css-languageservice": { "version": "6.3.10", "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.10.tgz", @@ -16047,6 +17068,23 @@ "node": "^20.17.0 || >=22.9.0" } }, + "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/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", diff --git a/package.json b/package.json index 34b717cce..316584c52 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,9 @@ "format": "biome format --write", "check": "biome check --write", "typecheck": "tsc --noEmit", - "updateAce": "node ./utils/updateAce.js" + "updateAce": "node ./utils/updateAce.js", + "test": "vitest run", + "test:watch": "vitest" }, "keywords": [ "ecosystem:cordova" @@ -112,6 +114,7 @@ "terminal": "^0.1.4", "ts-loader": "^9.5.7", "typescript": "^5.9.3", + "vitest": "^4.1.10", "vscode-languageserver-types": "^3.17.5", "ws": "^8.21.0" }, diff --git a/tests/unit/binaryExtensions.test.js b/tests/unit/binaryExtensions.test.js new file mode 100644 index 000000000..1999c1d55 --- /dev/null +++ b/tests/unit/binaryExtensions.test.js @@ -0,0 +1,81 @@ +import { describe, expect, it } from "vitest"; +import { + isBinaryFile, + isBinaryMime, + isBinaryPath, + isTextPath, +} from "utils/binaryExtensions"; + +describe("isBinaryPath", () => { + it("detects binary extensions", () => { + expect(isBinaryPath("photo.jpg")).toBe(true); + expect(isBinaryPath("downloads/app.apk")).toBe(true); + }); + + it("is case-insensitive", () => { + expect(isBinaryPath("IMAGE.PNG")).toBe(true); + }); + + it("detects compound extensions", () => { + expect(isBinaryPath("archive.tar.gz")).toBe(true); + }); + + it("ignores query strings and hashes", () => { + expect(isBinaryPath("https://x.com/a.zip?dl=1")).toBe(true); + }); + + it("returns false for text or extensionless paths", () => { + expect(isBinaryPath("script.js")).toBe(false); + expect(isBinaryPath("README")).toBe(false); + }); +}); + +describe("isTextPath", () => { + it("detects text extensions", () => { + expect(isTextPath("notes.txt")).toBe(true); + expect(isTextPath("src/main.js")).toBe(true); + }); + + it("returns false for binary or extensionless paths", () => { + expect(isTextPath("app.apk")).toBe(false); + expect(isTextPath("Makefile")).toBe(false); + }); +}); + +describe("isBinaryMime", () => { + it("detects binary mime types", () => { + expect(isBinaryMime("application/zip")).toBe(true); + expect(isBinaryMime("application/octet-stream")).toBe(true); + }); + + it("returns false for text mime types", () => { + expect(isBinaryMime("text/plain")).toBe(false); + expect(isBinaryMime("text/html; charset=utf-8")).toBe(false); + }); + + it("returns false for empty input", () => { + expect(isBinaryMime("")).toBe(false); + expect(isBinaryMime(null)).toBe(false); + }); +}); + +describe("isBinaryFile", () => { + it("accepts a plain path string", () => { + expect(isBinaryFile("x.png")).toBe(true); + expect(isBinaryFile("x.txt")).toBe(false); + }); + + it("returns false for falsy input", () => { + expect(isBinaryFile(null)).toBe(false); + expect(isBinaryFile(undefined)).toBe(false); + }); + + it("detects binary files by extension", () => { + expect(isBinaryFile({ name: "photo.jpg" })).toBe(true); + }); + + it("gives text mime types precedence over the extension", () => { + expect(isBinaryFile({ mime: "text/csv", url: "file.apk" })).toBe(false); + expect(isBinaryFile({ type: "text/plain", name: "x.png" })).toBe(false); + }); +}); diff --git a/tests/unit/color.test.js b/tests/unit/color.test.js new file mode 100644 index 000000000..d7e00acbb --- /dev/null +++ b/tests/unit/color.test.js @@ -0,0 +1,103 @@ +import { describe, expect, it } from "vitest"; +import Hex from "utils/color/hex"; +import Hsl from "utils/color/hsl"; +import { isValidColor } from "utils/color/regex"; +import Rgb from "utils/color/rgb"; + +describe("Rgb", () => { + it("stringifies opaque colors as rgb()", () => { + expect(new Rgb(255, 0, 0).toString()).toBe("rgb(255, 0, 0)"); + }); + + it("stringifies translucent colors as rgba()", () => { + expect(new Rgb(255, 0, 0, 0.5).toString()).toBe("rgba(255, 0, 0, 0.5)"); + }); + + it("can force the alpha channel on or off", () => { + expect(new Rgb(255, 0, 0).toString(true)).toBe("rgba(255, 0, 0, 1)"); + expect(new Rgb(255, 0, 0, 0.5).toString(false)).toBe("rgb(255, 0, 0)"); + }); +}); + +describe("Hex", () => { + it("stringifies colors as uppercase hex", () => { + expect(new Hex(255, 0, 0, 255).toString()).toBe("#FF0000"); + expect(new Hex(0, 128, 255, 255).toString()).toBe("#0080FF"); + }); + + it("includes the alpha channel when it is not opaque", () => { + expect(new Hex(255, 0, 0, 128).toString()).toBe("#FF000080"); + }); + + it("can force the alpha channel on or off", () => { + expect(new Hex(255, 0, 0, 128).toString(false)).toBe("#FF0000"); + expect(new Hex(255, 0, 0, 255).toString(true)).toBe("#FF0000FF"); + }); + + it("pads single-digit channels", () => { + expect(new Hex(1, 2, 3, 255).toString()).toBe("#010203"); + }); + + it("converts to an Rgb instance", () => { + const rgb = new Hex(10, 20, 30, 40).rgb; + expect(rgb).toBeInstanceOf(Rgb); + expect([rgb.r, rgb.g, rgb.b, rgb.a]).toEqual([10, 20, 30, 40]); + }); +}); + +describe("Hsl", () => { + it("exposes degree/percentage getters", () => { + const hsl = new Hsl(0.5, 0.25, 1, 0.5); + expect(hsl.hue).toBe(180); + expect(hsl.saturation).toBe(25); + expect(hsl.lightness).toBe(100); + expect(hsl.toString()).toBe("hsla(180, 25%, 100%, 0.5)"); + }); + + it("converts pure red from rgb", () => { + const hsl = Hsl.fromRgb(new Rgb(255, 0, 0)); + expect(hsl.h).toBe(0); + expect(hsl.s).toBe(1); + expect(hsl.l).toBe(0.5); + expect(hsl.toString()).toBe("hsl(0, 100%, 50%)"); + }); + + it("round-trips primary colors through rgb", () => { + for (const [r, g, b] of [ + [255, 0, 0], + [0, 255, 0], + [0, 0, 255], + [255, 255, 255], + [0, 0, 0], + ]) { + const rgb = Hsl.fromRgb(new Rgb(r, g, b)).rgb; + expect([rgb.r, rgb.g, rgb.b]).toEqual([r, g, b]); + } + }); +}); + +describe("isValidColor", () => { + it("accepts hex colors", () => { + expect(isValidColor("#ff0000")).toBe(true); + expect(isValidColor("#FFF")).toBe(true); + expect(isValidColor("#FF000080")).toBe(true); + }); + + it("accepts functional notations", () => { + expect(isValidColor("rgb(255, 0, 0)")).toBe(true); + expect(isValidColor("rgba(255, 0, 0, 0.5)")).toBe(true); + expect(isValidColor("hsl(120, 50%, 50%)")).toBe(true); + }); + + it("accepts named colors", () => { + expect(isValidColor("red")).toBe(true); + expect(isValidColor("rebeccapurple")).toBe(true); + }); + + it("rejects invalid values", () => { + expect(isValidColor("not-a-color")).toBe(false); + expect(isValidColor("")).toBe(false); + expect(isValidColor("rgb(300, 0, 0)")).toBe(false); + expect(isValidColor("#gg0000")).toBe(false); + }); +}); diff --git a/tests/unit/installSource.test.js b/tests/unit/installSource.test.js new file mode 100644 index 000000000..e5ec61261 --- /dev/null +++ b/tests/unit/installSource.test.js @@ -0,0 +1,23 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { INSTALL_SOURCE_PLAY, isPlayStoreInstall } from "utils/installSource"; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("isPlayStoreInstall", () => { + it("returns true when the app was installed from the Play Store", () => { + vi.stubGlobal("window", { appInstallSource: INSTALL_SOURCE_PLAY }); + expect(isPlayStoreInstall()).toBe(true); + }); + + it("returns false for any other install source", () => { + vi.stubGlobal("window", { appInstallSource: "com.android.packageinstaller" }); + expect(isPlayStoreInstall()).toBe(false); + }); + + it("returns false when the install source is unknown", () => { + vi.stubGlobal("window", {}); + expect(isPlayStoreInstall()).toBe(false); + }); +}); diff --git a/tests/unit/path.test.js b/tests/unit/path.test.js new file mode 100644 index 000000000..d465d0d7c --- /dev/null +++ b/tests/unit/path.test.js @@ -0,0 +1,122 @@ +import { describe, expect, it } from "vitest"; +import Path from "utils/Path"; + +describe("Path.dirname", () => { + it("returns the parent directory", () => { + expect(Path.dirname("/foo/bar/baz.txt")).toBe("/foo/bar"); + expect(Path.dirname("/foo/bar")).toBe("/foo"); + }); + + it("ignores trailing separators", () => { + expect(Path.dirname("foo/bar/")).toBe("./foo"); + }); + + it("returns root for top-level absolute paths", () => { + expect(Path.dirname("/foo")).toBe("/"); + }); + + it("returns '.' for bare filenames", () => { + expect(Path.dirname("file.txt")).toBe("."); + }); +}); + +describe("Path.basename", () => { + it("returns the last portion of a path", () => { + expect(Path.basename("/foo/bar.txt")).toBe("bar.txt"); + expect(Path.basename("foo/bar/baz")).toBe("baz"); + }); + + it("strips the given extension", () => { + expect(Path.basename("/foo/bar.txt", ".txt")).toBe("bar"); + }); + + it("ignores trailing separators", () => { + expect(Path.basename("/foo/bar/")).toBe("bar"); + }); + + it("handles edge cases", () => { + expect(Path.basename("/")).toBe("/"); + expect(Path.basename("")).toBe(""); + }); +}); + +describe("Path.extname", () => { + it("returns the extension including the dot", () => { + expect(Path.extname("file.txt")).toBe(".txt"); + expect(Path.extname("archive.tar.gz")).toBe(".gz"); + }); + + it("returns empty string when there is no extension", () => { + expect(Path.extname("foo/bar")).toBe(""); + }); + + it("returns empty string for dotfiles", () => { + expect(Path.extname(".gitignore")).toBe(""); + }); +}); + +describe("Path.join / normalize", () => { + it("joins segments and normalizes", () => { + expect(Path.join("foo", "bar", "baz")).toBe("foo/bar/baz"); + expect(Path.join("/foo/", "/bar/")).toBe("/foo/bar/"); + }); + + it("resolves '..' segments", () => { + expect(Path.join("foo", "bar", "../baz")).toBe("foo/baz"); + }); + + it("collapses duplicate separators and '.' segments", () => { + expect(Path.normalize("/foo//bar/./baz/../qux")).toBe("/foo/bar/qux"); + }); +}); + +describe("Path.resolve", () => { + it("resolves relative segments against a base", () => { + expect(Path.resolve("path/to/some/dir/", "../../dir")).toBe( + "/path/to/dir", + ); + }); + + it("resets on absolute segments", () => { + expect(Path.resolve("/foo", "/bar")).toBe("/bar"); + }); + + it("throws without arguments", () => { + expect(() => Path.resolve()).toThrow(); + }); +}); + +describe("Path.parse / format", () => { + it("parses a path into its parts", () => { + expect(Path.parse("/foo/bar.txt")).toEqual({ + root: "/", + dir: "/foo", + base: "bar.txt", + ext: ".txt", + name: "bar", + }); + }); + + it("formats a path object back to a string", () => { + expect(Path.format({ dir: "/foo", name: "bar", ext: ".txt" })).toBe( + "/foo/bar.txt", + ); + }); + + it("ignores a malformed extension when formatting", () => { + expect(Path.format({ root: "/", name: "bar", ext: "txt" })).toBe("/bar"); + }); +}); + +describe("Path.isAbsolute / convertToRelative", () => { + it("detects absolute paths", () => { + expect(Path.isAbsolute("/foo")).toBe(true); + expect(Path.isAbsolute("foo/bar")).toBe(false); + }); + + it("computes a path relative to a base", () => { + expect(Path.convertToRelative("/foo/bar", "/foo/baz/qux")).toBe( + "baz/qux", + ); + }); +}); diff --git a/tests/unit/quickToolsState.test.js b/tests/unit/quickToolsState.test.js new file mode 100644 index 000000000..6c3f8a1c6 --- /dev/null +++ b/tests/unit/quickToolsState.test.js @@ -0,0 +1,118 @@ +import { describe, expect, it } from "vitest"; +import { + clearModifierState, + clearQuickToolsButtonFeedback, + modifierKeys, + removeActionStackEntries, +} from "handlers/quickToolsState"; + +describe("clearModifierState", () => { + it("clears all modifiers and reports the change", () => { + const state = { shift: true, alt: true, ctrl: true, meta: true }; + expect(clearModifierState(state)).toBe(true); + for (const key of modifierKeys) { + expect(state[key]).toBe(false); + } + }); + + it("emits false for every registered listener", () => { + const state = { shift: true, alt: false, ctrl: true, meta: false }; + const emitted = []; + const events = { + shift: [(value) => emitted.push(["shift", value])], + alt: [(value) => emitted.push(["alt", value])], + ctrl: [(value) => emitted.push(["ctrl", value])], + meta: [(value) => emitted.push(["meta", value])], + }; + + expect(clearModifierState(state, events)).toBe(true); + expect(emitted).toEqual([ + ["shift", false], + ["alt", false], + ["ctrl", false], + ["meta", false], + ]); + }); + + it("returns false when nothing was active", () => { + const state = { shift: false, alt: false, ctrl: false, meta: false }; + expect(clearModifierState(state)).toBe(false); + }); +}); + +describe("removeActionStackEntries", () => { + function fakeStack(initial) { + const entries = [...initial]; + return { + entries, + remove(id) { + const index = entries.indexOf(id); + if (index === -1) return false; + entries.splice(index, 1); + return true; + }, + }; + } + + it("removes duplicate entries and counts them", () => { + const stack = fakeStack(["search-bar", "other", "search-bar"]); + expect(removeActionStackEntries(stack, "search-bar")).toBe(2); + expect(stack.entries).toEqual(["other"]); + }); + + it("returns 0 when the id is not present", () => { + const stack = fakeStack(["other"]); + expect(removeActionStackEntries(stack, "search-bar")).toBe(0); + expect(stack.entries).toEqual(["other"]); + }); +}); + +describe("clearQuickToolsButtonFeedback", () => { + function fakeButton(initialClasses = [], timeoutId) { + const classes = new Set(initialClasses); + return { + classes, + dataset: timeoutId !== undefined ? { timeout: String(timeoutId) } : {}, + classList: { + contains: (c) => classes.has(c), + remove: (...cs) => cs.forEach((c) => classes.delete(c)), + }, + }; + } + + function fakeContainer(buttons, matchesItself = false) { + return { + matches: () => matchesItself, + querySelectorAll: () => buttons, + }; + } + + it("clears active/click state and pending timeouts", () => { + const active = fakeButton(["icon", "active", "click"], 123); + const plain = fakeButton(["icon"]); + const container = fakeContainer([active, plain]); + + expect(clearQuickToolsButtonFeedback([container])).toBe(1); + expect([...active.classes]).toEqual(["icon"]); + expect(active.dataset.timeout).toBeUndefined(); + expect([...plain.classes]).toEqual(["icon"]); + }); + + it("skips containers that match themselves", () => { + // The implementation marks each container as visited before checking + // whether the container itself matches, so the self-match branch never + // clears it. This pins the current behavior. + const container = fakeContainer([], true); + container.dataset = { timeout: "456" }; + container.classList = fakeButton(["active"]).classList; + + expect(clearQuickToolsButtonFeedback([container])).toBe(0); + }); + + it("does not process duplicate containers twice", () => { + const button = fakeButton(["active"]); + const container = fakeContainer([button]); + + expect(clearQuickToolsButtonFeedback([container, container])).toBe(1); + }); +}); diff --git a/tests/unit/taskManager.test.js b/tests/unit/taskManager.test.js new file mode 100644 index 000000000..987689c2c --- /dev/null +++ b/tests/unit/taskManager.test.js @@ -0,0 +1,78 @@ +import { describe, expect, it } from "vitest"; +import TaskManager from "utils/taskManager"; + +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +describe("TaskManager (linear)", () => { + it("runs tasks one at a time in queue order", async () => { + const tm = new TaskManager("linear"); + const order = []; + + const task = (name, ms) => async () => { + await sleep(ms); + order.push(name); + return name; + }; + + const results = await Promise.all([ + tm.queueTask(task("a", 30)), + tm.queueTask(task("b", 10)), + tm.queueTask(task("c", 0)), + ]); + + expect(order).toEqual(["a", "b", "c"]); + expect(results).toEqual(["a", "b", "c"]); + }); + + it("passes an incrementing id to each task", async () => { + const tm = new TaskManager("linear"); + const ids = await Promise.all([ + tm.queueTask(async (id) => id), + tm.queueTask(async (id) => id), + tm.queueTask(async (id) => id), + ]); + + expect(ids).toEqual([0, 1, 2]); + }); + + it("rejects the failed task and continues with the queue", async () => { + const tm = new TaskManager("linear"); + + const failing = tm.queueTask(async () => { + throw new Error("boom"); + }); + const next = tm.queueTask(async () => "ok"); + + await expect(failing).rejects.toThrow("boom"); + await expect(next).resolves.toBe("ok"); + }); +}); + +describe("TaskManager (parallel)", () => { + it("runs tasks concurrently", async () => { + const tm = new TaskManager("parallel"); + const order = []; + + await Promise.all([ + tm.queueTask(async () => { + await sleep(30); + order.push("slow"); + }), + tm.queueTask(async () => { + order.push("fast"); + }), + ]); + + expect(order).toEqual(["fast", "slow"]); + }); + + it("propagates errors to the awaiting caller", async () => { + const tm = new TaskManager("parallel"); + + await expect( + tm.queueTask(async () => { + throw new Error("boom"); + }), + ).rejects.toThrow("boom"); + }); +}); diff --git a/tests/unit/uri.test.js b/tests/unit/uri.test.js new file mode 100644 index 000000000..dd7fbfd6b --- /dev/null +++ b/tests/unit/uri.test.js @@ -0,0 +1,72 @@ +import { describe, expect, it } from "vitest"; +import Uri from "utils/Uri"; + +const TREE_URI = + "content://com.android.externalstorage.documents/tree/primary%3ADCIM"; +const SINGLE_URI = + "content://com.android.externalstorage.documents/document/primary%3ADCIM%2Ffile.txt"; + +describe("Uri.parse", () => { + it("splits a tree uri containing '::'", () => { + expect(Uri.parse(`${TREE_URI}::primary:DCIM/file.txt`)).toEqual({ + rootUri: TREE_URI, + docId: "primary:DCIM/file.txt", + isFileUri: false, + }); + }); + + it("derives the docId from the last segment of a bare tree uri", () => { + expect(Uri.parse(TREE_URI)).toEqual({ + rootUri: TREE_URI, + docId: "primary:DCIM", + isFileUri: false, + }); + }); + + it("converts a single document uri into tree form", () => { + expect(Uri.parse(SINGLE_URI)).toEqual({ + rootUri: + "content://com.android.externalstorage.documents/tree/primary%3A", + docId: "primary:DCIM/file.txt", + isFileUri: false, + }); + }); + + it("throws on non-content uris", () => { + expect(() => Uri.parse("file:///sdcard/x")).toThrow("Invalid uri format"); + expect(() => Uri.parse("ftp://host/x")).toThrow("Invalid uri format"); + }); +}); + +describe("Uri.format", () => { + it("joins rootUri and docId with '::'", () => { + expect(Uri.format(TREE_URI, "primary:DCIM/file.txt")).toBe( + `${TREE_URI}::primary:DCIM/file.txt`, + ); + }); + + it("accepts an object", () => { + expect( + Uri.format({ rootUri: TREE_URI, docId: "primary:DCIM/file.txt" }), + ).toBe(`${TREE_URI}::primary:DCIM/file.txt`); + }); + + it("returns the rootUri when there is no docId", () => { + expect(Uri.format(TREE_URI)).toBe(TREE_URI); + }); +}); + +describe("Uri.getPrimaryAddress", () => { + it("returns the path after '::primary:'", () => { + expect(Uri.getPrimaryAddress(`${TREE_URI}::primary:DCIM/foo`)).toBe( + "DCIM/foo", + ); + }); +}); + +describe("Uri.getVirtualAddress", () => { + it("returns the url unchanged when no storage list is available", () => { + const url = `${TREE_URI}::primary:DCIM/foo`; + expect(Uri.getVirtualAddress(url)).toBe(url); + }); +}); diff --git a/tests/unit/url.test.js b/tests/unit/url.test.js new file mode 100644 index 000000000..f2304b659 --- /dev/null +++ b/tests/unit/url.test.js @@ -0,0 +1,159 @@ +import { describe, expect, it } from "vitest"; +import Url from "utils/Url"; + +describe("Url.getProtocol", () => { + it("extracts the protocol", () => { + expect(Url.getProtocol("ftp://localhost/foo")).toBe("ftp:"); + expect(Url.getProtocol("https://example.com")).toBe("https:"); + expect(Url.getProtocol("file:///sdcard")).toBe("file:"); + }); + + it("returns empty string when there is no protocol", () => { + expect(Url.getProtocol("/sdcard/foo")).toBe(""); + }); +}); + +describe("Url.basename / extname", () => { + it("returns the last segment of a url", () => { + expect(Url.basename("ftp://localhost/foo/bar/index.html")).toBe( + "index.html", + ); + }); + + it("ignores trailing slashes", () => { + expect(Url.basename("https://example.com/foo/")).toBe("foo"); + }); + + it("resolves basenames of content uris", () => { + expect( + Url.basename( + "content://com.android.externalstorage.documents/tree/primary%3ADCIM::primary:DCIM/file.txt", + ), + ).toBe("file.txt"); + }); + + it("returns the extension of the basename", () => { + expect(Url.extname("ftp://host/a/b.txt")).toBe(".txt"); + expect(Url.extname("https://example.com/foo")).toBe(""); + }); +}); + +describe("Url.join", () => { + it("joins path segments keeping the protocol", () => { + expect(Url.join("ftp://localhost/foo/", "bar", "baz.txt")).toBe( + "ftp://localhost/foo/bar/baz.txt", + ); + expect(Url.join("https://example.com", "/a/b")).toBe( + "https://example.com/a/b", + ); + }); + + it("requires at least two arguments", () => { + expect(() => Url.join("ftp://localhost")).toThrow(); + }); +}); + +describe("Url.parse / pathname / dirname", () => { + it("splits url and query", () => { + expect(Url.parse("ftp://host/a?x=1")).toEqual({ + url: "ftp://host/a", + query: "?x=1", + }); + expect(Url.parse("ftp://host/a")).toEqual({ + url: "ftp://host/a", + query: "", + }); + }); + + it("extracts the pathname", () => { + expect(Url.pathname("ftp://myhost.com/foo/bar")).toBe("/foo/bar"); + expect(Url.pathname("file:///sdcard/foo")).toBe("/sdcard/foo"); + }); + + it("returns the parent url", () => { + expect(Url.dirname("ftp://localhost/foo/bar")).toBe( + "ftp://localhost/foo/", + ); + }); +}); + +describe("Url.safe", () => { + it("encodes path segments but leaves the query untouched", () => { + expect(Url.safe("https://ex.com/a b/c?x=1 2")).toBe( + "https://ex.com/a%20b/c?x=1 2", + ); + }); +}); + +describe("Url.formate", () => { + it("builds a url from its parts", () => { + expect( + Url.formate({ + protocol: "ftp:", + hostname: "host", + username: "u", + password: "p", + path: "a/b", + port: 21, + query: { x: "1" }, + }), + ).toBe("ftp://u:p@host:21/a/b?x=1"); + }); + + it("builds a minimal url", () => { + expect(Url.formate({ protocol: "https:", hostname: "x.com" })).toBe( + "https://x.com", + ); + }); + + it("throws when protocol or hostname are missing", () => { + expect(() => Url.formate({ hostname: "x.com" })).toThrow(); + expect(() => Url.formate({ protocol: "ftp:" })).toThrow(); + }); +}); + +describe("Url.hidePassword / decodeUrl", () => { + it("strips the password from a url", () => { + expect(Url.hidePassword("ftp://user:secret@host.com/a")).toBe( + "ftp://user@host.com/a", + ); + }); + + it("leaves file urls untouched", () => { + expect(Url.hidePassword("file:///sdcard/x")).toBe("file:///sdcard/x"); + }); + + it("decodes credentials, port, path and query", () => { + // NOTE: url-parse only splits the port for special protocols + // (http/https/ws/...), so an http url is used to exercise the + // port decoding branch of decodeUrl. + expect( + Url.decodeUrl("http://user:p%40ss@host.com:8080/a%20b?x=1"), + ).toEqual({ + username: "user", + password: "p@ss", + hostname: "host.com", + pathname: "/a b", + port: 8080, + query: { x: "1" }, + }); + }); + + it("decodes credentials for ftp urls", () => { + const decoded = Url.decodeUrl("ftp://user:p%40ss@host.com/a%20b"); + expect(decoded.username).toBe("user"); + expect(decoded.password).toBe("p@ss"); + expect(decoded.hostname).toBe("host.com"); + expect(decoded.pathname).toBe("/a b"); + }); +}); + +describe("Url.areSame", () => { + it("ignores trailing slashes", () => { + expect(Url.areSame("ftp://a/", "ftp://a", "ftp://a/")).toBe(true); + }); + + it("detects different urls", () => { + expect(Url.areSame("ftp://a", "ftp://b")).toBe(false); + }); +}); diff --git a/tests/unit/version.test.js b/tests/unit/version.test.js new file mode 100644 index 000000000..246522c95 --- /dev/null +++ b/tests/unit/version.test.js @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { compareVersions, isVersionGreater, parseVersion } from "utils/version"; + +describe("utils/version", () => { + it("parses strict x.y.z versions", () => { + expect(parseVersion("1.2.3")).toEqual([1, 2, 3]); + expect(parseVersion("v1.2.3")).toEqual([1, 2, 3]); + expect(parseVersion("1.2")).toBeNull(); + expect(parseVersion("1.2.x")).toBeNull(); + }); + + it("compares versions", () => { + expect(compareVersions("1.2.0", "1.1.9")).toBe(1); + expect(compareVersions("1.1.1", "1.1.1")).toBe(0); + expect(compareVersions("1.0.0", "1.1.1")).toBe(-1); + }); + + it("only treats newer versions as upgrades", () => { + expect(isVersionGreater("1.1.2", "1.1.1")).toBe(true); + expect(isVersionGreater("1.2.0", "1.1.9")).toBe(true); + expect(isVersionGreater("1.1.1", "1.1.1")).toBe(false); + expect(isVersionGreater("1.0.0", "1.1.1")).toBe(false); + }); +}); diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 000000000..c3c6a1e9d --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,36 @@ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { configDefaults, defineConfig } from "vitest/config"; + +const srcDir = fileURLToPath(new URL("./src", import.meta.url)); + +// Mirror the bundler resolution (rspack `resolve.modules: ["node_modules", "src"]` +// and tsconfig `paths: { "*": ["./src/*"] }`) so unit tests can import app +// modules via bare paths like `utils/version` or `lib/settings`. +const srcAliases = fs + .readdirSync(srcDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => ({ + find: new RegExp(`^${entry.name}(?:/(.*))?$`), + replacement: `${path.join(srcDir, entry.name)}/$1`, + })); + +export default defineConfig({ + resolve: { + alias: srcAliases, + }, + test: { + // Vitest unit tests live ONLY under `tests/`. + // `src/test/` is Acode's in-app runtime test harness: it runs on-device + // inside the WebView (launched from the app commands) and depends on + // cordova/editorManager globals, so it must never be collected here. + include: ["tests/**/*.test.{js,ts}"], + exclude: [ + ...configDefaults.exclude, + "src/test/**", + "www/**", + "platforms/**", + ], + }, +});