diff --git a/.github/workflows/lint-and-format.yaml b/.github/workflows/ci.yaml
similarity index 93%
rename from .github/workflows/lint-and-format.yaml
rename to .github/workflows/ci.yaml
index 1a36c18..ad21ea9 100644
--- a/.github/workflows/lint-and-format.yaml
+++ b/.github/workflows/ci.yaml
@@ -33,3 +33,6 @@ jobs:
- name: Run ESLint
run: npm run lint
+
+ - name: Run Tests
+ run: npm run test
diff --git a/package-lock.json b/package-lock.json
index c06388b..e28e435 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
},
"devDependencies": {
"@eslint/js": "^10.0.1",
+ "@testing-library/react": "^16.3.3",
"@types/node": "^24.13.3",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.7",
@@ -23,10 +24,65 @@
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.6",
"globals": "^17.12.0",
+ "jsdom": "^30.1.0",
"prettier": "^3.9.7",
"typescript": "~6.0.2",
"typescript-eslint": "^8.69.0",
- "vite": "^8.3.0"
+ "vite": "^8.3.0",
+ "vitest": "^5.0.1"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-7.0.0.tgz",
+ "integrity": "sha512-IxddtGnnoidVSO6O/aiTueptbTThHvi2qGHu9/AWuD1J7i86Ox+jl+Y5SXcDH1RixzQlP2cjH/PjnlImZiAurQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-calc": "^3.3.0",
+ "@csstools/css-color-parser": "^4.2.0",
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0",
+ "lru-cache": "^11.5.2"
+ },
+ "engines": {
+ "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
+ "version": "11.5.2",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
+ "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@asamuzakjp/dom-selector": {
+ "version": "9.1.4",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-9.1.4.tgz",
+ "integrity": "sha512-ynDE7RDZM1z+YuSU+iGhRp8WcSXHqK9+G32ZEzsL35TKSoy72fYR7VfrJCQkWe3ciiIWGSTAIBc9XB5jVpqyTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bidi-js": "^1.1.0",
+ "css-tree": "^3.2.1",
+ "is-potential-custom-element-name": "^1.0.1",
+ "lru-cache": "^11.5.2"
+ },
+ "engines": {
+ "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": {
+ "version": "11.5.2",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
+ "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
}
},
"node_modules/@babel/code-frame": {
@@ -221,6 +277,16 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@babel/runtime": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/template": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
@@ -269,6 +335,19 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@bramus/specificity": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
+ "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "css-tree": "^3.0.0"
+ },
+ "bin": {
+ "specificity": "bin/cli.js"
+ }
+ },
"node_modules/@cacheable/memory": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz",
@@ -293,6 +372,146 @@
"keyv": "^5.6.0"
}
},
+ "node_modules/@csstools/color-helpers": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz",
+ "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.0.tgz",
+ "integrity": "sha512-XQKj5B7QiZcHiegCOCAzcAOJdhGgWOHbbu62h5e5mkHnn8lWcfiJhllkqWmxu5zWR9jucPHuo1iTB56P033hcg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.3.tgz",
+ "integrity": "sha512-y4LpL+lmpuyKDiEFq2PnZUVFdAjsoB/qQJod79yLNokXyW7jewi+/WJ69EfItj8A2unWtxXnGjw6LYXgXu5ZjA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^6.1.1",
+ "@csstools/css-calc": "^3.4.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
+ "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-syntax-patches-for-csstree": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.14.tgz",
+ "integrity": "sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "peerDependencies": {
+ "css-tree": "^3.2.1"
+ },
+ "peerDependenciesMeta": {
+ "css-tree": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
+ "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
@@ -421,6 +640,24 @@
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
+ "node_modules/@exodus/bytes": {
+ "version": "1.15.1",
+ "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz",
+ "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@noble/hashes": "^1.8.0 || ^2.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@noble/hashes": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@humanfs/core": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
@@ -859,6 +1096,81 @@
"react": "^18 || ^19"
}
},
+ "node_modules/@testing-library/dom": {
+ "version": "10.4.2",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz",
+ "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.12.5",
+ "@types/aria-query": "^5.0.1",
+ "aria-query": "5.3.0",
+ "dom-accessibility-api": "^0.5.9",
+ "lz-string": "^1.5.0",
+ "picocolors": "1.1.1",
+ "pretty-format": "^27.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@testing-library/react": {
+ "version": "16.3.3",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz",
+ "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": "^10.0.0",
+ "@types/react": "^18.0.0 || ^19.0.0",
+ "@types/react-dom": "^18.0.0 || ^19.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@types/aria-query": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/esrecurse": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
@@ -1183,6 +1495,44 @@
}
}
},
+ "node_modules/@vitest/mocker": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.1.tgz",
+ "integrity": "sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.31",
+ "@vitest/spy": "5.0.1",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^1.2.3"
+ },
+ "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/spy": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.1.tgz",
+ "integrity": "sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
"node_modules/acorn": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
@@ -1223,6 +1573,52 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "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/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
@@ -1246,6 +1642,16 @@
"node": ">=6.0.0"
}
},
+ "node_modules/bidi-js": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz",
+ "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "require-from-string": "^2.0.2"
+ }
+ },
"node_modules/brace-expansion": {
"version": "5.0.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
@@ -1328,6 +1734,16 @@
],
"license": "CC-BY-4.0"
},
+ "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/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -1359,6 +1775,20 @@
"node": ">= 8"
}
},
+ "node_modules/css-tree": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
+ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.27.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -1366,6 +1796,35 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/data-urls": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
+ "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/data-urls/node_modules/whatwg-url": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
+ "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.11.0",
+ "tr46": "^6.0.0",
+ "webidl-conversions": "^8.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -1384,6 +1843,13 @@
}
}
},
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -1391,6 +1857,17 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -1401,6 +1878,14 @@
"node": ">=8"
}
},
+ "node_modules/dom-accessibility-api": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.428",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.428.tgz",
@@ -1408,6 +1893,26 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/entities": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz",
+ "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
+ "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -1606,6 +2111,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",
@@ -1616,6 +2131,16 @@
"node": ">=0.10.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/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -1789,6 +2314,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/html-encoding-sniffer": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
+ "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.6.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -1832,6 +2370,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -1846,6 +2391,56 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/jsdom": {
+ "version": "30.1.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.1.0.tgz",
+ "integrity": "sha512-h/Q+H+KmlGf49ITw3TrykSVbg04S75S2O7ZtJc9gMGOWBrQRZmx5AJIKLtJ01e6Kszl+/tpFthCVqxNifxhoqg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^7.0.0",
+ "@asamuzakjp/dom-selector": "^9.1.2",
+ "@bramus/specificity": "^2.4.2",
+ "@csstools/css-syntax-patches-for-csstree": "^1.1.13",
+ "@exodus/bytes": "^1.15.1",
+ "css-tree": "^3.2.1",
+ "data-urls": "^7.0.0",
+ "decimal.js": "^10.6.0",
+ "html-encoding-sniffer": "^6.0.0",
+ "is-potential-custom-element-name": "^1.0.1",
+ "lru-cache": "^11.5.2",
+ "parse5": "^8.0.1",
+ "saxes": "^6.0.0",
+ "tough-cookie": "^6.0.2",
+ "undici": "^8.10.2",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^8.0.1",
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^17.1.1",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
+ },
+ "peerDependencies": {
+ "canvas": "^3.2.3"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jsdom/node_modules/lru-cache": {
+ "version": "11.5.2",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
+ "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -2197,6 +2792,34 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.1.tgz",
+ "integrity": "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.6.0"
+ }
+ },
+ "node_modules/mdn-data": {
+ "version": "2.27.1",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
+ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
"node_modules/minimatch": {
"version": "10.2.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
@@ -2256,6 +2879,20 @@
"node": ">=18"
}
},
+ "node_modules/obug": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz",
+ "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -2306,6 +2943,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/parse5": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
+ "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^8.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -2401,6 +3051,22 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
+ "node_modules/pretty-format": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^17.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -2452,6 +3118,24 @@
"react": "^19.3.0"
}
},
+ "node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/rolldown": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz",
@@ -2486,6 +3170,19 @@
"@rolldown/binding-win32-x64-msvc": "1.2.8"
}
},
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
"node_modules/scheduler": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
@@ -2525,6 +3222,13 @@
"node": ">=8"
}
},
+ "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/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -2535,6 +3239,40 @@
"node": ">=0.10.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/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/tinybench": {
+ "version": "6.1.4",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.1.4.tgz",
+ "integrity": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/tinyexec": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz",
+ "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
@@ -2552,6 +3290,52 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
+ "node_modules/tldts": {
+ "version": "7.4.13",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.13.tgz",
+ "integrity": "sha512-iHtaIWWIbMDkCeJdTBzZFGgbluE5J+oHlb2g7+oAz1S1gpuVpabRZdQyd471Vl8UUkcz2vXSL8xZH2kyCe8tfA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^7.4.13"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "7.4.13",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.13.tgz",
+ "integrity": "sha512-mbYsrih5FRtGxs3Usvl/PqwJsNpp+jsmrdFviiK02teHDG0/HebBG/pqCylje3kzgXYzuLoHJF/0mz9W53t8Xg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tough-cookie": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz",
+ "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^7.0.5"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
+ "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/ts-api-utils": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
@@ -2616,6 +3400,16 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
+ "node_modules/undici": {
+ "version": "8.10.2",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.2.tgz",
+ "integrity": "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=22.19.0"
+ }
+ },
"node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
@@ -2742,6 +3536,137 @@
}
}
},
+ "node_modules/vitest": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.1.tgz",
+ "integrity": "sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/mocker": "5.0.1",
+ "chai": "^6.2.2",
+ "es-module-lexer": "^2.3.2",
+ "expect-type": "^1.4.0",
+ "magic-string": "^1.2.3",
+ "obug": "^2.1.4",
+ "picomatch": "^4.0.7",
+ "std-env": "^4.2.0",
+ "tinybench": "6.1.4",
+ "tinyexec": "1.3.0",
+ "tinyglobby": "^0.2.17",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^22.12.0 || ^24.0.0 || >=26.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "5.0.1",
+ "@vitest/browser-preview": "5.0.1",
+ "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0",
+ "@vitest/coverage-istanbul": "5.0.1",
+ "@vitest/coverage-v8": "5.0.1",
+ "@vitest/ui": "5.0.1",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.4.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/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
+ "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
+ "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "17.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.1.tgz",
+ "integrity": "sha512-ohjk1mdUebJVadRt3bAhQhx8lSnISq+GDttK79LFl8EHQkAPvzwctoasC4hs8tBt6kLAncBWWyq1N52qEfKvDw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.15.1",
+ "tr46": "^6.0.0",
+ "webidl-conversions": "^8.0.1"
+ },
+ "engines": {
+ "node": "^22.14.0 || >=24.0.0"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -2758,6 +3683,23 @@
"node": ">= 8"
}
},
+ "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/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -2768,6 +3710,23 @@
"node": ">=0.10.0"
}
},
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
diff --git a/package.json b/package.json
index 8451f13..519e80f 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,8 @@
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
- "fmt": "npx prettier . --write"
+ "fmt": "npx prettier . --write",
+ "test": "vitest run"
},
"dependencies": {
"@tanstack/react-query": "^5.102.8",
@@ -18,6 +19,7 @@
},
"devDependencies": {
"@eslint/js": "^10.0.1",
+ "@testing-library/react": "^16.3.3",
"@types/node": "^24.13.3",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.7",
@@ -26,9 +28,11 @@
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.6",
"globals": "^17.12.0",
+ "jsdom": "^30.1.0",
"prettier": "^3.9.7",
"typescript": "~6.0.2",
"typescript-eslint": "^8.69.0",
- "vite": "^8.3.0"
+ "vite": "^8.3.0",
+ "vitest": "^5.0.1"
}
}
diff --git a/tests/components/ViewCity.test.tsx b/tests/components/ViewCity.test.tsx
new file mode 100644
index 0000000..c5ec3e4
--- /dev/null
+++ b/tests/components/ViewCity.test.tsx
@@ -0,0 +1,73 @@
+// @vitest-environment jsdom
+
+import { render, screen } from "@testing-library/react"
+import { beforeEach, describe, expect, it, vi } from "vitest"
+import ViewCity from "../../src/components/ViewCity"
+
+const { mockUseGetCityCoordinates, mockUseGetWeather } = vi.hoisted(() => ({
+ mockUseGetCityCoordinates: vi.fn(),
+ mockUseGetWeather: vi.fn(),
+}))
+
+vi.mock("../../src/hooks/fetchCities/useGetCityCoordinates", () => ({
+ default: mockUseGetCityCoordinates,
+}))
+
+vi.mock("../../src/hooks/fetchWeather/useGetWeather", () => ({
+ default: mockUseGetWeather,
+}))
+
+describe("ViewCity", () => {
+ beforeEach(() => {
+ mockUseGetCityCoordinates.mockReturnValue({
+ result: [{ latitude: 59.9, longitude: 10.7 }],
+ })
+ })
+
+ it("renders the current temperature and wind speed", () => {
+ mockUseGetWeather.mockReturnValue({
+ isLoading: false,
+ error: null,
+ result: {
+ properties: {
+ timeseries: [
+ {
+ data: {
+ instant: {
+ details: { air_temperature: 12.5, wind_speed: 4.2 },
+ },
+ },
+ },
+ ],
+ },
+ },
+ })
+
+ render()
+
+ expect(screen.getByText("Været i Oslo")).toBeTruthy()
+ expect(screen.getByText("Temperatur: 12.5")).toBeTruthy()
+ expect(screen.getByText("Vindhastighet: 4.2")).toBeTruthy()
+ expect(mockUseGetCityCoordinates).toHaveBeenCalledWith({ cityName: "Oslo" })
+ expect(mockUseGetWeather).toHaveBeenCalledWith(59.9, 10.7)
+ })
+
+ it("renders a loading message while weather data is loading", () => {
+ mockUseGetWeather.mockReturnValue({ isLoading: true, error: null })
+
+ render()
+
+ expect(screen.getByText("Laster")).toBeTruthy()
+ })
+
+ it("renders an error message when the weather request fails", () => {
+ mockUseGetWeather.mockReturnValue({
+ isLoading: false,
+ error: new Error("Request failed"),
+ })
+
+ render()
+
+ expect(screen.getByText("Det skjedde en feil")).toBeTruthy()
+ })
+})
diff --git a/tests/components/listItem.test.tsx b/tests/components/listItem.test.tsx
new file mode 100644
index 0000000..d568542
--- /dev/null
+++ b/tests/components/listItem.test.tsx
@@ -0,0 +1,36 @@
+// @vitest-environment jsdom
+
+import { render, screen } from "@testing-library/react"
+import { describe, expect, it } from "vitest"
+import ListItem from "../../src/components/listItem"
+
+describe("ListItem", () => {
+ it("renders city weather, forecast temperatures, and a rain icon", () => {
+ render(
+
+ )
+
+ expect(screen.getByText("Oslo:")).toBeTruthy()
+ expect(screen.getByText("🌧️")).toBeTruthy()
+ expect(screen.getByText("11°")).toBeTruthy()
+ expect(screen.getByText("8°")).toBeTruthy()
+ expect(screen.getByText("7°")).toBeTruthy()
+ })
+
+ it("shows fallback values when weather data is unavailable", () => {
+ render()
+
+ expect(screen.getByText("⛅")).toBeTruthy()
+ expect(screen.getByText("—")).toBeTruthy()
+ })
+})
diff --git a/tests/components/location-list.test.tsx b/tests/components/location-list.test.tsx
new file mode 100644
index 0000000..3e1422c
--- /dev/null
+++ b/tests/components/location-list.test.tsx
@@ -0,0 +1,64 @@
+// @vitest-environment jsdom
+
+import { render, screen } from "@testing-library/react"
+import { beforeEach, describe, expect, it, vi } from "vitest"
+import LocationList from "../../src/components/location-list"
+
+const { mockUseGetWeatherForCities } = vi.hoisted(() => ({
+ mockUseGetWeatherForCities: vi.fn(),
+}))
+
+vi.mock("../../src/hooks/fetchWeather/useGetWeather", () => ({
+ useGetWeatherForCities: mockUseGetWeatherForCities,
+}))
+
+const weatherResponse = (temperature: number, precipitation: number) => ({
+ properties: {
+ timeseries: [
+ {
+ time: "2026-09-17T10:00:00Z",
+ data: {
+ instant: { details: { air_temperature: temperature } },
+ next_6_hours: { details: { precipitation_amount: precipitation } },
+ },
+ },
+ ],
+ },
+})
+
+describe("LocationList", () => {
+ beforeEach(() => {
+ mockUseGetWeatherForCities.mockReset()
+ })
+
+ it("renders a loading message while city forecasts are loading", () => {
+ mockUseGetWeatherForCities.mockReturnValue({ isLoading: true, results: [] })
+
+ render()
+
+ expect(screen.getByText("Loading...")).toBeTruthy()
+ })
+
+ it("renders a weather row for every configured city", () => {
+ mockUseGetWeatherForCities.mockReturnValue({
+ isLoading: false,
+ results: [
+ weatherResponse(11, 0),
+ weatherResponse(8, 0.7),
+ weatherResponse(6, 0),
+ weatherResponse(25, 0),
+ ],
+ })
+
+ render()
+
+ expect(screen.getByText("City")).toBeTruthy()
+ expect(screen.getByText("Next 3 Days")).toBeTruthy()
+ expect(screen.getByText("Oslo:")).toBeTruthy()
+ expect(screen.getByText("Sørumsand:")).toBeTruthy()
+ expect(screen.getByText("Trondheim:")).toBeTruthy()
+ expect(screen.getByText("Las Vegas:")).toBeTruthy()
+ expect(screen.getByText("25°")).toBeTruthy()
+ expect(screen.getAllByText("🌧️")).toHaveLength(1)
+ })
+})
diff --git a/tests/hooks/fetchCities/useGetCityCoordinates.test.ts b/tests/hooks/fetchCities/useGetCityCoordinates.test.ts
new file mode 100644
index 0000000..c0d2790
--- /dev/null
+++ b/tests/hooks/fetchCities/useGetCityCoordinates.test.ts
@@ -0,0 +1,66 @@
+// @vitest-environment jsdom
+
+import { createElement, type PropsWithChildren } from "react"
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
+import { renderHook, waitFor } from "@testing-library/react"
+import { afterEach, expect, it, vi } from "vitest"
+import type { citySearchResult } from "../../../src/hooks/fetchCities/types"
+import useGetCityCoordinates from "../../../src/hooks/fetchCities/useGetCityCoordinates"
+
+afterEach(() => {
+ vi.unstubAllGlobals()
+})
+
+function createQueryClientWrapper() {
+ const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+ })
+
+ return ({ children }: PropsWithChildren) =>
+ createElement(QueryClientProvider, { client: queryClient }, children)
+}
+
+it("should fetch the city search results", async () => {
+ const mockResponse: citySearchResult = {
+ results: [
+ {
+ name: "Oslo",
+ latitude: 69,
+ longitude: 69,
+ elevation: 420,
+ timezone: "Europe/Oslo",
+ },
+ {
+ name: "Oslo",
+ latitude: 0,
+ longitude: 0,
+ elevation: 10,
+ timezone: "America/Chicago",
+ },
+ ],
+ }
+
+ const fetchMock = vi.fn()
+ fetchMock.mockResolvedValue(
+ new Response(JSON.stringify(mockResponse), {
+ status: 200,
+ headers: { "Content-Type": "application/json" },
+ })
+ )
+
+ vi.stubGlobal("fetch", fetchMock)
+
+ const { result } = renderHook(
+ () => useGetCityCoordinates({ cityName: "Oslo" }),
+ { wrapper: createQueryClientWrapper() }
+ )
+
+ await waitFor(() => {
+ expect(result.current.result).toEqual(mockResponse.results)
+ })
+
+ expect(fetchMock).toHaveBeenCalledTimes(1)
+ expect(fetchMock).toHaveBeenCalledWith(
+ "https://geocoding-api.open-meteo.com/v1/search?name=Oslo"
+ )
+})
diff --git a/tsconfig.app.json b/tsconfig.app.json
index 6830b6f..72c4a37 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -22,5 +22,5 @@
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
- "include": ["src"]
+ "include": ["src", "tests"]
}