diff --git a/package-lock.json b/package-lock.json index e1e561c57..60075d065 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" } @@ -4802,6 +4803,16 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/@oxc-project/types": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz", + "integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@parcel/watcher": { "version": "2.5.6", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", @@ -5112,6 +5123,323 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.1.tgz", + "integrity": "sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA==", + "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.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.1.tgz", + "integrity": "sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA==", + "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.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.1.tgz", + "integrity": "sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ==", + "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.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.1.tgz", + "integrity": "sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg==", + "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.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.1.tgz", + "integrity": "sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA==", + "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.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.1.tgz", + "integrity": "sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.1.tgz", + "integrity": "sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.1.tgz", + "integrity": "sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.1.tgz", + "integrity": "sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.1.tgz", + "integrity": "sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.1.tgz", + "integrity": "sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.1.tgz", + "integrity": "sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw==", + "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.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.2.1.tgz", + "integrity": "sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "2.0.0-alpha.3", + "@emnapi/runtime": "2.0.0-alpha.3", + "@napi-rs/wasm-runtime": "^1.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz", + "integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "2.0.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz", + "integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz", + "integrity": "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.1.tgz", + "integrity": "sha512-KjZdi8Q1wh89gsVmghvbrMgWl6ZWmRmHV6wjB7/g4Zf0dyO+hH3neZUtuDNPO00qq5YE5RITVWvrIZKRaAmzGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.1.tgz", + "integrity": "sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw==", + "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.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.1.tgz", + "integrity": "sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ==", + "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.0.3", "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-2.0.3.tgz", @@ -5407,6 +5735,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", @@ -5453,9 +5788,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, "license": "MIT", "optional": true, @@ -5470,6 +5805,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", @@ -5730,6 +6086,13 @@ "@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/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -5759,8 +6122,7 @@ "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", @@ -5862,6 +6224,129 @@ "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", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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", + "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/@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", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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": "MIT", + "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": { "version": "0.0.18", "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.18.tgz", @@ -9379,7 +9864,6 @@ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -9727,8 +10211,7 @@ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.2", @@ -9855,6 +10338,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", @@ -9918,6 +10411,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", @@ -12102,64 +12605,325 @@ "dependencies": { "commander": "^8.3.0" }, - "bin": { - "katex": "cli.js" + "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, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "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-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, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.0" + "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-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": { - "package-json": "^6.3.0" - }, + "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-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": { - "readable-stream": "^2.0.5" + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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": "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": { @@ -12361,6 +13125,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", @@ -13339,9 +14113,9 @@ "license": "ISC" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -14107,6 +14881,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", @@ -14132,9 +14913,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "license": "MIT", "engines": { "node": ">=12" @@ -14263,9 +15044,9 @@ } }, "node_modules/postcss": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", "dev": true, "funding": [ { @@ -14283,7 +15064,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -15342,6 +16123,40 @@ "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", "license": "Unlicense" }, + "node_modules/rolldown": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.1.tgz", + "integrity": "sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.142.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.2.1", + "@rolldown/binding-darwin-arm64": "1.2.1", + "@rolldown/binding-darwin-x64": "1.2.1", + "@rolldown/binding-freebsd-x64": "1.2.1", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.1", + "@rolldown/binding-linux-arm64-gnu": "1.2.1", + "@rolldown/binding-linux-arm64-musl": "1.2.1", + "@rolldown/binding-linux-ppc64-gnu": "1.2.1", + "@rolldown/binding-linux-s390x-gnu": "1.2.1", + "@rolldown/binding-linux-x64-gnu": "1.2.1", + "@rolldown/binding-linux-x64-musl": "1.2.1", + "@rolldown/binding-openharmony-arm64": "1.2.1", + "@rolldown/binding-wasm32-wasi": "1.2.1", + "@rolldown/binding-win32-arm64-msvc": "1.2.1", + "@rolldown/binding-win32-x64-msvc": "1.2.1" + } + }, "node_modules/roughjs": { "version": "4.6.6", "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", @@ -15720,6 +16535,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", @@ -15926,6 +16748,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", @@ -15936,6 +16765,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", @@ -16286,6 +17122,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.1.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", @@ -16296,9 +17139,9 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -16311,6 +17154,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "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", @@ -16913,6 +17766,174 @@ "dev": true, "license": "MIT" }, + "node_modules/vite": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.0.tgz", + "integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.23", + "rolldown": "~1.2.0", + "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.4.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", @@ -17122,6 +18143,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 210c0529c..0faaeef1f 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/src/handlers/quickToolsState.js b/src/handlers/quickToolsState.js index 6b025c2e6..7301aed0a 100644 --- a/src/handlers/quickToolsState.js +++ b/src/handlers/quickToolsState.js @@ -13,12 +13,13 @@ export function clearModifierState(state, events = {}) { } export function clearQuickToolsButtonFeedback(containers = []) { - const visited = new Set(); + const visitedContainers = new Set(); + const visitedButtons = new Set(); let cleared = 0; for (const container of containers) { - if (!container || visited.has(container)) continue; - visited.add(container); + if (!container || visitedContainers.has(container)) continue; + visitedContainers.add(container); const buttons = [ ...(container.matches?.(".active, .click, [data-timeout]") @@ -29,8 +30,8 @@ export function clearQuickToolsButtonFeedback(containers = []) { ]; for (const button of buttons) { - if (!button || visited.has(button)) continue; - visited.add(button); + if (!button || visitedButtons.has(button)) continue; + visitedButtons.add(button); if (button.dataset?.timeout) { clearTimeout(Number(button.dataset.timeout)); delete button.dataset.timeout; 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..14059d572 --- /dev/null +++ b/tests/unit/quickToolsState.test.js @@ -0,0 +1,117 @@ +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("clears containers that match themselves", () => { + const container = fakeButton(["active"], 456); + container.matches = () => true; + container.querySelectorAll = () => []; + + expect(clearQuickToolsButtonFeedback([container])).toBe(1); + expect([...container.classes]).toEqual([]); + expect(container.dataset.timeout).toBeUndefined(); + }); + + 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/**", + ], + }, +});