diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 98e2661d..fc45846d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -85,4 +85,4 @@ jobs:
run: bun run typecheck
- name: Test
- run: bun test
+ run: bun run test
diff --git a/web/bun.lock b/web/bun.lock
index 42ab7a23..8e3d0980 100644
--- a/web/bun.lock
+++ b/web/bun.lock
@@ -30,7 +30,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"dayjs": "^1.11.21",
- "dompurify": "3.4.11",
+ "dompurify": "3.4.13",
"i18next": "^26.3.4",
"i18next-browser-languagedetector": "^8.2.1",
"input-otp": "^1.4.2",
@@ -71,23 +71,27 @@
"@tanstack/react-query-devtools": "^5.101.2",
"@tanstack/react-router-devtools": "^1.167.0",
"@tanstack/router-plugin": "^1.168.19",
+ "@testing-library/jest-dom": "^7.0.0",
+ "@testing-library/react": "^16.3.2",
+ "@testing-library/user-event": "^14.6.1",
"@types/node": "^26.1.0",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@typescript/native-preview": "^7.0.0-dev.20260702.3",
"@xyflow/react": "^12.11.1",
"embla-carousel-react": "^8.6.0",
- "happy-dom": "^20.11.1",
+ "jsdom": "^29.1.1",
"knip": "^6.24.0",
"oxfmt": "^0.57.0",
"oxlint": "^1.72.0",
"shadcn": "^4.12.0",
+ "vitest": "^4.1.10",
},
},
},
"overrides": {
"brace-expansion": "2.1.1",
- "dompurify": "3.4.11",
+ "dompurify": "3.4.13",
"fast-uri": "3.1.2",
"hono": "4.12.22",
"ip-address": "10.2.0",
@@ -99,6 +103,8 @@
"uuid": "14.0.0",
},
"packages": {
+ "@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="],
+
"@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.23", "", { "dependencies": { "@ai-sdk/provider": "4.0.3", "@ai-sdk/provider-utils": "5.0.11", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-f85diFdPMXYJpxCjOYZchMQkRH8h3r6lhK4Q2xmzJ7UA2OQ80L3W7tFu61742xGQK7zHWm5AhxYhNuc50H9SGQ=="],
"@ai-sdk/provider": ["@ai-sdk/provider@4.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-e0CpNWJUY7OxAFAnCZkw+ri9QOHWwTs1tXP42782KFGCU07qt8NiXCrCVowyCB5dP2r5/Uls+g2oPd8kOJn9dw=="],
@@ -123,6 +129,14 @@
"@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="],
+ "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.11", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@csstools/css-calc": "^3.2.0", "@csstools/css-color-parser": "^4.1.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg=="],
+
+ "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.1.1", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1" } }, "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ=="],
+
+ "@asamuzakjp/generational-cache": ["@asamuzakjp/generational-cache@1.0.1", "", {}, "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg=="],
+
+ "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="],
+
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
"@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
@@ -187,6 +201,8 @@
"@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="],
+ "@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="],
+
"@chevrotain/types": ["@chevrotain/types@11.1.2", "", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="],
"@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g=="],
@@ -207,6 +223,18 @@
"@codemirror/view": ["@codemirror/view@6.43.6", "", { "dependencies": { "@codemirror/state": "^6.7.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA=="],
+ "@csstools/color-helpers": ["@csstools/color-helpers@6.1.0", "", {}, "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg=="],
+
+ "@csstools/css-calc": ["@csstools/css-calc@3.3.0", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ=="],
+
+ "@csstools/css-color-parser": ["@csstools/css-color-parser@4.1.10", "", { "dependencies": { "@csstools/color-helpers": "^6.1.0", "@csstools/css-calc": "^3.3.0" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw=="],
+
+ "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="],
+
+ "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.7", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig=="],
+
+ "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="],
+
"@date-fns/tz": ["@date-fns/tz@1.5.0", "", {}, "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg=="],
"@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="],
@@ -259,6 +287,8 @@
"@emotion/weak-memoize": ["@emotion/weak-memoize@0.4.0", "", {}, "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="],
+ "@exodus/bytes": ["@exodus/bytes@1.15.1", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q=="],
+
"@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="],
"@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="],
@@ -663,6 +693,38 @@
"@resvg/resvg-js-win32-x64-msvc": ["@resvg/resvg-js-win32-x64-msvc@2.4.1", "", { "os": "win32", "cpu": "x64" }, "sha512-vY4kTLH2S3bP+puU5x7hlAxHv+ulFgcK6Zn3efKSr0M0KnZ9A3qeAjZteIpkowEFfUeMPNg2dvvoFRJA9zqxSw=="],
+ "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="],
+
+ "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw=="],
+
+ "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g=="],
+
+ "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA=="],
+
+ "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.5", "", { "os": "linux", "cpu": "arm" }, "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw=="],
+
+ "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q=="],
+
+ "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA=="],
+
+ "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg=="],
+
+ "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA=="],
+
+ "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ=="],
+
+ "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg=="],
+
+ "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.5", "", { "os": "none", "cpu": "arm64" }, "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw=="],
+
+ "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA=="],
+
+ "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw=="],
+
+ "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA=="],
+
+ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
+
"@rsbuild/core": ["@rsbuild/core@2.1.6", "", { "dependencies": { "@rspack/core": "~2.1.4", "@swc/helpers": "^0.5.23" }, "peerDependencies": { "core-js": ">= 3.0.0" }, "optionalPeers": ["core-js"], "bin": { "rsbuild": "./bin/rsbuild.js" } }, "sha512-w2WxblstOgHnDElkqJZVO/jM/EqPaEhg7zqQhON3Xu3Mj9FlVOJx+SgimOtghFzxLt8x7atX4oMUtMTNSZGf0Q=="],
"@rsbuild/plugin-react": ["@rsbuild/plugin-react@2.1.0", "", { "dependencies": { "@rspack/plugin-react-refresh": "^2.0.2", "react-refresh": "^0.18.0" }, "peerDependencies": { "@rsbuild/core": "^2.0.0" }, "optionalPeers": ["@rsbuild/core"] }, "sha512-RQTIAWB/CwPjoWt9iAl+8HixeQVgZ7kEIBrWPCixfITyHdiD84h0YpUTpEUuz6kGHw1KXT9mHZ3Rwy6WG7aRDA=="],
@@ -801,6 +863,14 @@
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.162.0", "", {}, "sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA=="],
+ "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "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" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
+
+ "@testing-library/jest-dom": ["@testing-library/jest-dom@7.0.0", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" }, "peerDependencies": { "@testing-library/dom": ">=10 <11" } }, "sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg=="],
+
+ "@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="],
+
+ "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="],
+
"@tokenlens/core": ["@tokenlens/core@1.3.0", "", {}, "sha512-d8YNHNC+q10bVpi95fELJwJyPVf1HfvBEI18eFQxRSZTdByXrP+f/ZtlhSzkx0Jl0aEmYVeBA5tPeeYRioLViQ=="],
"@tokenlens/fetch": ["@tokenlens/fetch@1.3.0", "", { "dependencies": { "@tokenlens/core": "1.3.0" } }, "sha512-RONDRmETYly9xO8XMKblmrZjKSwCva4s5ebJwQNfNlChZoA5kplPoCgnWceHnn1J1iRjLVlrCNB43ichfmGBKQ=="],
@@ -827,6 +897,10 @@
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
+ "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
+
+ "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
+
"@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="],
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
@@ -891,6 +965,8 @@
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
+ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
+
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
@@ -985,6 +1061,20 @@
"@visactor/vutils-extension": ["@visactor/vutils-extension@2.1.4", "", { "dependencies": { "@visactor/vdataset": "~1.0.23", "@visactor/vutils": "~1.0.23" } }, "sha512-gyTxhrTN0Ybzis4FsHniTNQSKo9GWLT2f+0du3E6wEDQ/PijNRS6onkY3HyBXZbsnJlvk/sMZUqrtVJi3cTvzg=="],
+ "@vitest/expect": ["@vitest/expect@4.1.10", "", { "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" } }, "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA=="],
+
+ "@vitest/mocker": ["@vitest/mocker@4.1.10", "", { "dependencies": { "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow=="],
+
+ "@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="],
+
+ "@vitest/runner": ["@vitest/runner@4.1.10", "", { "dependencies": { "@vitest/utils": "4.1.10", "pathe": "^2.0.3" } }, "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg=="],
+
+ "@vitest/snapshot": ["@vitest/snapshot@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw=="],
+
+ "@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="],
+
+ "@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="],
+
"@workflow/serde": ["@workflow/serde@4.1.0", "", {}, "sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ=="],
"@xyflow/react": ["@xyflow/react@12.11.2", "", { "dependencies": { "@xyflow/system": "0.0.79", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { "@types/react": ">=17", "@types/react-dom": ">=17", "react": ">=17", "react-dom": ">=17" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-eLAlDWJfWnQEhJwGMjlWdAXO9eYllKpliUmPQlAmOLxz6mExXuzMVDUKLMquixgkrtmMFFtug3jGKmYYld12cA=="],
@@ -1011,7 +1101,9 @@
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
- "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
+ "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+ "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
"ansis": ["ansis@4.3.1", "", {}, "sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA=="],
@@ -1023,8 +1115,12 @@
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
+ "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
+
"array-source": ["array-source@0.0.4", "", {}, "sha512-frNdc+zBn80vipY+GdcJkLEbMWj3xmzArYApmUGxoiV8uAu/ygcs9icPdsGdA26h0MkHUMW6EN2piIvVx+M5Mw=="],
+ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
+
"assign-symbols": ["assign-symbols@1.0.0", "", {}, "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw=="],
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
@@ -1051,6 +1147,8 @@
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.43", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ=="],
+ "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
+
"body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="],
"brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="],
@@ -1077,6 +1175,8 @@
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
+ "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
+
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
@@ -1151,6 +1251,10 @@
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+ "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
+
+ "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="],
+
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
@@ -1229,6 +1333,8 @@
"dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="],
+ "data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="],
+
"date-fns": ["date-fns@4.4.0", "", {}, "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w=="],
"dayjs": ["dayjs@1.11.21", "", {}, "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA=="],
@@ -1237,6 +1343,8 @@
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+ "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
+
"decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
@@ -1269,7 +1377,9 @@
"diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="],
- "dompurify": ["dompurify@3.4.11", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw=="],
+ "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
+
+ "dompurify": ["dompurify@3.4.13", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ=="],
"dot-prop": ["dot-prop@6.0.1", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA=="],
@@ -1297,7 +1407,7 @@
"enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="],
- "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
+ "entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="],
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
@@ -1307,6 +1417,8 @@
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
+ "es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="],
+
"es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
@@ -1349,6 +1461,8 @@
"execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="],
+ "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="],
+
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
"express-rate-limit": ["express-rate-limit@8.6.0", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA=="],
@@ -1401,6 +1515,8 @@
"fs-extra": ["fs-extra@11.3.6", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA=="],
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="],
@@ -1487,6 +1603,8 @@
"hono": ["hono@4.12.22", "", {}, "sha512-7fvVPbB92zNRsQke+uiRGwtTuef0tB2Dg4hWxYfFNvkQhIltWoyi0ONReM5LWA+jJWS3nfT5lTq+qbsIpX0IQw=="],
+ "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
+
"html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="],
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
@@ -1515,6 +1633,8 @@
"import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
+ "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
+
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
@@ -1565,6 +1685,8 @@
"is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="],
+ "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
+
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"is-regexp": ["is-regexp@3.1.0", "", {}, "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA=="],
@@ -1595,6 +1717,8 @@
"js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="],
+ "jsdom": ["jsdom@29.1.1", "", { "dependencies": { "@asamuzakjp/css-color": "^5.1.11", "@asamuzakjp/dom-selector": "^7.1.1", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.3", "@exodus/bytes": "^1.15.0", "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.3.5", "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.1", "undici": "^7.25.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q=="],
+
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
@@ -1671,12 +1795,14 @@
"lottie-web": ["lottie-web@5.13.0", "", {}, "sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ=="],
- "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
+ "lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="],
"lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="],
"lucide-react": ["lucide-react@1.25.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-/mdJTRbiwcLOQ1NZZK1amZF9rIZyvO18D6r9TngE6TG1NmqHgFuT4eE7Xrkm9UsXMbBJD1NlfwHVltCDWHrOTw=="],
+ "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
+
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
@@ -1741,6 +1867,8 @@
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
+ "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
+
"mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
@@ -1841,6 +1969,8 @@
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
+ "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="],
+
"minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
@@ -1873,6 +2003,8 @@
"object-treeify": ["object-treeify@1.1.33", "", {}, "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A=="],
+ "obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="],
+
"on-change": ["on-change@4.0.0", "", {}, "sha512-PTu7C9Jsz4b+sNMDpH0eZFTr7uxdOtoDWRnhaVNK50bgrrnW5nvbWI0jm5DG9qOoTnIhBzE9xoKVFPD9xgtbdg=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
@@ -1915,7 +2047,7 @@
"parse-svg-path": ["parse-svg-path@0.1.2", "", {}, "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ=="],
- "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
+ "parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
@@ -1963,6 +2095,8 @@
"prettier": ["prettier@3.9.5", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg=="],
+ "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
+
"pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="],
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
@@ -1977,6 +2111,8 @@
"proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="],
+ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
+
"punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="],
"qrcode.react": ["qrcode.react@4.2.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA=="],
@@ -2081,6 +2217,8 @@
"recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="],
+ "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="],
+
"redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="],
"redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="],
@@ -2139,6 +2277,8 @@
"robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
+ "rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="],
+
"roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
@@ -2153,6 +2293,8 @@
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
+ "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
+
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"screenfull": ["screenfull@5.2.0", "", {}, "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA=="],
@@ -2193,6 +2335,8 @@
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
+ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
+
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"simple-statistics": ["simple-statistics@7.9.3", "", {}, "sha512-WXpxUfo7BJCRpyl4besiuMV7wNj9xiPIq7IKmUQO4upIaF8pK2AXwhjttHN5L8KXZrLkGMCHGHq4p+pJXiIahQ=="],
@@ -2221,8 +2365,12 @@
"sse.js": ["sse.js@2.8.0", "", {}, "sha512-35RyyFYpzzHZgMw9D5GxwADbL6gnntSwW/rKXcuIy1KkYCPjW6oia0moNdNRhs34oVHU1Sjgovj3l7uIEZjrKA=="],
+ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
+
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
+ "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="],
+
"stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="],
"stream-markdown-parser": ["stream-markdown-parser@1.1.3", "", { "dependencies": { "markdown-it-container": "^4.0.0", "markdown-it-footnote": "^4.0.0", "markdown-it-ins": "^4.0.0", "markdown-it-mark": "^4.0.0", "markdown-it-sub": "^2.0.0", "markdown-it-sup": "^2.0.0", "markdown-it-task-checkbox": "^1.0.6", "markdown-it-ts": "^1.0.4" } }, "sha512-tge6aKbOGU36vBLeeroHGBAJ7qiNcsTYyvmgqevVN0+5kMiLOjAyeDmv3PQkniTMbgOsswVHN9izmgmSw5bsBQ=="],
@@ -2245,6 +2393,8 @@
"strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
+ "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
+
"strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="],
"style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
@@ -2259,6 +2409,8 @@
"swr": ["swr@2.4.2", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-ej644Y2bvkIajfR32KGeSSdBXQW+ScjGjkybZgSE7kFpk9eGnV44XY9FJylXi+W75pavSX1PVNB57W5EbhGIYw=="],
+ "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
+
"systeminformation": ["systeminformation@5.33.0", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-0LYSL01CCbjVeJG7iXI8fUCFU76zMjzbHd/EU3or4QpSFYCLMgslR11prwHuA3siz5jmOkqoLhjgOyDRmXBKmA=="],
"tabbable": ["tabbable@6.5.0", "", {}, "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA=="],
@@ -2275,12 +2427,20 @@
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
+ "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
+
"tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="],
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="],
+ "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="],
+
+ "tldts": ["tldts@7.4.9", "", { "dependencies": { "tldts-core": "^7.4.9" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA=="],
+
+ "tldts-core": ["tldts-core@7.4.9", "", {}, "sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg=="],
+
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"to-vfile": ["to-vfile@8.0.0", "", { "dependencies": { "vfile": "^6.0.0" } }, "sha512-IcmH1xB5576MJc9qcfEC/m/nQCFt3fzMHz45sSlgJyTWjRbKW1HAkJpuf3DgE57YzIlZcwcBZA5ENQbBo4aLkg=="],
@@ -2293,6 +2453,10 @@
"topojson-server": ["topojson-server@3.0.1", "", { "dependencies": { "commander": "2" }, "bin": { "geo2topo": "bin/geo2topo" } }, "sha512-/VS9j/ffKr2XAOjlZ9CgyyeLmgJ9dMwq6Y0YEON8O7p/tGGk+dCWnrE03zEdu7i4L7YsFZLEPZPzCvcB7lEEXw=="],
+ "tough-cookie": ["tough-cookie@6.0.2", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA=="],
+
+ "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="],
+
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
@@ -2383,26 +2547,42 @@
"virtua": ["virtua@0.49.3", "", { "peerDependencies": { "react": ">=16.14.0", "react-dom": ">=16.14.0", "solid-js": ">=1.0", "svelte": ">=5.0", "vue": ">=3.2" }, "optionalPeers": ["react", "react-dom", "solid-js", "svelte", "vue"] }, "sha512-k1Yn988Vz/L40uDtEWPjfdVo15Suumh4tU4/z5Srs0elNcU9DgBskqdh3llpHyAlXrCeHWTfcclYvY1uU4MmIg=="],
+ "vite": ["vite@8.1.5", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.17", "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw=="],
+
+ "vitest": ["vitest@4.1.10", "", { "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" }, "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": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw=="],
+
"void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="],
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
+ "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
+
"walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="],
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
+ "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="],
+
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
- "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
+ "whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="],
+
+ "whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="],
"which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
+ "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
+
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"ws": ["ws@8.21.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="],
"wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
+ "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
+
+ "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
+
"yace": ["yace@1.1.0", "", {}, "sha512-jB29trAxPBvTIR/lXsgh97q22n/Rq5ZHbDT4DT6WUvXVlJLR7jdwiBKD+0zi3XZAD5N+YKa3GrdIrelqIXj2oQ=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
@@ -2421,6 +2601,8 @@
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
+ "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
+
"@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
"@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
@@ -2483,6 +2665,10 @@
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+ "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
+
+ "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
+
"@visactor/vdataset/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
"@visactor/vlayouts/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
@@ -2535,6 +2721,14 @@
"geojson-dissolve/@turf/meta": ["@turf/meta@3.14.0", "", {}, "sha512-OtXqLQuR9hlQ/HkAF/OdzRea7E0eZK1ay8y8CBXkoO2R6v34CsDrWYLMSo0ZzMsaQDpKo76NPP2GGo+PyG1cSg=="],
+ "happy-dom/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
+
+ "happy-dom/whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
+
+ "hast-util-from-html/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
+
+ "hast-util-raw/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
+
"hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
@@ -2563,10 +2757,10 @@
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
- "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
-
"postcss/nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="],
+ "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
+
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
@@ -2579,6 +2773,8 @@
"restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
+ "rolldown/@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="],
+
"send/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"set-value/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
@@ -2591,6 +2787,8 @@
"split-string/extend-shallow": ["extend-shallow@3.0.2", "", { "dependencies": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" } }, "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q=="],
+ "strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
+
"topojson-client/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"topojson-server/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
@@ -2633,10 +2831,12 @@
"d3/d3-dsv/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
- "enquirer/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
-
"express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
+ "hast-util-from-html/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
+
+ "hast-util-raw/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
+
"mermaid/katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
"micromark-extension-math/katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
diff --git a/web/package.json b/web/package.json
index d96a6c4b..2893f36b 100644
--- a/web/package.json
+++ b/web/package.json
@@ -11,6 +11,8 @@
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint -c .oxlintrc.json . --fix",
"preview": "rsbuild preview",
+ "test": "vitest run",
+ "test:watch": "vitest",
"format:check": "node scripts/format-with-protected-headers.mjs --check",
"format": "node scripts/format-with-protected-headers.mjs --write",
"copyright:check": "node scripts/add-copyright.mjs --check",
@@ -85,17 +87,21 @@
"@tanstack/react-query-devtools": "^5.101.2",
"@tanstack/react-router-devtools": "^1.167.0",
"@tanstack/router-plugin": "^1.168.19",
+ "@testing-library/jest-dom": "^7.0.0",
+ "@testing-library/react": "^16.3.2",
+ "@testing-library/user-event": "^14.6.1",
"@types/node": "^26.1.0",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@typescript/native-preview": "^7.0.0-dev.20260702.3",
"@xyflow/react": "^12.11.1",
"embla-carousel-react": "^8.6.0",
- "happy-dom": "^20.11.1",
+ "jsdom": "^29.1.1",
"knip": "^6.24.0",
"oxfmt": "^0.57.0",
"oxlint": "^1.72.0",
- "shadcn": "^4.12.0"
+ "shadcn": "^4.12.0",
+ "vitest": "^4.1.10"
},
"overrides": {
"brace-expansion": "2.1.1",
diff --git a/web/src/components/json-code-editor/__tests__/json-code-editor-utils.test.ts b/web/src/components/json-code-editor/__tests__/json-code-editor-utils.test.ts
index bbac95fd..00651f90 100644
--- a/web/src/components/json-code-editor/__tests__/json-code-editor-utils.test.ts
+++ b/web/src/components/json-code-editor/__tests__/json-code-editor-utils.test.ts
@@ -16,8 +16,7 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { describe, test } from 'node:test'
+import { describe, expect, test } from 'vitest'
import {
applyJsonSmartEnter,
@@ -29,42 +28,42 @@ import {
describe('json code editor utils', () => {
test('treats empty drafts as valid editable JSON drafts', () => {
- assert.deepEqual(getJsonValidationState(' \n'), {
+ expect(getJsonValidationState(' \n')).toEqual({
isValid: true,
messageKey: 'JSON',
})
})
test('reports invalid JSON without throwing away the draft', () => {
- assert.deepEqual(getJsonValidationState('{"model": }'), {
+ expect(getJsonValidationState('{"model": }')).toEqual({
isValid: false,
messageKey: 'Invalid JSON',
})
})
test('formats valid JSON with stable two-space indentation', () => {
- assert.deepEqual(formatJsonDraft('{"model":{"ratio":2}}'), {
+ expect(formatJsonDraft('{"model":{"ratio":2}}')).toEqual({
didFormat: true,
value: '{\n "model": {\n "ratio": 2\n }\n}',
})
})
test('keeps invalid JSON drafts unchanged when formatting is requested', () => {
- assert.deepEqual(formatJsonDraft('{"model": }'), {
+ expect(formatJsonDraft('{"model": }')).toEqual({
didFormat: false,
value: '{"model": }',
})
})
test('derives the one-based cursor line and column from text offsets', () => {
- assert.deepEqual(getCursorLocation('{\n "model": 1\n}', 5), {
+ expect(getCursorLocation('{\n "model": 1\n}', 5)).toEqual({
line: 2,
column: 4,
})
})
test('expands paired JSON brackets with a nested indentation line', () => {
- assert.deepEqual(applyJsonSmartEnter('{}', 1, 1), {
+ expect(applyJsonSmartEnter('{}', 1, 1)).toEqual({
value: '{\n \n}',
selectionStart: 4,
selectionEnd: 4,
@@ -90,11 +89,11 @@ describe('json code editor utils', () => {
source.scrollTop = 80
synchronizer.sync()
- assert.equal(queuedFrames.length, 1)
+ expect(queuedFrames.length).toBe(1)
queuedFrames[0]()
- assert.equal(contentLayer.style.transform, 'translate3d(-24px, -80px, 0)')
- assert.equal(lineNumberLayer.style.transform, 'translate3d(0, -80px, 0)')
+ expect(contentLayer.style.transform).toBe('translate3d(-24px, -80px, 0)')
+ expect(lineNumberLayer.style.transform).toBe('translate3d(0, -80px, 0)')
})
})
diff --git a/web/src/components/json-code-editor/__tests__/json-code-editor.test.tsx b/web/src/components/json-code-editor/__tests__/json-code-editor.test.tsx
index 24959b2c..9d4b5379 100644
--- a/web/src/components/json-code-editor/__tests__/json-code-editor.test.tsx
+++ b/web/src/components/json-code-editor/__tests__/json-code-editor.test.tsx
@@ -16,165 +16,98 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { after, describe, test } from 'node:test'
+import { fireEvent, render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { describe, expect, test, vi } from 'vitest'
-import { Window } from 'happy-dom'
-
-const domWindow = new Window()
-const domGlobals = [
- 'window',
- 'document',
- 'navigator',
- 'HTMLElement',
- 'HTMLTextAreaElement',
- 'Node',
- 'Element',
- 'Event',
- 'CustomEvent',
- 'MutationObserver',
- 'requestAnimationFrame',
- 'cancelAnimationFrame',
- 'getComputedStyle',
-] as const
-
-for (const key of domGlobals) {
- Object.defineProperty(globalThis, key, {
- configurable: true,
- value: domWindow[key],
- })
-}
-
-const { act } = await import('react')
-const { createRoot } = await import('react-dom/client')
-const i18next = (await import('i18next')).default
-const { initReactI18next } = await import('react-i18next')
-await i18next.use(initReactI18next).init({
- lng: 'en',
- resources: {
- en: {
- translation: {
- JSON: 'JSON',
- 'Invalid JSON': 'Invalid JSON',
- 'Copied to clipboard': 'Copied to clipboard',
- 'Failed to copy': 'Failed to copy',
- 'Format JSON': 'Format JSON',
- },
- },
- },
-})
-const { JsonCodeEditor } = await import('../../json-code-editor')
-const reactTestGlobals = globalThis as typeof globalThis & {
- IS_REACT_ACT_ENVIRONMENT?: boolean
-}
-reactTestGlobals.IS_REACT_ACT_ENVIRONMENT = true
-
-type RenderedEditor = {
- container: HTMLDivElement
- root: ReturnType
-}
-
-async function renderEditor(
- props: React.ComponentProps
-): Promise {
- const container = document.createElement('div')
- document.body.append(container)
- const root = createRoot(container)
-
- await act(async () => {
- root.render()
- })
-
- return { container, root }
-}
-
-async function unmountEditor(rendered: RenderedEditor) {
- await act(async () => rendered.root.unmount())
- rendered.container.remove()
-}
+import { JsonCodeEditor } from '../../json-code-editor'
describe('JsonCodeEditor component', () => {
- after(() => {
- domWindow.close()
+ test('forwards form attributes and the textarea ref', () => {
+ const textareaRef = vi.fn()
+ const rendered = render(
+ undefined}
+ id='json-input'
+ name='model_config'
+ placeholder='{"model":"gpt"}'
+ disabled
+ ariaLabel='Model configuration'
+ aria-describedby='model-help'
+ aria-invalid
+ data-form-root='settings-form'
+ textareaRef={textareaRef}
+ />
+ )
+ const textarea = screen.getByRole('textbox', {
+ name: 'Model configuration',
+ })
+
+ expect(textarea).toHaveAttribute('id', 'json-input')
+ expect(textarea).toHaveAttribute('name', 'model_config')
+ expect(textarea).toHaveAttribute('placeholder', '{"model":"gpt"}')
+ expect(textarea).toBeDisabled()
+ expect(textarea).toHaveAttribute('aria-describedby', 'model-help')
+ expect(textarea).toHaveAttribute('aria-invalid', 'true')
+ expect(textarea).toHaveAttribute('data-form-root', 'settings-form')
+ expect(textareaRef).toHaveBeenCalledWith(textarea)
+
+ rendered.unmount()
+ expect(textareaRef).toHaveBeenLastCalledWith(null)
})
- test('forwards form attributes and lifecycle callbacks to the textarea', async () => {
- const blurCalls: number[] = []
- const refValues: Array = []
- const rendered = await renderEditor({
- value: '{"model":"gpt"}',
- onChange: () => undefined,
- id: 'json-input',
- name: 'model_config',
- placeholder: '{"model":"gpt"}',
- disabled: true,
- 'aria-describedby': 'model-help',
- 'aria-invalid': true,
- 'data-form-root': 'settings-form',
- onBlur: () => blurCalls.push(1),
- textareaRef: (element) => refValues.push(element),
- })
- const textarea = rendered.container.querySelector('textarea')
+ test('calls onBlur when focus leaves the editor', () => {
+ const onBlur = vi.fn()
+ render(
+ undefined}
+ onBlur={onBlur}
+ ariaLabel='Model configuration'
+ />
+ )
- assert.ok(textarea)
- assert.equal(textarea.id, 'json-input')
- assert.equal(textarea.name, 'model_config')
- assert.equal(textarea.placeholder, '{"model":"gpt"}')
- assert.equal(textarea.disabled, true)
- assert.equal(textarea.getAttribute('aria-describedby'), 'model-help')
- assert.equal(textarea.getAttribute('aria-invalid'), 'true')
- assert.equal(textarea.getAttribute('data-form-root'), 'settings-form')
+ fireEvent.blur(screen.getByRole('textbox', { name: 'Model configuration' }))
- await act(async () => textarea.dispatchEvent(new Event('blur')))
- assert.deepEqual(blurCalls, [1])
- assert.equal(refValues[0], textarea)
-
- await unmountEditor(rendered)
- assert.equal(refValues.at(-1), null)
+ expect(onBlur).toHaveBeenCalledOnce()
})
- test('emits user edits and synchronizes a controlled value', async () => {
- const changes: string[] = []
- const rendered = await renderEditor({
- value: '{"count":1}',
- onChange: (value) => changes.push(value),
+ test('emits user edits and synchronizes a controlled value', () => {
+ const onChange = vi.fn()
+ const rendered = render(
+
+ )
+ const textarea = screen.getByRole('textbox', {
+ name: 'Model configuration',
})
- const textarea = rendered.container.querySelector('textarea')
- assert.ok(textarea)
- await act(async () => {
- textarea.value = '{"count":2}'
- textarea.dispatchEvent(new Event('input', { bubbles: true }))
- })
- assert.deepEqual(changes, ['{"count":2}'])
+ fireEvent.input(textarea, { target: { value: '{"count":2}' } })
+ expect(onChange).toHaveBeenCalledWith('{"count":2}')
- await act(async () => {
- rendered.root.render(
- changes.push(value)}
- />
- )
- })
- assert.equal(textarea.value, '{"count":3}')
-
- await unmountEditor(rendered)
+ rendered.rerender(
+
+ )
+ expect(textarea).toHaveValue('{"count":3}')
})
test('formats valid JSON through the public toolbar action', async () => {
- const changes: string[] = []
- const rendered = await renderEditor({
- value: '{"model":{"ratio":2}}',
- onChange: (value) => changes.push(value),
- })
- const formatButton = [
- ...rendered.container.querySelectorAll('button'),
- ].find((button) => button.textContent?.includes('Format JSON'))
+ const user = userEvent.setup()
+ const onChange = vi.fn()
+ render()
- assert.ok(formatButton)
- await act(async () => formatButton.click())
- assert.deepEqual(changes, ['{\n "model": {\n "ratio": 2\n }\n}'])
+ await user.click(screen.getByRole('button', { name: 'Format JSON' }))
- await unmountEditor(rendered)
+ expect(onChange).toHaveBeenCalledWith(
+ '{\n "model": {\n "ratio": 2\n }\n}'
+ )
})
})
diff --git a/web/src/components/model-group-selector/__tests__/layout.test.ts b/web/src/components/model-group-selector/__tests__/layout.test.ts
index ef5b7e6f..db60dbf6 100644
--- a/web/src/components/model-group-selector/__tests__/layout.test.ts
+++ b/web/src/components/model-group-selector/__tests__/layout.test.ts
@@ -16,8 +16,7 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { describe, test } from 'node:test'
+import { describe, expect, test } from 'vitest'
import {
modelGroupSelectorLayoutClasses,
@@ -29,8 +28,8 @@ describe('model group selector layout', () => {
const groupScrollClasses =
modelGroupSelectorLayoutClasses.groupScroll.split(' ')
- assert.ok(groupScrollClasses.includes('auto-rows-[2rem]'))
- assert.ok(groupScrollClasses.includes('content-start'))
+ expect(groupScrollClasses.includes('auto-rows-[2rem]')).toBeTruthy()
+ expect(groupScrollClasses.includes('content-start')).toBeTruthy()
})
test('centers the selected group inside its own scroll container', () => {
@@ -50,7 +49,7 @@ describe('model group selector layout', () => {
scrollSelectedOptionIntoView(selectedOption, scrollContainer)
- assert.deepEqual(scrollCalls, [{ top: 76, behavior: 'auto' }])
+ expect(scrollCalls).toEqual([{ top: 76, behavior: 'auto' }])
})
test('falls back to scrollIntoView when no group container is provided', () => {
@@ -63,6 +62,6 @@ describe('model group selector layout', () => {
scrollSelectedOptionIntoView(selectedOption)
- assert.deepEqual(scrollCalls, [{ block: 'center', inline: 'nearest' }])
+ expect(scrollCalls).toEqual([{ block: 'center', inline: 'nearest' }])
})
})
diff --git a/web/src/components/ui/dropdown-menu.test.tsx b/web/src/components/ui/dropdown-menu.test.tsx
index 71d6afe9..8a3e2d9d 100644
--- a/web/src/components/ui/dropdown-menu.test.tsx
+++ b/web/src/components/ui/dropdown-menu.test.tsx
@@ -16,8 +16,7 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { describe, test } from 'node:test'
+import { describe, expect, test } from 'vitest'
import { handleDropdownMenuItemSelect } from './dropdown-menu-events'
@@ -52,8 +51,8 @@ describe('DropdownMenuItem onSelect compatibility', () => {
selected = true
})
- assert.equal(selected, true)
- assert.equal(event.baseUIHandlerPrevented, false)
+ expect(selected).toBe(true)
+ expect(event.baseUIHandlerPrevented).toBe(false)
})
test('keeps the Base UI menu open when onSelect prevents default', () => {
@@ -63,7 +62,7 @@ describe('DropdownMenuItem onSelect compatibility', () => {
selectEvent.preventDefault()
})
- assert.equal(event.defaultPrevented, true)
- assert.equal(event.baseUIHandlerPrevented, true)
+ expect(event.defaultPrevented).toBe(true)
+ expect(event.baseUIHandlerPrevented).toBe(true)
})
})
diff --git a/web/src/features/auth/api.test.ts b/web/src/features/auth/api.test.ts
index 455b6886..c3fcb894 100644
--- a/web/src/features/auth/api.test.ts
+++ b/web/src/features/auth/api.test.ts
@@ -16,8 +16,7 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { describe, test } from 'node:test'
+import { describe, expect, test } from 'vitest'
import type { RefreshOutcome } from '@/lib/api'
import type { AuthBundle } from '@/stores/auth-store'
@@ -63,8 +62,8 @@ describe('logout coordination', () => {
},
})
- assert.deepEqual(result, { success: false, message: 'not revoked' })
- assert.equal(refreshCount, 0)
+ expect(result).toEqual({ success: false, message: 'not revoked' })
+ expect(refreshCount).toBe(0)
})
test('recovers a cookie mismatch and retries with the refreshed SID', async () => {
@@ -83,8 +82,8 @@ describe('logout coordination', () => {
},
})
- assert.deepEqual(result, { success: true, message: '' })
- assert.deepEqual(requestedSIDs, ['session-a', 'session-b'])
+ expect(result).toEqual({ success: true, message: '' })
+ expect(requestedSIDs).toEqual(['session-a', 'session-b'])
})
test('treats a mismatch that refresh confirms anonymous as signed out', async () => {
@@ -96,7 +95,7 @@ describe('logout coordination', () => {
refresh: async () => ({ kind: 'anonymous' }),
})
- assert.deepEqual(result, { success: true, message: '' })
+ expect(result).toEqual({ success: true, message: '' })
})
test('preserves the active session when mismatch recovery is temporary', async () => {
@@ -106,15 +105,14 @@ describe('logout coordination', () => {
error: new Error('offline'),
}
- await assert.rejects(
+ await expect(
executeLogout({
getExpectedSID: () => 'session-a',
request: async () => {
throw originalError
},
refresh: async () => transient,
- }),
- (error) => error === originalError
- )
+ })
+ ).rejects.toBe(originalError)
})
})
diff --git a/web/src/features/auth/lib/__tests__/oauth-callback-mode.test.ts b/web/src/features/auth/lib/__tests__/oauth-callback-mode.test.ts
index 180e7bf0..75b87c79 100644
--- a/web/src/features/auth/lib/__tests__/oauth-callback-mode.test.ts
+++ b/web/src/features/auth/lib/__tests__/oauth-callback-mode.test.ts
@@ -16,8 +16,7 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { describe, test } from 'node:test'
+import { describe, expect, test } from 'vitest'
import {
getOAuthSessionStorage,
@@ -40,15 +39,14 @@ const bindState = 'bind-state'
describe('resolveOAuthCallbackMode', () => {
test('matching provider and state mark is treated as a bind flow', () => {
const storage = fakeStorage()
- assert.equal(markOAuthBindPopup(storage, 'oidc', bindState), true)
+ expect(markOAuthBindPopup(storage, 'oidc', bindState)).toBe(true)
- assert.equal(
+ expect(
resolveOAuthCallbackMode('oidc', bindState, {
opener: openOpener,
storage,
- }),
- 'bind'
- )
+ })
+ ).toBe('bind')
})
// Regression: a tab opened from an external link (Slack, e-mail, another
@@ -58,75 +56,69 @@ describe('resolveOAuthCallbackMode', () => {
test('login redirect in a tab with a foreign opener stays a login flow', () => {
const storage = fakeStorage()
- assert.equal(
+ expect(
resolveOAuthCallbackMode('oidc', bindState, {
opener: openOpener,
storage,
- }),
- 'login'
- )
+ })
+ ).toBe('login')
})
test('bind marker for another provider does not hijack this callback', () => {
const storage = fakeStorage()
markOAuthBindPopup(storage, 'github', bindState)
- assert.equal(
+ expect(
resolveOAuthCallbackMode('oidc', bindState, {
opener: openOpener,
storage,
- }),
- 'login'
- )
+ })
+ ).toBe('login')
})
test('stale bind marker does not hijack a later callback', () => {
const storage = fakeStorage()
markOAuthBindPopup(storage, 'oidc', 'previous-state')
- assert.equal(
+ expect(
resolveOAuthCallbackMode('oidc', bindState, {
opener: openOpener,
storage,
- }),
- 'login'
- )
+ })
+ ).toBe('login')
})
test('bind marker without an opener falls back to login', () => {
const storage = fakeStorage()
markOAuthBindPopup(storage, 'oidc', bindState)
- assert.equal(
+ expect(
resolveOAuthCallbackMode('oidc', bindState, {
opener: null,
storage,
- }),
- 'login'
- )
+ })
+ ).toBe('login')
})
test('closed opener falls back to login', () => {
const storage = fakeStorage()
markOAuthBindPopup(storage, 'oidc', bindState)
- assert.equal(
+ expect(
resolveOAuthCallbackMode('oidc', bindState, {
opener: { closed: true },
storage,
- }),
- 'login'
- )
+ })
+ ).toBe('login')
})
test('missing storage degrades to login instead of throwing', () => {
- assert.equal(
+ expect(
resolveOAuthCallbackMode('oidc', bindState, {
opener: openOpener,
storage: null,
- }),
- 'login'
- )
+ })
+ ).toBe('login')
})
test('storage read failure degrades to login instead of throwing', () => {
@@ -137,13 +129,12 @@ describe('resolveOAuthCallbackMode', () => {
setItem: () => undefined,
}
- assert.equal(
+ expect(
resolveOAuthCallbackMode('oidc', bindState, {
opener: openOpener,
storage,
- }),
- 'login'
- )
+ })
+ ).toBe('login')
})
})
@@ -155,7 +146,7 @@ describe('OAuth bind popup storage', () => {
},
}
- assert.equal(getOAuthSessionStorage(owner), null)
+ expect(getOAuthSessionStorage(owner)).toBe(null)
})
test('marking reports unavailable or unwritable storage', () => {
@@ -166,9 +157,9 @@ describe('OAuth bind popup storage', () => {
},
}
- assert.equal(markOAuthBindPopup(null, 'oidc', bindState), false)
- assert.equal(markOAuthBindPopup(storage, 'oidc', bindState), false)
- assert.equal(
+ expect(markOAuthBindPopup(null, 'oidc', bindState)).toBe(false)
+ expect(markOAuthBindPopup(storage, 'oidc', bindState)).toBe(false)
+ expect(
markOAuthBindPopup(
{
getItem: () => null,
@@ -176,8 +167,7 @@ describe('OAuth bind popup storage', () => {
},
'oidc',
bindState
- ),
- false
- )
+ )
+ ).toBe(false)
})
})
diff --git a/web/src/features/auth/lib/auth-redirect.test.ts b/web/src/features/auth/lib/auth-redirect.test.ts
index f043c26b..550223db 100644
--- a/web/src/features/auth/lib/auth-redirect.test.ts
+++ b/web/src/features/auth/lib/auth-redirect.test.ts
@@ -16,8 +16,7 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { describe, test } from 'node:test'
+import { describe, expect, test } from 'vitest'
import type { AuthUser } from '@/stores/auth-store'
@@ -27,17 +26,15 @@ const origin = 'https://dashboard.example.com'
describe('authentication redirect validation', () => {
test('preserves safe internal paths, search parameters, and fragments', () => {
- assert.equal(
- sanitizeAuthRedirect('/console?tab=usage#recent', origin),
+ expect(sanitizeAuthRedirect('/console?tab=usage#recent', origin)).toBe(
'/console?tab=usage#recent'
)
- assert.equal(
+ expect(
sanitizeAuthRedirect(
'https://dashboard.example.com/dashboard?tab=quota#daily',
origin
- ),
- '/dashboard?tab=quota#daily'
- )
+ )
+ ).toBe('/dashboard?tab=quota#daily')
})
test('rejects external and ambiguously parsed redirect targets', () => {
@@ -53,13 +50,13 @@ describe('authentication redirect validation', () => {
]
for (const target of unsafeTargets) {
- assert.equal(sanitizeAuthRedirect(target, origin), null)
+ expect(sanitizeAuthRedirect(target, origin)).toBe(null)
}
})
test('rejects invalid or non-HTTP application origins', () => {
- assert.equal(sanitizeAuthRedirect('/dashboard', 'not-an-origin'), null)
- assert.equal(sanitizeAuthRedirect('/dashboard', 'file:///tmp/app'), null)
+ expect(sanitizeAuthRedirect('/dashboard', 'not-an-origin')).toBe(null)
+ expect(sanitizeAuthRedirect('/dashboard', 'file:///tmp/app')).toBe(null)
})
})
@@ -67,31 +64,27 @@ describe('saved authentication language', () => {
const user: AuthUser = { id: 1, username: 'user', role: 1 }
test('prefers the explicit user language', () => {
- assert.equal(
+ expect(
getSavedLanguage({
...user,
language: 'ja',
setting: { language: 'fr' },
- }),
- 'ja'
- )
+ })
+ ).toBe('ja')
})
test('reads object and JSON string settings', () => {
- assert.equal(
- getSavedLanguage({ ...user, setting: { language: 'fr' } }),
+ expect(getSavedLanguage({ ...user, setting: { language: 'fr' } })).toBe(
'fr'
)
- assert.equal(
- getSavedLanguage({ ...user, setting: '{"language":"ru"}' }),
+ expect(getSavedLanguage({ ...user, setting: '{"language":"ru"}' })).toBe(
'ru'
)
})
test('ignores malformed and non-string setting languages', () => {
- assert.equal(getSavedLanguage({ ...user, setting: '{' }), undefined)
- assert.equal(
- getSavedLanguage({ ...user, setting: { language: 123 } }),
+ expect(getSavedLanguage({ ...user, setting: '{' })).toBe(undefined)
+ expect(getSavedLanguage({ ...user, setting: { language: 123 } })).toBe(
undefined
)
})
diff --git a/web/src/features/auth/lib/oauth-bind-window.test.ts b/web/src/features/auth/lib/oauth-bind-window.test.ts
index 3ef4ba43..54a33385 100644
--- a/web/src/features/auth/lib/oauth-bind-window.test.ts
+++ b/web/src/features/auth/lib/oauth-bind-window.test.ts
@@ -16,8 +16,7 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { describe, test } from 'node:test'
+import { describe, expect, test } from 'vitest'
import {
parseTelegramBindCallback,
@@ -51,51 +50,48 @@ function fakeTimerRuntime() {
describe('OAuth bind popup lifecycle', () => {
test('parses Telegram success and stable error callbacks', () => {
- assert.deepEqual(
+ expect(
parseTelegramBindCallback({
telegram_bind: 'success',
flow_token: 'flow-success',
- }),
- {
- kind: 'result',
- flowToken: 'flow-success',
- success: true,
- }
- )
- assert.deepEqual(
+ })
+ ).toEqual({
+ kind: 'result',
+ flowToken: 'flow-success',
+ success: true,
+ })
+ expect(
parseTelegramBindCallback({
telegram_bind: 'error',
flow_token: 'flow-error',
error_code: 'TELEGRAM_BIND_ALREADY_BOUND',
- }),
- {
- kind: 'result',
- flowToken: 'flow-error',
- success: false,
- code: 'TELEGRAM_BIND_ALREADY_BOUND',
- }
- )
+ })
+ ).toEqual({
+ kind: 'result',
+ flowToken: 'flow-error',
+ success: false,
+ code: 'TELEGRAM_BIND_ALREADY_BOUND',
+ })
})
test('rejects Telegram callbacks without a flow token and ignores descriptions', () => {
- assert.deepEqual(parseTelegramBindCallback({ telegram_bind: 'error' }), {
+ expect(parseTelegramBindCallback({ telegram_bind: 'error' })).toEqual({
kind: 'invalid',
})
- assert.deepEqual(
+ expect(
parseTelegramBindCallback({
telegram_bind: 'error',
flow_token: 'flow-error',
error_code: 'UNKNOWN_CODE',
error_description: 'untrusted message',
- } as Parameters[0]),
- {
- kind: 'result',
- flowToken: 'flow-error',
- success: false,
- code: 'UNKNOWN_CODE',
- }
- )
- assert.equal(parseTelegramBindCallback({}), null)
+ } as Parameters[0])
+ ).toEqual({
+ kind: 'result',
+ flowToken: 'flow-error',
+ success: false,
+ code: 'UNKNOWN_CODE',
+ })
+ expect(parseTelegramBindCallback({})).toBe(null)
})
test('posts only complete Telegram bind results to an available opener', () => {
@@ -112,11 +108,10 @@ describe('OAuth bind popup lifecycle', () => {
error_code: 'UNKNOWN_CODE',
})
- assert.equal(
- postTelegramBindResult(callback, opener, 'https://dashboard.example.com'),
- true
- )
- assert.deepEqual(messages, [
+ expect(
+ postTelegramBindResult(callback, opener, 'https://dashboard.example.com')
+ ).toBe(true)
+ expect(messages).toEqual([
{
message: {
type: 'telegram:binding:result',
@@ -128,23 +123,17 @@ describe('OAuth bind popup lifecycle', () => {
},
])
- assert.equal(
- postTelegramBindResult(
- { kind: 'invalid' },
- opener,
- 'https://example.com'
- ),
- false
- )
- assert.equal(
+ expect(
+ postTelegramBindResult({ kind: 'invalid' }, opener, 'https://example.com')
+ ).toBe(false)
+ expect(
postTelegramBindResult(
callback,
{ ...opener, closed: true },
'https://example.com'
- ),
- false
- )
- assert.equal(messages.length, 1)
+ )
+ ).toBe(false)
+ expect(messages.length).toBe(1)
})
test('waits 30 seconds for the opener response and can be cancelled', () => {
@@ -158,11 +147,11 @@ describe('OAuth bind popup lifecycle', () => {
timer.runtime
)
- assert.equal(timer.delay, 30_000)
+ expect(timer.delay).toBe(30_000)
cancel()
timer.fire()
- assert.equal(timedOut, false)
- assert.deepEqual(timer.cancelled, [timer.handle])
+ expect(timedOut).toBe(false)
+ expect(timer.cancelled).toEqual([timer.handle])
})
test('reports a closed popup once and clears its poller', () => {
@@ -178,13 +167,13 @@ describe('OAuth bind popup lifecycle', () => {
timer.runtime
)
- assert.equal(timer.delay, 500)
+ expect(timer.delay).toBe(500)
timer.fire()
- assert.equal(closedCount, 0)
+ expect(closedCount).toBe(0)
popup.closed = true
timer.fire()
timer.fire()
- assert.equal(closedCount, 1)
- assert.deepEqual(timer.cancelled, [timer.handle])
+ expect(closedCount).toBe(1)
+ expect(timer.cancelled).toEqual([timer.handle])
})
})
diff --git a/web/src/features/auth/lib/telegram-login.test.ts b/web/src/features/auth/lib/telegram-login.test.ts
index 95ee310c..ad64e967 100644
--- a/web/src/features/auth/lib/telegram-login.test.ts
+++ b/web/src/features/auth/lib/telegram-login.test.ts
@@ -16,14 +16,13 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { describe, test } from 'node:test'
+import { describe, expect, test } from 'vitest'
import { pickTelegramAuthorization } from './telegram-login'
describe('Telegram login authorization', () => {
test('keeps only fields signed by the Telegram login contract', () => {
- assert.deepEqual(
+ expect(
pickTelegramAuthorization({
id: 12345,
first_name: 'Test',
@@ -35,33 +34,27 @@ describe('Telegram login authorization', () => {
lang: 'en',
admin: true,
redirect: 'https://attacker.example',
- }),
- {
- id: 12345,
- first_name: 'Test',
- last_name: 'User',
- username: 'test_user',
- photo_url: 'https://t.me/i/userpic/320/test.jpg',
- auth_date: 1_900_000_000,
- hash: 'signed-hash',
- lang: 'en',
- }
- )
+ })
+ ).toEqual({
+ id: 12345,
+ first_name: 'Test',
+ last_name: 'User',
+ username: 'test_user',
+ photo_url: 'https://t.me/i/userpic/320/test.jpg',
+ auth_date: 1_900_000_000,
+ hash: 'signed-hash',
+ lang: 'en',
+ })
})
test('rejects incomplete or structurally invalid callbacks', () => {
- assert.equal(pickTelegramAuthorization(null), null)
- assert.equal(
- pickTelegramAuthorization({ auth_date: 1, hash: 'hash' }),
- null
- )
- assert.equal(
- pickTelegramAuthorization({ id: 1, auth_date: 1, hash: '' }),
- null
- )
- assert.equal(
- pickTelegramAuthorization({ id: {}, auth_date: 1, hash: 'hash' }),
+ expect(pickTelegramAuthorization(null)).toBe(null)
+ expect(pickTelegramAuthorization({ auth_date: 1, hash: 'hash' })).toBe(null)
+ expect(pickTelegramAuthorization({ id: 1, auth_date: 1, hash: '' })).toBe(
null
)
+ expect(
+ pickTelegramAuthorization({ id: {}, auth_date: 1, hash: 'hash' })
+ ).toBe(null)
})
})
diff --git a/web/src/features/channels/lib/__tests__/channel-field-update.test.ts b/web/src/features/channels/lib/__tests__/channel-field-update.test.ts
index d984f656..cfd463ba 100644
--- a/web/src/features/channels/lib/__tests__/channel-field-update.test.ts
+++ b/web/src/features/channels/lib/__tests__/channel-field-update.test.ts
@@ -16,8 +16,7 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { describe, test } from 'node:test'
+import { describe, expect, test } from 'vitest'
import {
CHANNEL_FIELD_UPDATE_DELAY_MS,
@@ -31,7 +30,7 @@ function createFakeTimers() {
return {
timers: {
setTimeout: (callback: () => void, delay: number) => {
- assert.equal(delay, CHANNEL_FIELD_UPDATE_DELAY_MS)
+ expect(delay).toBe(CHANNEL_FIELD_UPDATE_DELAY_MS)
const id = nextId++
pending.set(id, callback)
return id
@@ -63,11 +62,11 @@ describe('channel field update scheduler', () => {
scheduler.schedule(1)
scheduler.schedule(2)
scheduler.schedule(3)
- assert.deepEqual(updates, [])
- assert.equal(fake.pendingCount, 1)
+ expect(updates).toEqual([])
+ expect(fake.pendingCount).toBe(1)
fake.fireAll()
- assert.deepEqual(updates, [3])
+ expect(updates).toEqual([3])
})
test('flush commits the pending value immediately and cancels the timer', () => {
@@ -80,11 +79,11 @@ describe('channel field update scheduler', () => {
scheduler.schedule(7)
scheduler.flush()
- assert.deepEqual(updates, [7])
- assert.equal(fake.pendingCount, 0)
+ expect(updates).toEqual([7])
+ expect(fake.pendingCount).toBe(0)
fake.fireAll()
- assert.deepEqual(updates, [7])
+ expect(updates).toEqual([7])
})
test('flush without a pending value does nothing', () => {
@@ -99,7 +98,7 @@ describe('channel field update scheduler', () => {
scheduler.schedule(5)
scheduler.flush()
scheduler.flush()
- assert.deepEqual(updates, [5])
+ expect(updates).toEqual([5])
})
test('preserves a pending value of 0', () => {
@@ -112,6 +111,6 @@ describe('channel field update scheduler', () => {
scheduler.schedule(0)
scheduler.flush()
- assert.deepEqual(updates, [0])
+ expect(updates).toEqual([0])
})
})
diff --git a/web/src/features/channels/lib/__tests__/channel-table-row-id.test.ts b/web/src/features/channels/lib/__tests__/channel-table-row-id.test.ts
index d0e19baf..2d690627 100644
--- a/web/src/features/channels/lib/__tests__/channel-table-row-id.test.ts
+++ b/web/src/features/channels/lib/__tests__/channel-table-row-id.test.ts
@@ -16,8 +16,7 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { describe, test } from 'node:test'
+import { describe, expect, test } from 'vitest'
import type { Channel } from '../../types'
import { getChannelTableRowId, type TagRow } from '../channel-utils'
@@ -35,12 +34,8 @@ describe('channel table row identity', () => {
const beforeUpdate = [first, updated, third].map(getChannelTableRowId)
const afterUpdate = [updated, first, third].map(getChannelTableRowId)
- assert.deepEqual(beforeUpdate, [
- 'channel:101',
- 'channel:202',
- 'channel:303',
- ])
- assert.deepEqual(afterUpdate, ['channel:202', 'channel:101', 'channel:303'])
+ expect(beforeUpdate).toEqual(['channel:101', 'channel:202', 'channel:303'])
+ expect(afterUpdate).toEqual(['channel:202', 'channel:101', 'channel:303'])
})
test('uses separate namespaces for tag and channel rows', () => {
@@ -50,7 +45,7 @@ describe('channel table row identity', () => {
children: [channel(202)],
} as TagRow
- assert.equal(getChannelTableRowId(tagRow), 'tag:202')
- assert.equal(getChannelTableRowId(channel(202)), 'channel:202')
+ expect(getChannelTableRowId(tagRow)).toBe('tag:202')
+ expect(getChannelTableRowId(channel(202))).toBe('channel:202')
})
})
diff --git a/web/src/features/channels/lib/__tests__/new-api-channel.test.ts b/web/src/features/channels/lib/__tests__/new-api-channel.test.ts
index 5e4f6c9b..f24fb3b8 100644
--- a/web/src/features/channels/lib/__tests__/new-api-channel.test.ts
+++ b/web/src/features/channels/lib/__tests__/new-api-channel.test.ts
@@ -16,8 +16,7 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { describe, test } from 'node:test'
+import { describe, expect, test } from 'vitest'
import {
CHANNEL_TYPE_NEW_API,
@@ -45,45 +44,40 @@ describe('New API channel', () => {
(item) => item.value === CHANNEL_TYPE_NEW_API
)
- assert.deepEqual(option, {
+ expect(option).toEqual({
value: CHANNEL_TYPE_NEW_API,
label: 'New API',
})
- assert.equal(
+ expect(
CHANNEL_TYPE_OPTIONS.findIndex(
(item) => item.value === CHANNEL_TYPE_NEW_API
- ) + 1,
- CHANNEL_TYPE_OPTIONS.findIndex((item) => item.value === 58)
- )
- assert.equal(MODEL_FETCHABLE_TYPES.has(CHANNEL_TYPE_NEW_API), true)
- assert.equal(getChannelTypeIcon(CHANNEL_TYPE_NEW_API), 'NewAPI')
- assert.equal(
- getKeyPromptForType(CHANNEL_TYPE_NEW_API),
+ ) + 1
+ ).toBe(CHANNEL_TYPE_OPTIONS.findIndex((item) => item.value === 58))
+ expect(MODEL_FETCHABLE_TYPES.has(CHANNEL_TYPE_NEW_API)).toBe(true)
+ expect(getChannelTypeIcon(CHANNEL_TYPE_NEW_API)).toBe('NewAPI')
+ expect(getKeyPromptForType(CHANNEL_TYPE_NEW_API)).toBe(
'Enter API key for this channel'
)
- assert.equal(getChannelTypeConfig(CHANNEL_TYPE_NEW_API).icon, 'NewAPI')
+ expect(getChannelTypeConfig(CHANNEL_TYPE_NEW_API).icon).toBe('NewAPI')
})
test('requires a non-blank Base URL', () => {
const blankResult = channelFormSchema.safeParse(newAPIForm(' '))
- assert.equal(blankResult.success, false)
+ expect(blankResult.success).toBe(false)
if (!blankResult.success) {
- assert.equal(
+ expect(
blankResult.error.issues.some(
(issue) =>
issue.path[0] === 'base_url' &&
issue.message === 'Base URL is required for this channel type'
- ),
- true
- )
+ )
+ ).toBe(true)
}
- assert.equal(
- channelFormSchema.safeParse(newAPIForm('https://new-api.example'))
- .success,
- true
- )
+ expect(
+ channelFormSchema.safeParse(newAPIForm('https://new-api.example')).success
+ ).toBe(true)
})
test('keeps Sub2API Base URL validation unchanged', () => {
@@ -92,6 +86,6 @@ describe('New API channel', () => {
type: 59,
})
- assert.equal(result.success, true)
+ expect(result.success).toBe(true)
})
})
diff --git a/web/src/features/dashboard/lib/flow-selection.test.ts b/web/src/features/dashboard/lib/flow-selection.test.ts
index 4317c717..43bbccd7 100644
--- a/web/src/features/dashboard/lib/flow-selection.test.ts
+++ b/web/src/features/dashboard/lib/flow-selection.test.ts
@@ -16,8 +16,7 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { describe, test } from 'node:test'
+import { describe, expect, test } from 'vitest'
import type { FlowUserFilterOption } from '../types'
import {
@@ -46,89 +45,75 @@ const users: FlowUserFilterOption[] = [
describe('dashboard flow selection helpers', () => {
test('limits user chips to currently visible users', () => {
- assert.deepEqual(
- visibleFlowUsers(users, []).map((user) => user.value),
- ['user:1', 'user:2']
- )
- assert.deepEqual(
- visibleFlowUsers(users, ['user:2']).map((user) => user.value),
- ['user:2']
- )
+ expect(visibleFlowUsers(users, []).map((user) => user.value)).toEqual([
+ 'user:1',
+ 'user:2',
+ ])
+ expect(
+ visibleFlowUsers(users, ['user:2']).map((user) => user.value)
+ ).toEqual(['user:2'])
})
test('filters visible users without mutating the source options', () => {
const visible = visibleFlowUsers(users, ['user:1'])
- assert.deepEqual(
- visible.map((user) => user.value),
- ['user:1']
- )
- assert.deepEqual(
- users.map((user) => user.value),
- ['user:1', 'user:2']
- )
+ expect(visible.map((user) => user.value)).toEqual(['user:1'])
+ expect(users.map((user) => user.value)).toEqual(['user:1', 'user:2'])
})
test('formats compact selected counts for flow multiselect summaries', () => {
- assert.equal(compactFlowSelectionLabel(0), '*')
- assert.equal(compactFlowSelectionLabel(1), '1')
- assert.equal(compactFlowSelectionLabel(23), '23')
+ expect(compactFlowSelectionLabel(0)).toBe('*')
+ expect(compactFlowSelectionLabel(1)).toBe('1')
+ expect(compactFlowSelectionLabel(23)).toBe('23')
})
test('prioritizes loading and error states before empty flow data', () => {
- assert.equal(
+ expect(
flowDisplayState({
isLoading: true,
isError: true,
linkCount: 0,
themeReady: true,
- }),
- 'loading'
- )
- assert.equal(
+ })
+ ).toBe('loading')
+ expect(
flowDisplayState({
isLoading: false,
isError: true,
linkCount: 0,
themeReady: true,
- }),
- 'error'
- )
- assert.equal(
+ })
+ ).toBe('error')
+ expect(
flowDisplayState({
isLoading: false,
isError: false,
linkCount: 0,
themeReady: true,
- }),
- 'empty'
- )
- assert.equal(
+ })
+ ).toBe('empty')
+ expect(
flowDisplayState({
isLoading: false,
isError: false,
linkCount: 1,
themeReady: false,
- }),
- 'loading'
- )
+ })
+ ).toBe('loading')
})
test('throws unsuccessful flow responses instead of treating them as empty data', () => {
- assert.throws(
- () =>
- requireSuccessfulFlowRows(
- { success: false, data: [], message: 'database unavailable' },
- 'Failed to load'
- ),
- /database unavailable/
- )
- assert.deepEqual(
+ expect(() =>
+ requireSuccessfulFlowRows(
+ { success: false, data: [], message: 'database unavailable' },
+ 'Failed to load'
+ )
+ ).toThrow(/database unavailable/)
+ expect(
requireSuccessfulFlowRows(
{ success: true, data: [{ user_id: 1, quota: 10 }] },
'Failed to load'
- ),
- [{ user_id: 1, quota: 10 }]
- )
+ )
+ ).toEqual([{ user_id: 1, quota: 10 }])
})
})
diff --git a/web/src/features/dashboard/lib/flow.test.ts b/web/src/features/dashboard/lib/flow.test.ts
index 1645d2f0..724eb367 100644
--- a/web/src/features/dashboard/lib/flow.test.ts
+++ b/web/src/features/dashboard/lib/flow.test.ts
@@ -16,8 +16,7 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { describe, test } from 'node:test'
+import { describe, expect, test } from 'vitest'
import type { FlowQuotaDataItem } from '../types'
import {
@@ -113,18 +112,16 @@ describe('dashboard flow data', () => {
role: 'user',
})
- assert.equal(result.summary.quota, 150)
- assert.equal(result.summary.tokens, 60)
- assert.equal(result.summary.requests, 3)
- assert.deepEqual(
- result.flow.links.map((link) => [link.source, link.target, link.value]),
- [
- ['group:vip', 'model:gpt-4.1', 150],
- ['token:11', 'group:vip', 150],
- ]
- )
- assert.equal(
- result.flow.nodes.some((node) => node.kind === 'channel'),
+ expect(result.summary.quota).toBe(150)
+ expect(result.summary.tokens).toBe(60)
+ expect(result.summary.requests).toBe(3)
+ expect(
+ result.flow.links.map((link) => [link.source, link.target, link.value])
+ ).toEqual([
+ ['group:vip', 'model:gpt-4.1', 150],
+ ['token:11', 'group:vip', 150],
+ ])
+ expect(result.flow.nodes.some((node) => node.kind === 'channel')).toBe(
false
)
})
@@ -134,18 +131,17 @@ describe('dashboard flow data', () => {
role: 'admin',
})
- assert.deepEqual(
- result.flow.links.map((link) => [link.source, link.target, link.value]),
- [
- ['group:default', 'model:claude-4-sonnet', 70],
- ['group:vip', 'model:gpt-4.1', 150],
- ['model:claude-4-sonnet', 'channel:101', 70],
- ['model:gpt-4.1', 'channel:101', 100],
- ['model:gpt-4.1', 'channel:102', 50],
- ['user:1', 'group:vip', 150],
- ['user:2', 'group:default', 70],
- ]
- )
+ expect(
+ result.flow.links.map((link) => [link.source, link.target, link.value])
+ ).toEqual([
+ ['group:default', 'model:claude-4-sonnet', 70],
+ ['group:vip', 'model:gpt-4.1', 150],
+ ['model:claude-4-sonnet', 'channel:101', 70],
+ ['model:gpt-4.1', 'channel:101', 100],
+ ['model:gpt-4.1', 'channel:102', 50],
+ ['user:1', 'group:vip', 150],
+ ['user:2', 'group:default', 70],
+ ])
})
test('builds root user-node-token-group-model-channel flow', () => {
@@ -153,22 +149,21 @@ describe('dashboard flow data', () => {
role: 'root',
})
- assert.deepEqual(
- result.flow.links.map((link) => [link.source, link.target, link.value]),
- [
- ['group:default', 'model:claude-4-sonnet', 3],
- ['group:vip', 'model:gpt-4.1', 3],
- ['model:claude-4-sonnet', 'channel:101', 3],
- ['model:gpt-4.1', 'channel:101', 2],
- ['model:gpt-4.1', 'channel:102', 1],
- ['node:node-a', 'token:11', 3],
- ['node:node-b', 'token:22', 3],
- ['token:11', 'group:vip', 3],
- ['token:22', 'group:default', 3],
- ['user:1', 'node:node-a', 3],
- ['user:2', 'node:node-b', 3],
- ]
- )
+ expect(
+ result.flow.links.map((link) => [link.source, link.target, link.value])
+ ).toEqual([
+ ['group:default', 'model:claude-4-sonnet', 3],
+ ['group:vip', 'model:gpt-4.1', 3],
+ ['model:claude-4-sonnet', 'channel:101', 3],
+ ['model:gpt-4.1', 'channel:101', 2],
+ ['model:gpt-4.1', 'channel:102', 1],
+ ['node:node-a', 'token:11', 3],
+ ['node:node-b', 'token:22', 3],
+ ['token:11', 'group:vip', 3],
+ ['token:22', 'group:default', 3],
+ ['user:1', 'node:node-a', 3],
+ ['user:2', 'node:node-b', 3],
+ ])
})
test('filters by selected users', () => {
@@ -177,15 +172,14 @@ describe('dashboard flow data', () => {
selectedUsers: ['user:2'],
})
- assert.equal(result.summary.quota, 70)
- assert.deepEqual(
- result.flow.links.map((link) => [link.source, link.target, link.value]),
- [
- ['group:default', 'model:claude-4-sonnet', 70],
- ['model:claude-4-sonnet', 'channel:101', 70],
- ['user:2', 'group:default', 70],
- ]
- )
+ expect(result.summary.quota).toBe(70)
+ expect(
+ result.flow.links.map((link) => [link.source, link.target, link.value])
+ ).toEqual([
+ ['group:default', 'model:claude-4-sonnet', 70],
+ ['model:claude-4-sonnet', 'channel:101', 70],
+ ['user:2', 'group:default', 70],
+ ])
})
test('filters rows by selected flow nodes', () => {
@@ -194,16 +188,15 @@ describe('dashboard flow data', () => {
selectedNodes: [{ kind: 'model', id: 'model:gpt-4.1' }],
})
- assert.equal(result.summary.quota, 150)
- assert.deepEqual(
- result.flow.links.map((link) => [link.source, link.target, link.value]),
- [
- ['group:vip', 'model:gpt-4.1', 150],
- ['model:gpt-4.1', 'channel:101', 100],
- ['model:gpt-4.1', 'channel:102', 50],
- ['user:1', 'group:vip', 150],
- ]
- )
+ expect(result.summary.quota).toBe(150)
+ expect(
+ result.flow.links.map((link) => [link.source, link.target, link.value])
+ ).toEqual([
+ ['group:vip', 'model:gpt-4.1', 150],
+ ['model:gpt-4.1', 'channel:101', 100],
+ ['model:gpt-4.1', 'channel:102', 50],
+ ['user:1', 'group:vip', 150],
+ ])
})
test('combines node filters with OR inside a column and AND across columns', () => {
@@ -222,20 +215,19 @@ describe('dashboard flow data', () => {
],
})
- assert.equal(sameColumn.summary.quota, 220)
- assert.equal(crossColumn.summary.quota, 100)
- assert.deepEqual(
+ expect(sameColumn.summary.quota).toBe(220)
+ expect(crossColumn.summary.quota).toBe(100)
+ expect(
crossColumn.flow.links.map((link) => [
link.source,
link.target,
link.value,
- ]),
- [
- ['group:vip', 'model:gpt-4.1', 100],
- ['model:gpt-4.1', 'channel:101', 100],
- ['user:1', 'group:vip', 100],
- ]
- )
+ ])
+ ).toEqual([
+ ['group:vip', 'model:gpt-4.1', 100],
+ ['model:gpt-4.1', 'channel:101', 100],
+ ['user:1', 'group:vip', 100],
+ ])
})
test('combines user and node filters', () => {
@@ -245,15 +237,14 @@ describe('dashboard flow data', () => {
selectedNodes: [{ kind: 'channel', id: 'channel:101' }],
})
- assert.equal(result.summary.quota, 100)
- assert.deepEqual(
- result.flow.links.map((link) => [link.source, link.target, link.value]),
- [
- ['group:vip', 'model:gpt-4.1', 100],
- ['model:gpt-4.1', 'channel:101', 100],
- ['user:1', 'group:vip', 100],
- ]
- )
+ expect(result.summary.quota).toBe(100)
+ expect(
+ result.flow.links.map((link) => [link.source, link.target, link.value])
+ ).toEqual([
+ ['group:vip', 'model:gpt-4.1', 100],
+ ['model:gpt-4.1', 'channel:101', 100],
+ ['user:1', 'group:vip', 100],
+ ])
})
test('reconnects links when a middle stage is hidden', () => {
@@ -262,20 +253,16 @@ describe('dashboard flow data', () => {
visibleStages: ['user', 'model', 'channel'],
})
- assert.deepEqual(
- result.flow.links.map((link) => [link.source, link.target, link.value]),
- [
- ['model:claude-4-sonnet', 'channel:101', 70],
- ['model:gpt-4.1', 'channel:101', 100],
- ['model:gpt-4.1', 'channel:102', 50],
- ['user:1', 'model:gpt-4.1', 150],
- ['user:2', 'model:claude-4-sonnet', 70],
- ]
- )
- assert.equal(
- result.flow.nodes.some((node) => node.kind === 'group'),
- false
- )
+ expect(
+ result.flow.links.map((link) => [link.source, link.target, link.value])
+ ).toEqual([
+ ['model:claude-4-sonnet', 'channel:101', 70],
+ ['model:gpt-4.1', 'channel:101', 100],
+ ['model:gpt-4.1', 'channel:102', 50],
+ ['user:1', 'model:gpt-4.1', 150],
+ ['user:2', 'model:claude-4-sonnet', 70],
+ ])
+ expect(result.flow.nodes.some((node) => node.kind === 'group')).toBe(false)
})
test('ignores stage filters that would leave fewer than two columns', () => {
@@ -284,26 +271,24 @@ describe('dashboard flow data', () => {
visibleStages: ['model'],
})
- assert.deepEqual(
- result.flow.links.map((link) => [link.source, link.target, link.value]),
- [
- ['group:vip', 'model:gpt-4.1', 150],
- ['token:11', 'group:vip', 150],
- ]
- )
+ expect(
+ result.flow.links.map((link) => [link.source, link.target, link.value])
+ ).toEqual([
+ ['group:vip', 'model:gpt-4.1', 150],
+ ['token:11', 'group:vip', 150],
+ ])
})
test('builds user filter options with stable values', () => {
const options = buildFlowFilterOptions(rows, 'quota')
- assert.deepEqual(
- options.users.map((user) => [user.value, user.label, user.valueLabel]),
- [
- ['user:1', 'alice', '150'],
- ['user:2', 'bob', '70'],
- ]
- )
- assert.notEqual(options.users[0].color, options.users[1].color)
+ expect(
+ options.users.map((user) => [user.value, user.label, user.valueLabel])
+ ).toEqual([
+ ['user:1', 'alice', '150'],
+ ['user:2', 'bob', '70'],
+ ])
+ expect(options.users[0].color).not.toBe(options.users[1].color)
})
test('builds node filter options without applying top limits', () => {
@@ -313,22 +298,20 @@ describe('dashboard flow data', () => {
overflowMode: 'aggregate',
})
- assert.equal(
+ expect(
result.filterOptions.nodes.some(
(option) => option.kind === 'model' && option.value === 'model:model-c'
- ),
- true
- )
- assert.deepEqual(
+ )
+ ).toBe(true)
+ expect(
result.filterOptions.nodes
.filter((option) => option.kind === 'model')
- .map((option) => [option.value, option.valueLabel]),
- [
- ['model:model-a', '100'],
- ['model:model-b', '80'],
- ['model:model-c', '10'],
- ]
- )
+ .map((option) => [option.value, option.valueLabel])
+ ).toEqual([
+ ['model:model-a', '100'],
+ ['model:model-b', '80'],
+ ['model:model-c', '10'],
+ ])
})
test('facets node filter options by selected nodes from other columns', () => {
@@ -338,30 +321,27 @@ describe('dashboard flow data', () => {
})
const nodeOptions = result.filterOptions.nodes
- assert.deepEqual(
+ expect(
nodeOptions
.filter((option) => option.kind === 'node')
- .map((option) => [option.value, option.valueLabel]),
- [
- ['node:node-a', '150'],
- ['node:node-b', '70'],
- ]
- )
- assert.deepEqual(
+ .map((option) => [option.value, option.valueLabel])
+ ).toEqual([
+ ['node:node-a', '150'],
+ ['node:node-b', '70'],
+ ])
+ expect(
nodeOptions
.filter((option) => option.kind === 'token')
- .map((option) => [option.value, option.valueLabel]),
- [['token:11', '150']]
- )
- assert.deepEqual(
+ .map((option) => [option.value, option.valueLabel])
+ ).toEqual([['token:11', '150']])
+ expect(
nodeOptions
.filter((option) => option.kind === 'channel')
- .map((option) => [option.value, option.valueLabel]),
- [
- ['channel:101', '100'],
- ['channel:102', '50'],
- ]
- )
+ .map((option) => [option.value, option.valueLabel])
+ ).toEqual([
+ ['channel:101', '100'],
+ ['channel:102', '50'],
+ ])
})
test('keeps same-column node options available for OR filtering', () => {
@@ -370,24 +350,22 @@ describe('dashboard flow data', () => {
selectedNodes: [{ kind: 'model', id: 'model:gpt-4.1' }],
})
- assert.deepEqual(
+ expect(
result.filterOptions.nodes
.filter((option) => option.kind === 'model')
- .map((option) => [option.value, option.valueLabel]),
- [
- ['model:gpt-4.1', '150'],
- ['model:claude-4-sonnet', '70'],
- ]
- )
- assert.deepEqual(
+ .map((option) => [option.value, option.valueLabel])
+ ).toEqual([
+ ['model:gpt-4.1', '150'],
+ ['model:claude-4-sonnet', '70'],
+ ])
+ expect(
result.filterOptions.nodes
.filter((option) => option.kind === 'channel')
- .map((option) => [option.value, option.valueLabel]),
- [
- ['channel:101', '100'],
- ['channel:102', '50'],
- ]
- )
+ .map((option) => [option.value, option.valueLabel])
+ ).toEqual([
+ ['channel:101', '100'],
+ ['channel:102', '50'],
+ ])
})
test('combines user filters with faceted node filter options', () => {
@@ -397,22 +375,20 @@ describe('dashboard flow data', () => {
selectedNodes: [{ kind: 'channel', id: 'channel:101' }],
})
- assert.equal(result.summary.quota, 100)
- assert.deepEqual(
+ expect(result.summary.quota).toBe(100)
+ expect(
result.filterOptions.nodes
.filter((option) => option.kind === 'model')
- .map((option) => [option.value, option.valueLabel]),
- [['model:gpt-4.1', '100']]
- )
- assert.deepEqual(
+ .map((option) => [option.value, option.valueLabel])
+ ).toEqual([['model:gpt-4.1', '100']])
+ expect(
result.filterOptions.nodes
.filter((option) => option.kind === 'channel')
- .map((option) => [option.value, option.valueLabel]),
- [
- ['channel:101', '100'],
- ['channel:102', '50'],
- ]
- )
+ .map((option) => [option.value, option.valueLabel])
+ ).toEqual([
+ ['channel:101', '100'],
+ ['channel:102', '50'],
+ ])
})
test('aggregates overflow nodes into per-column Other buckets', () => {
@@ -434,18 +410,18 @@ describe('dashboard flow data', () => {
.filter((link) => link.source.startsWith('user:'))
.reduce((sum, link) => sum + link.value, 0)
- assert.equal(result.summary.quota, 190)
- assert.equal(firstStepTotal, 190)
- assert.equal(otherUser?.label, 'Other user')
- assert.equal(otherFirstStepLink?.value, 10)
- assert.equal(nodeIds.has('user:3'), false)
- assert.equal(nodeIds.has('group:free'), false)
- assert.equal(nodeIds.has('model:model-c'), false)
- assert.equal(nodeIds.has('channel:203'), false)
- assert.equal(nodeIds.has('user:__other__'), true)
- assert.equal(nodeIds.has('group:__other__'), true)
- assert.equal(nodeIds.has('model:__other__'), true)
- assert.equal(nodeIds.has('channel:__other__'), true)
+ expect(result.summary.quota).toBe(190)
+ expect(firstStepTotal).toBe(190)
+ expect(otherUser?.label).toBe('Other user')
+ expect(otherFirstStepLink?.value).toBe(10)
+ expect(nodeIds.has('user:3')).toBe(false)
+ expect(nodeIds.has('group:free')).toBe(false)
+ expect(nodeIds.has('model:model-c')).toBe(false)
+ expect(nodeIds.has('channel:203')).toBe(false)
+ expect(nodeIds.has('user:__other__')).toBe(true)
+ expect(nodeIds.has('group:__other__')).toBe(true)
+ expect(nodeIds.has('model:__other__')).toBe(true)
+ expect(nodeIds.has('channel:__other__')).toBe(true)
})
test('hides overflow paths when overflow mode is hide', () => {
@@ -460,11 +436,11 @@ describe('dashboard flow data', () => {
.filter((link) => link.source.startsWith('user:'))
.reduce((sum, link) => sum + link.value, 0)
- assert.equal(result.summary.quota, 190)
- assert.equal(firstStepTotal, 180)
- assert.equal(nodeIds.has('user:3'), false)
- assert.equal(nodeIds.has('user:__other__'), false)
- assert.equal(nodeIds.has('model:__other__'), false)
+ expect(result.summary.quota).toBe(190)
+ expect(firstStepTotal).toBe(180)
+ expect(nodeIds.has('user:3')).toBe(false)
+ expect(nodeIds.has('user:__other__')).toBe(false)
+ expect(nodeIds.has('model:__other__')).toBe(false)
})
test('ranks top nodes using the selected flow metric', () => {
@@ -484,18 +460,11 @@ describe('dashboard flow data', () => {
overflowMode: 'aggregate',
})
- assert.equal(
- byQuota.flow.nodes.some((node) => node.id === 'user:1'),
- true
- )
- assert.equal(
- byRequests.flow.nodes.some((node) => node.id === 'user:2'),
- true
- )
- assert.equal(
- byTokens.flow.nodes.some((node) => node.id === 'user:3'),
+ expect(byQuota.flow.nodes.some((node) => node.id === 'user:1')).toBe(true)
+ expect(byRequests.flow.nodes.some((node) => node.id === 'user:2')).toBe(
true
)
+ expect(byTokens.flow.nodes.some((node) => node.id === 'user:3')).toBe(true)
})
test('applies top limits only to visible stages', () => {
@@ -507,19 +476,18 @@ describe('dashboard flow data', () => {
})
const nodeIds = new Set(result.flow.nodes.map((node) => node.id))
- assert.equal(nodeIds.has('user:1'), true)
- assert.equal(nodeIds.has('user:__other__'), true)
- assert.equal(nodeIds.has('model:model-a'), true)
- assert.equal(nodeIds.has('model:__other__'), true)
- assert.equal(nodeIds.has('group:__other__'), false)
- assert.equal(nodeIds.has('channel:__other__'), false)
- assert.deepEqual(
- result.flow.links.map((link) => [link.source, link.target, link.value]),
- [
- ['user:__other__', 'model:__other__', 90],
- ['user:1', 'model:model-a', 100],
- ]
- )
+ expect(nodeIds.has('user:1')).toBe(true)
+ expect(nodeIds.has('user:__other__')).toBe(true)
+ expect(nodeIds.has('model:model-a')).toBe(true)
+ expect(nodeIds.has('model:__other__')).toBe(true)
+ expect(nodeIds.has('group:__other__')).toBe(false)
+ expect(nodeIds.has('channel:__other__')).toBe(false)
+ expect(
+ result.flow.links.map((link) => [link.source, link.target, link.value])
+ ).toEqual([
+ ['user:__other__', 'model:__other__', 90],
+ ['user:1', 'model:model-a', 100],
+ ])
})
test('applies top limits after node filters', () => {
@@ -531,17 +499,16 @@ describe('dashboard flow data', () => {
})
const nodeIds = new Set(result.flow.nodes.map((node) => node.id))
- assert.equal(result.summary.quota, 10)
- assert.equal(nodeIds.has('model:model-c'), true)
- assert.equal(nodeIds.has('model:__other__'), false)
- assert.deepEqual(
- result.flow.links.map((link) => [link.source, link.target, link.value]),
- [
- ['group:free', 'model:model-c', 10],
- ['model:model-c', 'channel:203', 10],
- ['user:3', 'group:free', 10],
- ]
- )
+ expect(result.summary.quota).toBe(10)
+ expect(nodeIds.has('model:model-c')).toBe(true)
+ expect(nodeIds.has('model:__other__')).toBe(false)
+ expect(
+ result.flow.links.map((link) => [link.source, link.target, link.value])
+ ).toEqual([
+ ['group:free', 'model:model-c', 10],
+ ['model:model-c', 'channel:203', 10],
+ ['user:3', 'group:free', 10],
+ ])
})
test('ignores selected node filters for hidden stages', () => {
@@ -551,9 +518,8 @@ describe('dashboard flow data', () => {
selectedNodes: [{ kind: 'group', id: 'group:vip' }],
})
- assert.equal(result.summary.quota, 220)
- assert.equal(
- result.flow.nodes.some((node) => node.id === 'group:vip'),
+ expect(result.summary.quota).toBe(220)
+ expect(result.flow.nodes.some((node) => node.id === 'group:vip')).toBe(
false
)
})
@@ -576,35 +542,35 @@ describe('dashboard flow data', () => {
])
)
- assert.deepEqual(nodeState.get('user:1'), {
+ expect(nodeState.get('user:1')).toEqual({
highlighted: true,
dimmed: false,
})
- assert.deepEqual(nodeState.get('node:node-a'), {
+ expect(nodeState.get('node:node-a')).toEqual({
highlighted: true,
dimmed: false,
})
- assert.deepEqual(nodeState.get('model:gpt-4.1'), {
+ expect(nodeState.get('model:gpt-4.1')).toEqual({
highlighted: true,
dimmed: false,
})
- assert.deepEqual(nodeState.get('channel:101'), {
+ expect(nodeState.get('channel:101')).toEqual({
highlighted: true,
dimmed: false,
})
- assert.deepEqual(nodeState.get('user:2'), {
+ expect(nodeState.get('user:2')).toEqual({
highlighted: false,
dimmed: true,
})
- assert.deepEqual(linkState.get('user:1->node:node-a'), {
+ expect(linkState.get('user:1->node:node-a')).toEqual({
highlighted: true,
dimmed: false,
})
- assert.deepEqual(linkState.get('model:gpt-4.1->channel:101'), {
+ expect(linkState.get('model:gpt-4.1->channel:101')).toEqual({
highlighted: true,
dimmed: false,
})
- assert.deepEqual(linkState.get('model:claude-4-sonnet->channel:101'), {
+ expect(linkState.get('model:claude-4-sonnet->channel:101')).toEqual({
highlighted: false,
dimmed: true,
})
@@ -628,23 +594,23 @@ describe('dashboard flow data', () => {
])
)
- assert.deepEqual(linkState.get('model:gpt-4.1->channel:101'), {
+ expect(linkState.get('model:gpt-4.1->channel:101')).toEqual({
highlighted: true,
dimmed: false,
})
- assert.deepEqual(linkState.get('model:gpt-4.1->channel:102'), {
+ expect(linkState.get('model:gpt-4.1->channel:102')).toEqual({
highlighted: false,
dimmed: true,
})
- assert.deepEqual(nodeState.get('user:1'), {
+ expect(nodeState.get('user:1')).toEqual({
highlighted: true,
dimmed: false,
})
- assert.deepEqual(nodeState.get('node:node-a'), {
+ expect(nodeState.get('node:node-a')).toEqual({
highlighted: true,
dimmed: false,
})
- assert.deepEqual(nodeState.get('user:2'), {
+ expect(nodeState.get('user:2')).toEqual({
highlighted: false,
dimmed: true,
})
@@ -686,11 +652,11 @@ describe('dashboard flow data', () => {
(link) => link.source === 'user:2' && link.target === 'group:vip'
)
- assert.equal(sharedLink?.value, 150)
- assert.equal(sharedLink?.highlighted, true)
- assert.equal(sharedLink?.dimmed, false)
- assert.equal(inactiveUserLink?.highlighted, false)
- assert.equal(inactiveUserLink?.dimmed, true)
+ expect(sharedLink?.value).toBe(150)
+ expect(sharedLink?.highlighted).toBe(true)
+ expect(sharedLink?.dimmed).toBe(false)
+ expect(inactiveUserLink?.highlighted).toBe(false)
+ expect(inactiveUserLink?.dimmed).toBe(true)
})
test('does not emit highlight states without a visible active node', () => {
@@ -703,30 +669,26 @@ describe('dashboard flow data', () => {
activeNode: { kind: 'user', id: 'user:1' },
})
- assert.equal(
+ expect(
withoutActive.flow.nodes.every(
(node) => node.highlighted === undefined && node.dimmed === undefined
- ),
- true
- )
- assert.equal(
+ )
+ ).toBe(true)
+ expect(
withoutActive.flow.links.every(
(link) => link.highlighted === undefined && link.dimmed === undefined
- ),
- true
- )
- assert.equal(
+ )
+ ).toBe(true)
+ expect(
hiddenActive.flow.nodes.every(
(node) => node.highlighted === undefined && node.dimmed === undefined
- ),
- true
- )
- assert.equal(
+ )
+ ).toBe(true)
+ expect(
hiddenActive.flow.links.every(
(link) => link.highlighted === undefined && link.dimmed === undefined
- ),
- true
- )
+ )
+ ).toBe(true)
})
test('builds Sankey spec with quota token request tooltips', () => {
@@ -743,19 +705,19 @@ describe('dashboard flow data', () => {
link.source === 'user:1' && link.target === 'node:node-a'
)
- assert.equal(flowSpec.type, 'sankey')
- assert.equal(flowSpec.title.text, 'Flow')
- assert.deepEqual(flowSpec.emphasis, { enable: false })
- assert.equal(flowSpec.tooltip.mark.visible({ datum: aliceNode }), true)
- assert.equal(flowSpec.tooltip.mark.visible({ datum: userNodeLink }), true)
- assert.equal(flowSpec.animation, false)
- assert.equal(values.nodes.length, 6)
- assert.equal(values.links.length, 5)
- assert.equal(aliceNode.name, 'alice')
- assert.match(userNodeLink.linkColor, /^rgba\(/)
+ expect(flowSpec.type).toBe('sankey')
+ expect(flowSpec.title.text).toBe('Flow')
+ expect(flowSpec.emphasis).toEqual({ enable: false })
+ expect(flowSpec.tooltip.mark.visible({ datum: aliceNode })).toBe(true)
+ expect(flowSpec.tooltip.mark.visible({ datum: userNodeLink })).toBe(true)
+ expect(flowSpec.animation).toBe(false)
+ expect(values.nodes.length).toBe(6)
+ expect(values.links.length).toBe(5)
+ expect(aliceNode.name).toBe('alice')
+ expect(userNodeLink.linkColor).toMatch(/^rgba\(/)
const tooltipRows = flowSpec.tooltip.mark.content
- assert.deepEqual(
+ expect(
tooltipRows
.filter((row: Record) =>
typeof row.visible === 'function'
@@ -767,14 +729,13 @@ describe('dashboard flow data', () => {
typeof row.value === 'function'
? row.value({ datum: userNodeLink })
: row.value,
- ]),
- [
- ['Quota', '100'],
- ['Tokens', '40'],
- ['Requests', '2'],
- ['Share', '100.0%'],
- ]
- )
+ ])
+ ).toEqual([
+ ['Quota', '100'],
+ ['Tokens', '40'],
+ ['Requests', '2'],
+ ['Share', '100.0%'],
+ ])
})
test('maps active flow highlight states into the Sankey spec', () => {
@@ -801,15 +762,15 @@ describe('dashboard flow data', () => {
const nodeOpacity = flowSpec.node.style.fillOpacity
const linkOpacity = flowSpec.link.style.fillOpacity
- assert.deepEqual(flowSpec.emphasis, { enable: false })
- assert.equal(aliceNode.highlighted, true)
- assert.equal(bobNode.dimmed, true)
- assert.equal(highlightedLink.highlighted, true)
- assert.equal(dimmedLink.dimmed, true)
- assert.equal(nodeOpacity(aliceNode), 1)
- assert.equal(nodeOpacity(bobNode), 0.18)
- assert.equal(linkOpacity(highlightedLink), 0.86)
- assert.equal(linkOpacity(dimmedLink), 0.08)
- assert.equal(highlightedLink.zIndex > dimmedLink.zIndex, true)
+ expect(flowSpec.emphasis).toEqual({ enable: false })
+ expect(aliceNode.highlighted).toBe(true)
+ expect(bobNode.dimmed).toBe(true)
+ expect(highlightedLink.highlighted).toBe(true)
+ expect(dimmedLink.dimmed).toBe(true)
+ expect(nodeOpacity(aliceNode)).toBe(1)
+ expect(nodeOpacity(bobNode)).toBe(0.18)
+ expect(linkOpacity(highlightedLink)).toBe(0.86)
+ expect(linkOpacity(dimmedLink)).toBe(0.08)
+ expect(highlightedLink.zIndex > dimmedLink.zIndex).toBe(true)
})
})
diff --git a/web/src/features/keys/components/__tests__/api-key-group-cell.test.tsx b/web/src/features/keys/components/__tests__/api-key-group-cell.test.tsx
index 5cb64ae5..2f07baa2 100644
--- a/web/src/features/keys/components/__tests__/api-key-group-cell.test.tsx
+++ b/web/src/features/keys/components/__tests__/api-key-group-cell.test.tsx
@@ -16,39 +16,9 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { after, describe, test } from 'node:test'
+import { render } from '@testing-library/react'
+import { describe, expect, test } from 'vitest'
-import { Window } from 'happy-dom'
-
-const domWindow = new Window()
-const domGlobals = [
- 'window',
- 'document',
- 'navigator',
- 'HTMLElement',
- 'HTMLButtonElement',
- 'SVGElement',
- 'Node',
- 'Element',
- 'Event',
- 'CustomEvent',
- 'MutationObserver',
- 'ResizeObserver',
- 'requestAnimationFrame',
- 'cancelAnimationFrame',
- 'getComputedStyle',
-] as const
-
-for (const key of domGlobals) {
- Object.defineProperty(globalThis, key, {
- configurable: true,
- value: domWindow[key],
- })
-}
-
-const { act } = await import('react')
-const { createRoot } = await import('react-dom/client')
const { createInstance } = await import('i18next')
const { I18nextProvider, initReactI18next } = await import('react-i18next')
const { TooltipProvider } = await import('@/components/ui/tooltip')
@@ -70,11 +40,6 @@ await i18n.use(initReactI18next).init({
},
})
-const reactTestGlobals = globalThis as typeof globalThis & {
- IS_REACT_ACT_ENVIRONMENT?: boolean
-}
-reactTestGlobals.IS_REACT_ACT_ENVIRONMENT = true
-
function CellHarness(props: {
group: string
ratio?: number | string
@@ -96,141 +61,93 @@ function CellHarness(props: {
}
describe('API key group table cell', () => {
- after(() => {
- domWindow.close()
- })
-
- test('renders two unclipped rings and a localized Auto ratio when API data uses a nonlocalized string', async () => {
- const container = document.createElement('div')
- document.body.append(container)
- const root = createRoot(container)
-
- await act(async () =>
- root.render(
-
- )
+ test('renders an unclipped ring and a localized Auto ratio when API data uses a nonlocalized string', () => {
+ const { container } = render(
+
)
const badgeCell = container.querySelector(
'[data-api-key-group-cell="auto"]'
)
- assert.ok(badgeCell)
- assert.equal(badgeCell.classList.contains('overflow-visible'), true)
- assert.equal(badgeCell.classList.contains('overflow-hidden'), false)
+ expect(badgeCell).toHaveClass('overflow-visible')
+ expect(badgeCell).not.toHaveClass('overflow-hidden')
const frames = container.querySelectorAll('[data-auto-group-frame]')
const movingRings = container.querySelectorAll(
'[data-auto-group-flow-border]'
)
- assert.equal(frames.length, 2)
- assert.equal(movingRings.length, 2)
+ expect(frames.length).toBe(1)
+ expect(movingRings.length).toBe(1)
for (const frame of frames) {
- assert.equal(frame.classList.contains('relative'), true)
- assert.equal(frame.classList.contains('overflow-visible'), true)
- assert.equal(frame.classList.contains('rounded-4xl'), true)
- assert.equal(frame.classList.contains('p-px'), true)
+ expect(frame).toHaveClass(
+ 'relative',
+ 'overflow-visible',
+ 'rounded-4xl',
+ 'p-px'
+ )
}
const ratio = container.querySelector(
'[data-auto-group-effect="ratio"]'
)
- assert.ok(ratio)
- assert.equal(ratio.textContent, 'Auto Ratio')
- assert.equal(ratio.textContent?.includes('x'), false)
- assert.equal(container.textContent?.includes('自动'), false)
- assert.equal(container.textContent?.includes('Cross-group'), true)
+ expect(ratio).toHaveTextContent('Auto Ratio')
+ expect(ratio).not.toHaveTextContent('x')
+ expect(container).not.toHaveTextContent('自动')
+ expect(container).toHaveTextContent('Cross-group')
const crossGroupBadge = [
...container.querySelectorAll('[data-slot="status-badge"]'),
].find((badge) => badge.textContent === 'Cross-group')
- assert.ok(crossGroupBadge)
- assert.equal(crossGroupBadge.closest('[data-auto-group-frame]'), null)
-
- await act(async () => root.unmount())
- container.remove()
+ expect(crossGroupBadge).not.toBeUndefined()
+ expect(crossGroupBadge?.closest('[data-auto-group-frame]')).toBeNull()
})
- test('keeps static Auto frames but omits both moving layers for reduced motion', async () => {
- const container = document.createElement('div')
- document.body.append(container)
- const root = createRoot(container)
-
- await act(async () =>
- root.render()
+ test('keeps the static Auto ratio frame but omits its moving layer for reduced motion', () => {
+ const { container } = render(
+
)
- assert.equal(
- container.querySelectorAll('[data-auto-group-frame]').length,
- 2
- )
- assert.equal(
- container.querySelectorAll('[data-auto-group-flow-border]').length,
- 0
- )
-
- await act(async () => root.unmount())
- container.remove()
+ expect(container.querySelectorAll('[data-auto-group-frame]').length).toBe(1)
+ expect(
+ container.querySelectorAll('[data-auto-group-flow-border]').length
+ ).toBe(0)
})
- test('shows only the Auto badge when ratio data is unavailable', async () => {
- const container = document.createElement('div')
- document.body.append(container)
- const root = createRoot(container)
-
- await act(async () =>
- root.render()
+ test('shows only the cross-group badge when ratio data is unavailable', () => {
+ const { container } = render(
+
)
- assert.equal(
- container.querySelectorAll('[data-auto-group-frame]').length,
- 1
- )
- assert.equal(
- container.querySelectorAll('[data-auto-group-flow-border]').length,
- 1
- )
- assert.equal(
- container.querySelector('[data-auto-group-effect="ratio"]'),
+ expect(container.querySelectorAll('[data-auto-group-frame]').length).toBe(0)
+ expect(
+ container.querySelectorAll('[data-auto-group-flow-border]').length
+ ).toBe(0)
+ expect(container.querySelector('[data-auto-group-effect="ratio"]')).toBe(
null
)
- assert.equal(container.textContent?.includes('Auto'), true)
- assert.equal(container.textContent?.includes('Ratio'), false)
-
- await act(async () => root.unmount())
- container.remove()
+ expect(container).toHaveTextContent('Cross-group')
+ expect(container).not.toHaveTextContent('Auto')
+ expect(container).not.toHaveTextContent('Ratio')
})
- test('narrows normal group ratios to numbers and never applies Auto rings', async () => {
- const container = document.createElement('div')
- document.body.append(container)
- const root = createRoot(container)
-
- await act(async () =>
- root.render(
-
- )
+ test('narrows normal group ratios to numbers and never applies Auto rings', () => {
+ const { container, rerender } = render(
+
)
- assert.equal(container.textContent?.includes('vip'), true)
- assert.equal(container.textContent?.includes('自动'), false)
- assert.equal(container.querySelector('[data-auto-group-frame]'), null)
- assert.equal(container.querySelector('[data-auto-group-flow-border]'), null)
+ expect(container).toHaveTextContent('vip')
+ expect(container).not.toHaveTextContent('自动')
+ expect(container.querySelector('[data-auto-group-frame]')).toBe(null)
+ expect(container.querySelector('[data-auto-group-flow-border]')).toBe(null)
- await act(async () =>
- root.render(
-
- )
- )
+ rerender()
- assert.equal(container.textContent?.includes('3x'), true)
- assert.equal(container.querySelector('[data-auto-group-frame]'), null)
-
- await act(async () => root.unmount())
- container.remove()
+ expect(container).toHaveTextContent('3x')
+ expect(container.querySelector('[data-auto-group-frame]')).toBe(null)
})
})
diff --git a/web/src/features/keys/components/__tests__/api-key-group-combobox.test.tsx b/web/src/features/keys/components/__tests__/api-key-group-combobox.test.tsx
index 5c3b6525..7833c52c 100644
--- a/web/src/features/keys/components/__tests__/api-key-group-combobox.test.tsx
+++ b/web/src/features/keys/components/__tests__/api-key-group-combobox.test.tsx
@@ -16,58 +16,26 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { after, describe, test } from 'node:test'
-
-import { Window } from 'happy-dom'
-
-const domWindow = new Window()
-const domGlobals = [
- 'window',
- 'document',
- 'navigator',
- 'HTMLElement',
- 'HTMLButtonElement',
- 'HTMLInputElement',
- 'SVGElement',
- 'Node',
- 'Element',
- 'Event',
- 'KeyboardEvent',
- 'PointerEvent',
- 'CustomEvent',
- 'MutationObserver',
- 'ResizeObserver',
- 'requestAnimationFrame',
- 'cancelAnimationFrame',
- 'getComputedStyle',
-] as const
-
-for (const key of domGlobals) {
- Object.defineProperty(globalThis, key, {
- configurable: true,
- value: domWindow[key],
- })
-}
+import { fireEvent, render, screen, within } from '@testing-library/react'
+import { describe, expect, test } from 'vitest'
let shouldReduceMotion = false
-const reducedMotionMediaQuery = domWindow.matchMedia('(prefers-reduced-motion)')
+const reducedMotionMediaQuery = window.matchMedia('(prefers-reduced-motion)')
Object.defineProperty(reducedMotionMediaQuery, 'matches', {
configurable: true,
get: () => shouldReduceMotion,
})
-Object.defineProperty(domWindow, 'matchMedia', {
+Object.defineProperty(window, 'matchMedia', {
configurable: true,
value: () => reducedMotionMediaQuery,
})
function setReducedMotion(value: boolean) {
shouldReduceMotion = value
- reducedMotionMediaQuery.dispatchEvent(new domWindow.Event('change'))
+ reducedMotionMediaQuery.dispatchEvent(new Event('change'))
}
-const { act, useState } = await import('react')
-const { createRoot } = await import('react-dom/client')
+const { useState } = await import('react')
const { createInstance } = await import('i18next')
const { I18nextProvider, initReactI18next } = await import('react-i18next')
const { ApiKeyGroupCombobox } = await import('../api-key-group-combobox')
@@ -88,11 +56,6 @@ await i18n.use(initReactI18next).init({
},
})
-const reactTestGlobals = globalThis as typeof globalThis & {
- IS_REACT_ACT_ENVIRONMENT?: boolean
-}
-reactTestGlobals.IS_REACT_ACT_ENVIRONMENT = true
-
const options = [
{
value: 'auto',
@@ -119,176 +82,133 @@ function Harness(props: { initialValue: string }) {
)
}
-function getTrigger(container: ParentNode): HTMLButtonElement {
- const trigger = container.querySelector(
- 'button[role="combobox"]'
- )
- assert.ok(trigger)
- return trigger
+function getTrigger(): HTMLButtonElement {
+ return screen.getByRole('combobox')
}
function getCommandItem(label: string): HTMLElement {
const item = [
...document.querySelectorAll('[data-slot="command-item"]'),
].find((candidate) => candidate.textContent?.includes(label))
- assert.ok(item)
+ if (!item) {
+ throw new Error(`Expected command item containing "${label}"`)
+ }
return item
}
describe('API key group combobox Auto effect', () => {
- after(() => {
- domWindow.close()
- })
-
- test('rings the selected Auto trigger and its localized ratio without rendering the API ratio text', async () => {
+ test('rings the selected Auto trigger and its localized ratio without rendering the API ratio text', () => {
setReducedMotion(false)
- const container = document.createElement('div')
- document.body.append(container)
- const root = createRoot(container)
+ render()
- await act(async () => root.render())
-
- const trigger = getTrigger(container)
- assert.equal(trigger.getAttribute('aria-expanded'), 'false')
- assert.equal(trigger.dataset.autoGroupEffect, 'trigger')
- assert.equal(trigger.classList.contains('bg-linear-to-r'), false)
- assert.equal(trigger.classList.contains('overflow-hidden'), false)
- assert.equal(trigger.classList.contains('overflow-visible'), true)
+ const trigger = getTrigger()
+ expect(trigger).toHaveAttribute('aria-expanded', 'false')
+ expect(trigger).toHaveAttribute('data-auto-group-effect', 'trigger')
+ expect(trigger).not.toHaveClass('bg-linear-to-r', 'overflow-hidden')
+ expect(trigger).toHaveClass('overflow-visible')
const triggerFlowBorder = trigger.querySelector(
'[data-auto-group-flow-border]'
)
- assert.ok(triggerFlowBorder)
- assert.equal(triggerFlowBorder.getAttribute('aria-hidden'), 'true')
- assert.equal(
- triggerFlowBorder.classList.contains('pointer-events-none'),
- true
- )
- assert.equal(
- triggerFlowBorder.classList.contains('auto-group-flow-border'),
- true
+ expect(triggerFlowBorder).toHaveAttribute('aria-hidden', 'true')
+ expect(triggerFlowBorder).toHaveClass(
+ 'pointer-events-none',
+ 'auto-group-flow-border'
)
const triggerRatio = trigger.querySelector(
'[data-auto-group-effect="ratio"]'
)
- assert.ok(triggerRatio)
- assert.equal(triggerRatio.textContent, 'Auto Ratio')
- assert.equal(triggerRatio.textContent?.includes('Auto'), true)
- assert.equal(triggerRatio.textContent?.includes('x'), false)
- assert.equal(trigger.textContent?.includes('自动'), false)
- assert.equal(triggerRatio.classList.contains('relative'), true)
- assert.equal(triggerRatio.classList.contains('overflow-visible'), true)
- assert.equal(triggerRatio.classList.contains('rounded-4xl'), true)
- assert.ok(triggerRatio.querySelector('[data-auto-group-flow-border]'))
+ expect(triggerRatio).toHaveTextContent('Auto Ratio')
+ expect(triggerRatio).not.toHaveTextContent('x')
+ expect(trigger).not.toHaveTextContent('自动')
+ expect(triggerRatio).toHaveClass(
+ 'relative',
+ 'overflow-visible',
+ 'rounded-4xl'
+ )
+ expect(
+ triggerRatio?.querySelector('[data-auto-group-flow-border]')
+ ).toBeInTheDocument()
- await act(async () => trigger.click())
- assert.equal(trigger.getAttribute('aria-expanded'), 'true')
+ fireEvent.click(trigger)
+ expect(trigger).toHaveAttribute('aria-expanded', 'true')
const autoOption = getCommandItem('Global automatic routing')
- assert.equal(autoOption.dataset.autoGroupEffect, 'option')
- assert.equal(autoOption.getAttribute('aria-selected'), 'true')
- assert.equal(autoOption.classList.contains('bg-linear-to-r'), false)
- assert.equal(autoOption.classList.contains('overflow-visible'), true)
- assert.ok(autoOption.querySelector('[data-auto-group-flow-border]'))
+ expect(autoOption).toHaveAttribute('data-auto-group-effect', 'option')
+ expect(autoOption).toHaveAttribute('aria-selected', 'true')
+ expect(autoOption).not.toHaveClass('bg-linear-to-r')
+ expect(autoOption).toHaveClass('overflow-visible')
+ expect(
+ autoOption.querySelector('[data-auto-group-flow-border]')
+ ).toBeInTheDocument()
const optionRatio = autoOption.querySelector(
'[data-auto-group-effect="ratio"]'
)
- assert.ok(optionRatio)
- assert.equal(optionRatio.textContent, 'Auto Ratio')
- assert.ok(optionRatio.querySelector('[data-auto-group-flow-border]'))
+ expect(optionRatio).toHaveTextContent('Auto Ratio')
+ expect(
+ optionRatio?.querySelector('[data-auto-group-flow-border]')
+ ).toBeInTheDocument()
const defaultOption = getCommandItem('User group')
- assert.equal(defaultOption.hasAttribute('data-auto-group-effect'), false)
- assert.equal(
- defaultOption.querySelector('[data-auto-group-flow-border]'),
+ expect(defaultOption).not.toHaveAttribute('data-auto-group-effect')
+ expect(defaultOption.querySelector('[data-auto-group-flow-border]')).toBe(
null
)
- assert.equal(defaultOption.textContent?.includes('1x Ratio'), true)
- assert.equal(
- defaultOption.querySelector('[data-auto-group-effect="ratio"]'),
- null
- )
-
- await act(async () => root.unmount())
- container.remove()
+ expect(defaultOption).toHaveTextContent('1x Ratio')
+ expect(
+ defaultOption.querySelector('[data-auto-group-effect="ratio"]')
+ ).toBe(null)
})
test('keeps search and selection behavior while leaving normal groups unstyled', async () => {
setReducedMotion(false)
- const container = document.createElement('div')
- document.body.append(container)
- const root = createRoot(container)
+ const { container } = render()
- await act(async () => root.render())
+ const trigger = getTrigger()
+ fireEvent.click(trigger)
- const trigger = getTrigger(container)
- await act(async () => trigger.click())
-
- const searchInput = document.querySelector(
- 'input[placeholder="Search..."]'
- )
- assert.ok(searchInput)
- await act(async () => {
- const valueSetter = Object.getOwnPropertyDescriptor(
- domWindow.HTMLInputElement.prototype,
- 'value'
- )?.set
- assert.ok(valueSetter)
- valueSetter.call(searchInput, 'vip')
- searchInput.dispatchEvent(
- new domWindow.Event('input', { bubbles: true }) as unknown as Event
- )
+ fireEvent.input(screen.getByPlaceholderText('Search...'), {
+ target: { value: 'vip' },
})
const visibleOptions = [
...document.querySelectorAll('[data-slot="command-item"]'),
]
- assert.equal(
+ expect(
visibleOptions.some((option) =>
option.textContent?.includes('Global automatic routing')
- ),
- false
- )
+ )
+ ).toBe(false)
const vipOption = getCommandItem('Priority group')
- await act(async () => vipOption.click())
+ fireEvent.click(vipOption)
- assert.equal(
- container.querySelector('[data-testid="selected-group"]')?.textContent,
+ expect(within(container).getByTestId('selected-group')).toHaveTextContent(
'vip'
)
- assert.equal(trigger.getAttribute('aria-expanded'), 'false')
- assert.equal(trigger.hasAttribute('data-auto-group-effect'), false)
- assert.equal(trigger.querySelector('[data-auto-group-flow-border]'), null)
-
- await act(async () => root.unmount())
- container.remove()
+ expect(trigger).toHaveAttribute('aria-expanded', 'false')
+ expect(trigger).not.toHaveAttribute('data-auto-group-effect')
+ expect(trigger.querySelector('[data-auto-group-flow-border]')).toBe(null)
})
test('preserves the static Auto treatment but omits moving layers for reduced motion', async () => {
setReducedMotion(true)
- const container = document.createElement('div')
- document.body.append(container)
- const root = createRoot(container)
+ render()
- await act(async () => root.render())
+ const trigger = getTrigger()
+ expect(trigger).toHaveAttribute('data-auto-group-effect', 'trigger')
+ expect(trigger.querySelector('[data-auto-group-flow-border]')).toBe(null)
+ expect(
+ trigger.querySelector('[data-auto-group-effect="ratio"]')
+ ).toBeInTheDocument()
- const trigger = getTrigger(container)
- assert.equal(trigger.dataset.autoGroupEffect, 'trigger')
- assert.equal(trigger.querySelector('[data-auto-group-flow-border]'), null)
- assert.ok(trigger.querySelector('[data-auto-group-effect="ratio"]'))
-
- await act(async () => trigger.click())
+ fireEvent.click(trigger)
const autoOption = getCommandItem('Global automatic routing')
- assert.equal(autoOption.dataset.autoGroupEffect, 'option')
- assert.equal(
- autoOption.querySelector('[data-auto-group-flow-border]'),
- null
- )
- assert.ok(autoOption.querySelector('[data-auto-group-effect="ratio"]'))
-
- await act(async () => root.unmount())
- container.remove()
+ expect(autoOption).toHaveAttribute('data-auto-group-effect', 'option')
+ expect(autoOption.querySelector('[data-auto-group-flow-border]')).toBe(null)
+ expect(
+ autoOption.querySelector('[data-auto-group-effect="ratio"]')
+ ).toBeInTheDocument()
setReducedMotion(false)
})
})
diff --git a/web/src/features/keys/components/__tests__/api-keys-mutate-drawer.test.tsx b/web/src/features/keys/components/__tests__/api-keys-mutate-drawer.test.tsx
index 238d0b21..e57fcbfa 100644
--- a/web/src/features/keys/components/__tests__/api-keys-mutate-drawer.test.tsx
+++ b/web/src/features/keys/components/__tests__/api-keys-mutate-drawer.test.tsx
@@ -16,45 +16,9 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { after, afterEach, describe, test } from 'node:test'
+import { fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { afterEach, describe, expect, test } from 'vitest'
-import { Window } from 'happy-dom'
-
-const domWindow = new Window()
-const domGlobals = [
- 'window',
- 'document',
- 'navigator',
- 'HTMLElement',
- 'HTMLButtonElement',
- 'HTMLInputElement',
- 'HTMLFormElement',
- 'SVGElement',
- 'Node',
- 'Element',
- 'Event',
- 'KeyboardEvent',
- 'PointerEvent',
- 'MouseEvent',
- 'FocusEvent',
- 'CustomEvent',
- 'MutationObserver',
- 'ResizeObserver',
- 'requestAnimationFrame',
- 'cancelAnimationFrame',
- 'getComputedStyle',
-] as const
-
-for (const key of domGlobals) {
- Object.defineProperty(globalThis, key, {
- configurable: true,
- value: domWindow[key],
- })
-}
-
-const { act } = await import('react')
-const { createRoot } = await import('react-dom/client')
const { createInstance } = await import('i18next')
const { I18nextProvider, initReactI18next } = await import('react-i18next')
const { QueryClient, QueryClientProvider } =
@@ -69,20 +33,13 @@ await i18n.use(initReactI18next).init({
resources: { en: { translation: {} } },
})
-const reactTestGlobals = globalThis as typeof globalThis & {
- IS_REACT_ACT_ENVIRONMENT?: boolean
-}
-reactTestGlobals.IS_REACT_ACT_ENVIRONMENT = true
-
type ApiMethod = (url: string, data?: unknown) => Promise<{ data: unknown }>
type MockableApi = {
get: ApiMethod
post: ApiMethod
}
type RenderedDrawer = {
- host: HTMLDivElement
queryClient: InstanceType
- root: ReturnType
}
const apiClient = api as unknown as MockableApi
@@ -120,44 +77,14 @@ function installApiFixtures(createdPayloads: Array>) {
}
}
apiClient.post = async (url, data) => {
- assert.equal(url, '/api/token/')
- assert.ok(data && typeof data === 'object')
+ expect(url).toBe('/api/token/')
+ expect(data && typeof data === 'object').toBeTruthy()
createdPayloads.push(data as Record)
return { data: { success: true, data: {} } }
}
}
-async function waitForCondition(
- condition: () => boolean,
- failureMessage: string
-): Promise {
- if (condition()) return
-
- await new Promise((resolve, reject) => {
- const observer = new MutationObserver(() => {
- if (!condition()) return
- clearTimeout(timeoutId)
- observer.disconnect()
- resolve()
- })
- const timeoutId = setTimeout(() => {
- observer.disconnect()
- reject(new Error(`${failureMessage}: ${document.body.textContent}`))
- }, 1500)
-
- observer.observe(document, {
- attributes: true,
- childList: true,
- characterData: true,
- subtree: true,
- })
- })
-}
-
async function renderCreateDrawer(): Promise {
- const host = document.createElement('div')
- document.body.append(host)
- const root = createRoot(host)
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
})
@@ -192,43 +119,49 @@ async function renderCreateDrawer(): Promise {
},
{ updatedAt: freshAt }
)
- renderedDrawer = { host, queryClient, root }
+ renderedDrawer = { queryClient }
- await act(async () =>
- root.render(
-
-
-
- undefined} />
-
-
-
- )
+ render(
+
+
+
+ undefined} />
+
+
+
)
- await act(async () =>
- waitForCondition(() => {
+ await waitFor(
+ () => {
const saveButton = findButton('Save changes', false)
- return saveButton !== null && !saveButton.disabled
- }, 'API key drawer did not finish initializing')
+ expect(saveButton).toBeEnabled()
+ },
+ { timeout: 1500 }
)
}
function findButton(text: string, required: true): HTMLButtonElement
function findButton(text: string, required: false): HTMLButtonElement | null
function findButton(text: string, required = true): HTMLButtonElement | null {
- const button = [
- ...document.querySelectorAll('button'),
- ].find((candidate) => candidate.textContent?.includes(text))
- if (required) assert.ok(button, `Expected button containing "${text}"`)
+ const button = screen
+ .queryAllByRole('button')
+ .find((candidate) => candidate.textContent?.includes(text))
+ if (required && !button) {
+ throw new Error(`Expected button containing "${text}"`)
+ }
return button ?? null
}
-function getControlByLabel(labelText: string): T {
+function getControlByLabel(labelText: 'Name' | 'Quantity'): HTMLInputElement
+function getControlByLabel(labelText: 'Group'): HTMLButtonElement
+function getControlByLabel(labelText: 'Auto group order'): HTMLElement
+function getControlByLabel(labelText: string): HTMLElement {
const label = [...document.querySelectorAll('label')].find(
(candidate) => candidate.textContent?.trim() === labelText
)
- assert.ok(label, `Expected label "${labelText}"`)
- assert.ok(label.htmlFor)
+ if (!label) {
+ throw new Error(`Expected label "${labelText}"`)
+ }
+
const control =
label.control ??
label
@@ -236,51 +169,38 @@ function getControlByLabel(labelText: string): T {
?.querySelector(
'[data-slot="form-control"], input, textarea, button[role="combobox"], [role="group"]'
)
- assert.ok(control)
- return control as T
+ if (!control) {
+ throw new Error(`Expected control for label "${labelText}"`)
+ }
+ return control
}
-async function changeInput(input: HTMLInputElement, value: string) {
- await act(async () => {
- const valueSetter = Object.getOwnPropertyDescriptor(
- domWindow.HTMLInputElement.prototype,
- 'value'
- )?.set
- assert.ok(valueSetter)
- valueSetter.call(input, value)
- input.dispatchEvent(
- new domWindow.Event('input', { bubbles: true }) as unknown as Event
- )
- })
+function changeInput(input: HTMLInputElement, value: string): void {
+ fireEvent.input(input, { target: { value } })
}
-async function selectComboboxOption(
+function selectComboboxOption(
trigger: HTMLButtonElement,
optionDescription: string
-) {
- await act(async () => trigger.click())
+): void {
+ fireEvent.click(trigger)
const option = [
...document.querySelectorAll('[data-slot="command-item"]'),
].find((candidate) => candidate.textContent?.includes(optionDescription))
- assert.ok(option, `Expected option containing "${optionDescription}"`)
- await act(async () => option.click())
+ if (!option) {
+ throw new Error(`Expected option containing "${optionDescription}"`)
+ }
+ fireEvent.click(option)
}
-afterEach(async () => {
+afterEach(() => {
apiClient.get = originalGet
apiClient.post = originalPost
- domWindow.localStorage.clear()
+ localStorage.clear()
if (renderedDrawer) {
- await act(async () => renderedDrawer?.root.unmount())
renderedDrawer.queryClient.clear()
- renderedDrawer.host.remove()
renderedDrawer = null
}
- document.body.replaceChildren()
-})
-
-after(() => {
- domWindow.close()
})
describe('API keys mutate drawer Auto group integration', () => {
@@ -289,38 +209,31 @@ describe('API keys mutate drawer Auto group integration', () => {
installApiFixtures(createdPayloads)
await renderCreateDrawer()
- const groupTrigger = getControlByLabel('Group')
- assert.equal(groupTrigger.textContent?.includes('auto'), true)
- assert.equal(
+ const groupTrigger = getControlByLabel('Group')
+ expect(groupTrigger.textContent?.includes('auto')).toBe(true)
+ expect(
document.body.textContent?.includes(
'Using the complete global Auto order (2 groups)'
- ),
- true
- )
- assert.deepEqual(
+ )
+ ).toBe(true)
+ expect(
[
...document.querySelectorAll('[data-slot="global-auto-order-name"]'),
- ].map((item) => item.textContent),
- ['vip', 'default']
- )
- assert.equal(findButton('Restore global Auto', true).disabled, true)
+ ].map((item) => item.textContent)
+ ).toEqual(['vip', 'default'])
+ expect(findButton('Restore global Auto', true).disabled).toBe(true)
- await changeInput(getControlByLabel('Name'), 'batch')
- await changeInput(getControlByLabel('Quantity'), '2')
- await act(async () => findButton('Save changes', true).click())
- await act(async () =>
- waitForCondition(
- () => createdPayloads.length === 2,
- 'batch API keys were not created'
- )
- )
+ changeInput(getControlByLabel('Name'), 'batch')
+ changeInput(getControlByLabel('Quantity'), '2')
+ fireEvent.click(findButton('Save changes', true))
+ await waitFor(() => expect(createdPayloads).toHaveLength(2))
- assert.equal(createdPayloads.length, 2)
- assert.equal(createdPayloads[0]?.name, 'batch')
+ expect(createdPayloads.length).toBe(2)
+ expect(createdPayloads[0]?.name).toBe('batch')
for (const payload of createdPayloads) {
- assert.equal(payload.group, 'auto')
- assert.deepEqual(payload.auto_groups, [])
- assert.equal(payload.cross_group_retry, true)
+ expect(payload.group).toBe('auto')
+ expect(payload.auto_groups).toEqual([])
+ expect(payload.cross_group_retry).toBe(true)
}
})
@@ -329,43 +242,39 @@ describe('API keys mutate drawer Auto group integration', () => {
installApiFixtures(createdPayloads)
await renderCreateDrawer()
- const autoOrderControl = getControlByLabel('Auto group order')
+ const autoOrderControl = getControlByLabel('Auto group order')
const addGroupTrigger = autoOrderControl.querySelector(
'button[role="combobox"]'
)
- assert.ok(addGroupTrigger)
- await selectComboboxOption(addGroupTrigger, 'Priority access')
+ if (!addGroupTrigger) {
+ throw new Error('Expected Auto group order combobox')
+ }
+ selectComboboxOption(addGroupTrigger, 'Priority access')
- assert.ok(document.querySelector('button[aria-label="Remove vip"]'))
- assert.equal(
- document.body.textContent?.includes('1 / 3 groups selected'),
+ expect(
+ document.querySelector('button[aria-label="Remove vip"]')
+ ).toBeTruthy()
+ expect(document.body.textContent?.includes('1 / 3 groups selected')).toBe(
true
)
- assert.equal(findButton('Restore global Auto', true).disabled, false)
+ expect(findButton('Restore global Auto', true).disabled).toBe(false)
- const groupTrigger = getControlByLabel('Group')
- await selectComboboxOption(groupTrigger, 'Standard access')
- assert.equal(
- document.querySelector('button[aria-label="Remove vip"]'),
- null
- )
- await selectComboboxOption(groupTrigger, 'Automatic routing')
+ const groupTrigger = getControlByLabel('Group')
+ selectComboboxOption(groupTrigger, 'Standard access')
+ expect(document.querySelector('button[aria-label="Remove vip"]')).toBe(null)
+ selectComboboxOption(groupTrigger, 'Automatic routing')
- assert.ok(document.querySelector('button[aria-label="Remove vip"]'))
- assert.equal(
- document.body.textContent?.includes('1 / 3 groups selected'),
+ expect(
+ document.querySelector('button[aria-label="Remove vip"]')
+ ).toBeTruthy()
+ expect(document.body.textContent?.includes('1 / 3 groups selected')).toBe(
true
)
- assert.equal(findButton('Restore global Auto', true).disabled, false)
+ expect(findButton('Restore global Auto', true).disabled).toBe(false)
- await changeInput(getControlByLabel('Name'), 'custom')
- await act(async () => findButton('Save changes', true).click())
- await act(async () =>
- waitForCondition(
- () => createdPayloads.length === 1,
- 'custom-order API key was not created'
- )
- )
- assert.deepEqual(createdPayloads[0]?.auto_groups, ['vip'])
+ changeInput(getControlByLabel('Name'), 'custom')
+ fireEvent.click(findButton('Save changes', true))
+ await waitFor(() => expect(createdPayloads).toHaveLength(1))
+ expect(createdPayloads[0]?.auto_groups).toEqual(['vip'])
})
})
diff --git a/web/src/features/keys/components/__tests__/auto-group-order-editor.test.tsx b/web/src/features/keys/components/__tests__/auto-group-order-editor.test.tsx
index f37f5075..3df0d191 100644
--- a/web/src/features/keys/components/__tests__/auto-group-order-editor.test.tsx
+++ b/web/src/features/keys/components/__tests__/auto-group-order-editor.test.tsx
@@ -16,42 +16,10 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { after, describe, test } from 'node:test'
+import { fireEvent, render, within } from '@testing-library/react'
+import { describe, expect, test } from 'vitest'
-import { Window } from 'happy-dom'
-
-const domWindow = new Window()
-const domGlobals = [
- 'window',
- 'document',
- 'navigator',
- 'HTMLElement',
- 'HTMLButtonElement',
- 'HTMLInputElement',
- 'SVGElement',
- 'Node',
- 'Element',
- 'Event',
- 'KeyboardEvent',
- 'PointerEvent',
- 'CustomEvent',
- 'MutationObserver',
- 'ResizeObserver',
- 'requestAnimationFrame',
- 'cancelAnimationFrame',
- 'getComputedStyle',
-] as const
-
-for (const key of domGlobals) {
- Object.defineProperty(globalThis, key, {
- configurable: true,
- value: domWindow[key],
- })
-}
-
-const { act, useState } = await import('react')
-const { createRoot } = await import('react-dom/client')
+const { useState } = await import('react')
const { createInstance } = await import('i18next')
const { I18nextProvider, initReactI18next } = await import('react-i18next')
const { AutoGroupOrderEditor } = await import('../auto-group-order-editor')
@@ -90,11 +58,6 @@ await i18n.use(initReactI18next).init({
},
})
-const reactTestGlobals = globalThis as typeof globalThis & {
- IS_REACT_ACT_ENVIRONMENT?: boolean
-}
-reactTestGlobals.IS_REACT_ACT_ENVIRONMENT = true
-
const globalOptions = [
{ value: 'vip', label: 'VIP', desc: 'Priority access', ratio: 3 },
{ value: 'default', label: 'Default', desc: 'Standard access', ratio: 1 },
@@ -176,163 +139,104 @@ function CustomEmptyHarness() {
)
}
-function findButton(container: ParentNode, label: string): HTMLButtonElement {
- const button = container.querySelector(
- `button[aria-label="${label}"]`
- )
- assert.ok(button)
- return button
+function findButton(container: HTMLElement, label: string): HTMLButtonElement {
+ return within(container).getByRole('button', { name: label })
+}
+
+function getCommandItem(label: string): HTMLElement {
+ const item = [
+ ...document.querySelectorAll('[data-slot="command-item"]'),
+ ].find((candidate) => candidate.textContent?.includes(label))
+ if (!item) {
+ throw new Error(`Expected command item containing "${label}"`)
+ }
+ return item
}
describe('Auto group order editor', () => {
- after(() => {
- domWindow.close()
- })
+ test('enforces the limit and exposes accessible reorder controls', () => {
+ const { container } = render()
- test('enforces the limit and exposes accessible reorder controls', async () => {
- const container = document.createElement('div')
- document.body.append(container)
- const root = createRoot(container)
+ const addButton = within(container).getByRole('combobox')
+ expect(addButton).toBeDisabled()
+ expect(container).toHaveTextContent('2 / 2 groups selected')
+ expect(
+ within(container).getByRole('group', { name: 'Auto group order' })
+ ).toBeInTheDocument()
+ expect(findButton(container, 'Drag default to reorder').type).toBe('button')
- await act(async () => root.render())
-
- const addButton = container.querySelector(
- 'button[role="combobox"]'
- )
- assert.ok(addButton)
- assert.equal(addButton.disabled, true)
- assert.equal(container.textContent?.includes('2 / 2 groups selected'), true)
- assert.ok(
- container.querySelector('[role="group"][aria-label="Auto group order"]')
- )
- assert.equal(
- findButton(container, 'Drag default to reorder').type,
- 'button'
- )
-
- await act(async () => findButton(container, 'Move default down').click())
- assert.equal(
- container.querySelector('[data-testid="order"]')?.textContent,
+ fireEvent.click(findButton(container, 'Move default down'))
+ expect(within(container).getByTestId('order')).toHaveTextContent(
'vip,default'
)
- await act(async () => {
- findButton(container, 'Drag vip to reorder').dispatchEvent(
- new domWindow.KeyboardEvent('keydown', {
- key: 'ArrowDown',
- bubbles: true,
- }) as unknown as KeyboardEvent
- )
+ fireEvent.keyDown(findButton(container, 'Drag vip to reorder'), {
+ key: 'ArrowDown',
})
- assert.equal(
- container.querySelector('[data-testid="order"]')?.textContent,
+ expect(within(container).getByTestId('order')).toHaveTextContent(
'default,vip'
)
-
- await act(async () => root.unmount())
- container.remove()
})
- test('adds and removes groups, then restores inheritance as an empty value', async () => {
- const container = document.createElement('div')
- document.body.append(container)
- const root = createRoot(container)
+ test('adds and removes groups, then restores inheritance as an empty value', () => {
+ const { container } = render()
+ fireEvent.click(findButton(container, 'Remove vip'))
- await act(async () => root.render())
- await act(async () => findButton(container, 'Remove vip').click())
+ expect(within(container).getByTestId('order')).toHaveTextContent('default')
+ const addButton = within(container).getByRole('combobox')
+ expect(addButton).toBeEnabled()
- assert.equal(
- container.querySelector('[data-testid="order"]')?.textContent,
- 'default'
- )
- const addButton = container.querySelector(
- 'button[role="combobox"]'
- )
- assert.ok(addButton)
- assert.equal(addButton.disabled, false)
-
- await act(async () => addButton.click())
- const teamOption = [
- ...document.querySelectorAll('[data-slot="command-item"]'),
- ].find((option) => option.textContent?.includes('team'))
- assert.ok(teamOption)
- await act(async () => teamOption.click())
- assert.equal(
- container.querySelector('[data-testid="order"]')?.textContent,
+ fireEvent.click(addButton)
+ fireEvent.click(getCommandItem('team'))
+ expect(within(container).getByTestId('order')).toHaveTextContent(
'default,team'
)
- assert.equal(addButton.disabled, true)
+ expect(addButton).toBeDisabled()
- const restoreButton = [...container.querySelectorAll('button')].find(
- (button) => button.textContent?.includes('Restore global Auto')
+ fireEvent.click(
+ within(container).getByRole('button', { name: 'Restore global Auto' })
)
- assert.ok(restoreButton)
- await act(async () => restoreButton.click())
- assert.equal(
- container.querySelector('[data-testid="order"]')?.textContent,
- ''
- )
- assert.equal(
- container.querySelector('[data-testid="mode"]')?.textContent,
- 'inherit'
- )
- assert.equal(
- container.textContent?.includes(
- 'Using the complete global Auto order (3 groups)'
- ),
- true
+ expect(within(container).getByTestId('order')).toBeEmptyDOMElement()
+ expect(within(container).getByTestId('mode')).toHaveTextContent('inherit')
+ expect(container).toHaveTextContent(
+ 'Using the complete global Auto order (3 groups)'
)
const inheritedItems = container.querySelectorAll(
'[data-slot="global-auto-order"] > li'
)
- assert.deepEqual(
+ expect(
[...inheritedItems].map(
(item) =>
item.querySelector('[data-slot="global-auto-order-name"]')
?.textContent
- ),
- ['VIP', 'Default', 'Team']
- )
-
- await act(async () => root.unmount())
- container.remove()
+ )
+ ).toEqual(['VIP', 'Default', 'Team'])
})
- test('shows the complete inherited order with metadata beyond the custom limit', async () => {
- const container = document.createElement('div')
- document.body.append(container)
- const root = createRoot(container)
+ test('shows the complete inherited order with metadata beyond the custom limit', () => {
+ const { container } = render()
- await act(async () => root.render())
-
- assert.equal(
- container.textContent?.includes(
- 'Using the complete global Auto order (3 groups)'
- ),
- true
- )
- assert.equal(
- container.textContent?.includes('0 / 2 groups selected'),
- false
+ expect(container).toHaveTextContent(
+ 'Using the complete global Auto order (3 groups)'
)
+ expect(container).not.toHaveTextContent('0 / 2 groups selected')
const order = container.querySelector(
'[data-slot="global-auto-order"]'
)
- assert.ok(order)
- assert.equal(order.classList.contains('overflow-y-auto'), true)
- assert.equal(order.classList.contains('flex-wrap'), true)
+ if (!order) {
+ throw new Error('Expected inherited Auto group order')
+ }
+ expect(order).toHaveClass('overflow-y-auto', 'flex-wrap')
const items = [...order.querySelectorAll('li')]
- assert.equal(items.length, 3)
- assert.equal(
- order.querySelectorAll('[data-slot="global-auto-order-connector"]')
- .length,
- 2
- )
- assert.deepEqual(
+ expect(items.length).toBe(3)
+ expect(
+ order.querySelectorAll('[data-slot="global-auto-order-connector"]').length
+ ).toBe(2)
+ expect(
items.map((item) => ({
index: item.querySelector('[data-slot="global-auto-order-index"]')
?.textContent,
@@ -345,196 +249,117 @@ describe('Auto group order editor', () => {
'[data-slot="global-auto-order-description"]'
)?.textContent,
ratio: item.querySelector('[data-slot="badge"]')?.textContent,
- })),
- [
- {
- index: '1',
- name: 'VIP',
- title: 'Priority access',
- description: 'Priority access',
- ratio: '3x Ratio',
- },
- {
- index: '2',
- name: 'Default',
- title: 'Standard access',
- description: 'Standard access',
- ratio: '1x Ratio',
- },
- {
- index: '3',
- name: 'Team',
- title: 'Shared access',
- description: 'Shared access',
- ratio: '2x Ratio',
- },
- ]
- )
+ }))
+ ).toEqual([
+ {
+ index: '1',
+ name: 'VIP',
+ title: 'Priority access',
+ description: 'Priority access',
+ ratio: '3x Ratio',
+ },
+ {
+ index: '2',
+ name: 'Default',
+ title: 'Standard access',
+ description: 'Standard access',
+ ratio: '1x Ratio',
+ },
+ {
+ index: '3',
+ name: 'Team',
+ title: 'Shared access',
+ description: 'Shared access',
+ ratio: '2x Ratio',
+ },
+ ])
for (const item of items) {
const chip = item.querySelector('[data-slot="global-auto-order-chip"]')
- assert.ok(chip)
+ expect(chip).toBeInTheDocument()
const description = item.querySelector(
'[data-slot="global-auto-order-description"]'
)
- assert.ok(description)
- assert.equal(description.classList.contains('sr-only'), true)
+ expect(description).toHaveClass('sr-only')
}
- assert.equal(
- items[0]?.querySelector('[data-slot="global-auto-order-connector"]'),
- null
- )
+ expect(
+ items[0]?.querySelector('[data-slot="global-auto-order-connector"]')
+ ).toBe(null)
for (const item of items.slice(1)) {
const connector = item.querySelector(
'[data-slot="global-auto-order-connector"]'
)
- assert.ok(connector)
- assert.equal(connector.getAttribute('aria-hidden'), 'true')
+ expect(connector).toHaveAttribute('aria-hidden', 'true')
}
- assert.equal(container.querySelector('[aria-label^="Drag "]'), null)
- assert.equal(container.querySelector('[aria-label^="Move "]'), null)
- assert.equal(container.querySelector('[aria-label^="Remove "]'), null)
+ expect(container.querySelector('[aria-label^="Drag "]')).toBe(null)
+ expect(container.querySelector('[aria-label^="Move "]')).toBe(null)
+ expect(container.querySelector('[aria-label^="Remove "]')).toBe(null)
- const restoreButton = [...container.querySelectorAll('button')].find(
- (button) => button.textContent?.includes('Restore global Auto')
- )
- assert.ok(restoreButton)
- assert.equal(restoreButton.disabled, true)
-
- await act(async () => root.unmount())
- container.remove()
+ expect(
+ within(container).getByRole('button', { name: 'Restore global Auto' })
+ ).toBeDisabled()
})
- test('shows an explicit empty state when the global Auto order has no groups', async () => {
- const container = document.createElement('div')
- document.body.append(container)
- const root = createRoot(container)
+ test('shows an explicit empty state when the global Auto order has no groups', () => {
+ const { container } = render()
- await act(async () =>
- root.render()
+ expect(container).toHaveTextContent(
+ 'Using the complete global Auto order (0 groups)'
)
+ expect(container).toHaveTextContent(
+ 'No available groups in the global Auto order.'
+ )
+ expect(container.querySelector('[data-slot="global-auto-order"]')).toBe(
+ null
+ )
+ })
- assert.equal(
- container.textContent?.includes(
- 'Using the complete global Auto order (0 groups)'
- ),
- true
+ test('keeps an empty custom order distinct from global inheritance', () => {
+ const { container } = render()
+
+ expect(within(container).getByTestId('mode')).toHaveTextContent('custom')
+ expect(container).toHaveTextContent(
+ 'No valid custom Auto groups remain. Add a group or restore global Auto.'
)
- assert.equal(
- container.textContent?.includes(
- 'No available groups in the global Auto order.'
- ),
- true
- )
- assert.equal(
- container.querySelector('[data-slot="global-auto-order"]'),
+ expect(container.querySelector('[data-slot="global-auto-order"]')).toBe(
null
)
- await act(async () => root.unmount())
- container.remove()
+ const restoreButton = within(container).getByRole('button', {
+ name: 'Restore global Auto',
+ })
+ expect(restoreButton).toBeEnabled()
+ fireEvent.click(restoreButton)
+
+ expect(within(container).getByTestId('mode')).toHaveTextContent('inherit')
+ expect(
+ container.querySelector('[data-slot="global-auto-order"]')
+ ).toBeInTheDocument()
})
- test('keeps an empty custom order distinct from global inheritance', async () => {
- const container = document.createElement('div')
- document.body.append(container)
- const root = createRoot(container)
+ test('adding a group from inheritance explicitly creates a custom order', () => {
+ const { container } = render()
- await act(async () => root.render())
+ fireEvent.click(within(container).getByRole('combobox'))
+ fireEvent.click(getCommandItem('VIP'))
- assert.equal(
- container.querySelector('[data-testid="mode"]')?.textContent,
- 'custom'
- )
- assert.equal(
- container.textContent?.includes(
- 'No valid custom Auto groups remain. Add a group or restore global Auto.'
- ),
- true
- )
- assert.equal(
- container.querySelector('[data-slot="global-auto-order"]'),
+ expect(within(container).getByTestId('mode')).toHaveTextContent('custom')
+ expect(within(container).getByTestId('order')).toHaveTextContent('vip')
+ expect(container.querySelector('[data-slot="global-auto-order"]')).toBe(
null
)
-
- const restoreButton = [...container.querySelectorAll('button')].find(
- (button) => button.textContent?.includes('Restore global Auto')
- )
- assert.ok(restoreButton)
- assert.equal(restoreButton.disabled, false)
- await act(async () => restoreButton.click())
-
- assert.equal(
- container.querySelector('[data-testid="mode"]')?.textContent,
- 'inherit'
- )
- assert.ok(container.querySelector('[data-slot="global-auto-order"]'))
-
- await act(async () => root.unmount())
- container.remove()
})
- test('adding a group from inheritance explicitly creates a custom order', async () => {
- const container = document.createElement('div')
- document.body.append(container)
- const root = createRoot(container)
+ test('removing the last custom group does not silently enable inheritance', () => {
+ const { container } = render()
+ fireEvent.click(findButton(container, 'Remove default'))
- await act(async () => root.render())
-
- const addButton = container.querySelector(
- 'button[role="combobox"]'
+ expect(within(container).getByTestId('order')).toBeEmptyDOMElement()
+ expect(within(container).getByTestId('mode')).toHaveTextContent('custom')
+ expect(container).toHaveTextContent(
+ 'No valid custom Auto groups remain. Add a group or restore global Auto.'
)
- assert.ok(addButton)
- await act(async () => addButton.click())
- const vipOption = [
- ...document.querySelectorAll('[data-slot="command-item"]'),
- ].find((option) => option.textContent?.includes('VIP'))
- assert.ok(vipOption)
- await act(async () => vipOption.click())
-
- assert.equal(
- container.querySelector('[data-testid="mode"]')?.textContent,
- 'custom'
- )
- assert.equal(
- container.querySelector('[data-testid="order"]')?.textContent,
- 'vip'
- )
- assert.equal(
- container.querySelector('[data-slot="global-auto-order"]'),
- null
- )
-
- await act(async () => root.unmount())
- container.remove()
- })
-
- test('removing the last custom group does not silently enable inheritance', async () => {
- const container = document.createElement('div')
- document.body.append(container)
- const root = createRoot(container)
-
- await act(async () => root.render())
- await act(async () => findButton(container, 'Remove default').click())
-
- assert.equal(
- container.querySelector('[data-testid="order"]')?.textContent,
- ''
- )
- assert.equal(
- container.querySelector('[data-testid="mode"]')?.textContent,
- 'custom'
- )
- assert.equal(
- container.textContent?.includes(
- 'No valid custom Auto groups remain. Add a group or restore global Auto.'
- ),
- true
- )
-
- await act(async () => root.unmount())
- container.remove()
})
})
diff --git a/web/src/features/keys/lib/__tests__/auto-group-form.test.ts b/web/src/features/keys/lib/__tests__/auto-group-form.test.ts
index 296023b6..479ff594 100644
--- a/web/src/features/keys/lib/__tests__/auto-group-form.test.ts
+++ b/web/src/features/keys/lib/__tests__/auto-group-form.test.ts
@@ -16,10 +16,8 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { describe, test } from 'node:test'
-
import type { TFunction } from 'i18next'
+import { describe, expect, test } from 'vitest'
import { apiKeySchema, type ApiKey } from '../../types'
import {
@@ -60,16 +58,16 @@ describe('API key Auto group form mapping', () => {
const legacyApiKey: Record = { ...baseApiKey }
delete legacyApiKey.auto_groups
- assert.equal(apiKeySchema.parse(legacyApiKey).auto_groups, null)
+ expect(apiKeySchema.parse(legacyApiKey).auto_groups).toBe(null)
})
test('creates an Auto token that inherits the global order', () => {
const defaults = getApiKeyFormDefaultValues(true)
- assert.equal(defaults.group, 'auto')
- assert.equal(defaults.auto_groups_mode, 'inherit')
- assert.deepEqual(defaults.auto_groups, [])
- assert.deepEqual(transformFormDataToPayload(defaults).auto_groups, [])
+ expect(defaults.group).toBe('auto')
+ expect(defaults.auto_groups_mode).toBe('inherit')
+ expect(defaults.auto_groups).toEqual([])
+ expect(transformFormDataToPayload(defaults).auto_groups).toEqual([])
})
test('maps omitted, null, and empty snapshots to inheritance on edit', () => {
@@ -88,8 +86,8 @@ describe('API key Auto group form mapping', () => {
2
)
- assert.equal(defaults.auto_groups_mode, 'inherit')
- assert.deepEqual(defaults.auto_groups, [])
+ expect(defaults.auto_groups_mode).toBe('inherit')
+ expect(defaults.auto_groups).toEqual([])
}
})
@@ -103,8 +101,8 @@ describe('API key Auto group form mapping', () => {
2
)
- assert.equal(defaults.auto_groups_mode, 'custom')
- assert.deepEqual(defaults.auto_groups, ['vip', 'default'])
+ expect(defaults.auto_groups_mode).toBe('custom')
+ expect(defaults.auto_groups).toEqual(['vip', 'default'])
})
test('keeps a fully filtered snapshot custom and rejects it until resolved', () => {
@@ -114,15 +112,14 @@ describe('API key Auto group form mapping', () => {
2
)
- assert.equal(defaults.auto_groups_mode, 'custom')
- assert.deepEqual(defaults.auto_groups, [])
+ expect(defaults.auto_groups_mode).toBe('custom')
+ expect(defaults.auto_groups).toEqual([])
const result = getApiKeyFormSchema(t, 2).safeParse(defaults)
- assert.equal(result.success, false)
+ expect(result.success).toBe(false)
if (result.success) return
- assert.deepEqual(result.error.issues[0]?.path, ['auto_groups'])
- assert.equal(
- result.error.issues[0]?.message,
+ expect(result.error.issues[0]?.path).toEqual(['auto_groups'])
+ expect(result.error.issues[0]?.message).toBe(
'Select at least one Auto group or restore global Auto.'
)
})
@@ -134,7 +131,7 @@ describe('API key Auto group form mapping', () => {
auto_groups: ['vip', 'default'],
}
- assert.deepEqual(transformFormDataToPayload(custom).auto_groups, [
+ expect(transformFormDataToPayload(custom).auto_groups).toEqual([
'vip',
'default',
])
@@ -142,7 +139,7 @@ describe('API key Auto group form mapping', () => {
test('submits an empty array for inheritance and for non-Auto groups', () => {
const inherited = getApiKeyFormDefaultValues(true)
- assert.deepEqual(transformFormDataToPayload(inherited).auto_groups, [])
+ expect(transformFormDataToPayload(inherited).auto_groups).toEqual([])
const nonAuto = {
...inherited,
@@ -150,8 +147,8 @@ describe('API key Auto group form mapping', () => {
auto_groups_mode: 'custom' as const,
auto_groups: ['vip'],
}
- assert.deepEqual(transformFormDataToPayload(nonAuto).auto_groups, [])
- assert.equal(transformFormDataToPayload(nonAuto).cross_group_retry, false)
+ expect(transformFormDataToPayload(nonAuto).auto_groups).toEqual([])
+ expect(transformFormDataToPayload(nonAuto).cross_group_retry).toBe(false)
})
test('rejects snapshots over the configured limit', () => {
@@ -162,13 +159,10 @@ describe('API key Auto group form mapping', () => {
auto_groups: ['default', 'vip'],
})
- assert.equal(result.success, false)
+ expect(result.success).toBe(false)
if (result.success) return
- assert.equal(result.error.issues[0]?.path[0], 'auto_groups')
- assert.equal(
- result.error.issues[0]?.message,
- 'Select at most 1 Auto groups'
- )
+ expect(result.error.issues[0]?.path[0]).toBe('auto_groups')
+ expect(result.error.issues[0]?.message).toBe('Select at most 1 Auto groups')
})
test('rejects duplicate custom groups', () => {
@@ -179,10 +173,9 @@ describe('API key Auto group form mapping', () => {
auto_groups: ['vip', 'vip'],
})
- assert.equal(result.success, false)
+ expect(result.success).toBe(false)
if (result.success) return
- assert.equal(
- result.error.issues[0]?.message,
+ expect(result.error.issues[0]?.message).toBe(
'Auto groups must not contain duplicates'
)
})
diff --git a/web/src/features/playground/hooks/use-stream-request.test.ts b/web/src/features/playground/hooks/use-stream-request.test.ts
index a913fec3..29bcde76 100644
--- a/web/src/features/playground/hooks/use-stream-request.test.ts
+++ b/web/src/features/playground/hooks/use-stream-request.test.ts
@@ -16,8 +16,7 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { describe, test } from 'node:test'
+import { describe, expect, test } from 'vitest'
import type { ChatCompletionRequest } from '../types'
import { createStreamRequestController } from './use-stream-request'
@@ -103,12 +102,12 @@ describe('latest-wins stream request coordination', () => {
const second = controller.send(payload, noopCallbacks)
firstHeaders.resolve({ Authorization: 'Bearer stale' })
await first
- assert.equal(sources.length, 0)
+ expect(sources.length).toBe(0)
secondHeaders.resolve({ Authorization: 'Bearer current' })
await second
- assert.equal(sources.length, 1)
- assert.equal(sources[0]?.streamed, true)
+ expect(sources.length).toBe(1)
+ expect(sources[0]?.streamed).toBe(true)
})
test('stop cancels a request that is still waiting for headers', async () => {
@@ -128,7 +127,7 @@ describe('latest-wins stream request coordination', () => {
headers.resolve({ Authorization: 'Bearer ignored' })
await request
- assert.equal(sourceCount, 0)
+ expect(sourceCount).toBe(0)
})
test('dispose cancels a pending header request without a state update', async () => {
@@ -149,8 +148,8 @@ describe('latest-wins stream request coordination', () => {
headers.resolve({ Authorization: 'Bearer ignored' })
await request
- assert.equal(sourceCount, 0)
- assert.deepEqual(streamingStates, [false])
+ expect(sourceCount).toBe(0)
+ expect(streamingStates).toEqual([false])
})
test('closes the previous source and ignores all of its later events', async () => {
@@ -182,7 +181,7 @@ describe('latest-wins stream request coordination', () => {
await controller.send(payload, callbacks)
const second = controller.send(payload, callbacks)
- assert.equal(sources[0]?.closed, true)
+ expect(sources[0]?.closed).toBe(true)
sources[0]?.emit(
'message',
JSON.stringify({ choices: [{ delta: { content: 'stale' } }] })
@@ -195,6 +194,6 @@ describe('latest-wins stream request coordination', () => {
JSON.stringify({ choices: [{ delta: { content: 'current' } }] })
)
- assert.deepEqual(updates, ['current'])
+ expect(updates).toEqual(['current'])
})
})
diff --git a/web/src/features/profile/components/__tests__/login-session-utils.test.ts b/web/src/features/profile/components/__tests__/login-session-utils.test.ts
index c75df750..f97c0b3e 100644
--- a/web/src/features/profile/components/__tests__/login-session-utils.test.ts
+++ b/web/src/features/profile/components/__tests__/login-session-utils.test.ts
@@ -1,3 +1,4 @@
+import type { TFunction } from 'i18next'
/*
Copyright (C) 2023-2026 QuantumNous
@@ -16,10 +17,7 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { describe, test } from 'node:test'
-
-import type { TFunction } from 'i18next'
+import { describe, expect, test } from 'vitest'
import { loginMethodLabel, sessionDevice } from '../login-session-utils'
@@ -27,14 +25,10 @@ const translate = ((key: string) => key) as TFunction
describe('login session presentation', () => {
test('labels built-in and provider OAuth login methods', () => {
- assert.equal(loginMethodLabel('password', translate), 'Password')
- assert.equal(
- loginMethodLabel('2fa', translate),
- 'Two-factor Authentication'
- )
- assert.equal(loginMethodLabel('oauth:github', translate), 'OAuth · GitHub')
- assert.equal(
- loginMethodLabel('oauth:custom-provider', translate),
+ expect(loginMethodLabel('password', translate)).toBe('Password')
+ expect(loginMethodLabel('2fa', translate)).toBe('Two-factor Authentication')
+ expect(loginMethodLabel('oauth:github', translate)).toBe('OAuth · GitHub')
+ expect(loginMethodLabel('oauth:custom-provider', translate)).toBe(
'OAuth · custom-provider'
)
})
@@ -43,8 +37,7 @@ describe('login session presentation', () => {
const userAgent =
'Mozilla/5.0 (iPad; CPU OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1'
- assert.equal(
- sessionDevice(userAgent, 'Unknown device', 'Browser'),
+ expect(sessionDevice(userAgent, 'Unknown device', 'Browser')).toBe(
'Safari · iOS'
)
})
@@ -53,8 +46,7 @@ describe('login session presentation', () => {
const userAgent =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15'
- assert.equal(
- sessionDevice(userAgent, 'Unknown device', 'Browser', 5),
+ expect(sessionDevice(userAgent, 'Unknown device', 'Browser', 5)).toBe(
'Safari · iOS'
)
})
@@ -63,8 +55,7 @@ describe('login session presentation', () => {
const userAgent =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'
- assert.equal(
- sessionDevice(userAgent, 'Unknown device', 'Browser', 10),
+ expect(sessionDevice(userAgent, 'Unknown device', 'Browser', 10)).toBe(
'Chrome · Windows'
)
})
@@ -73,8 +64,7 @@ describe('login session presentation', () => {
const userAgent =
'Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36'
- assert.equal(
- sessionDevice(userAgent, 'Unknown device', 'Browser', 5),
+ expect(sessionDevice(userAgent, 'Unknown device', 'Browser', 5)).toBe(
'Chrome · Android'
)
})
@@ -83,15 +73,13 @@ describe('login session presentation', () => {
const userAgent =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15'
- assert.equal(
- sessionDevice(userAgent, 'Unknown device', 'Browser'),
+ expect(sessionDevice(userAgent, 'Unknown device', 'Browser')).toBe(
'Safari · macOS'
)
})
test('falls back to the unknown-device label for an empty user agent', () => {
- assert.equal(
- sessionDevice('', 'Unknown device', 'Browser'),
+ expect(sessionDevice('', 'Unknown device', 'Browser')).toBe(
'Unknown device'
)
})
diff --git a/web/src/features/redemption-codes/components/__tests__/redemptions-mutate-drawer.test.tsx b/web/src/features/redemption-codes/components/__tests__/redemptions-mutate-drawer.test.tsx
index 46008e10..b100c55c 100644
--- a/web/src/features/redemption-codes/components/__tests__/redemptions-mutate-drawer.test.tsx
+++ b/web/src/features/redemption-codes/components/__tests__/redemptions-mutate-drawer.test.tsx
@@ -16,56 +16,17 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-
-import { Window } from 'happy-dom'
+import {
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+ type RenderResult,
+} from '@testing-library/react'
+import { afterEach, describe, expect, test } from 'vitest'
import type { Redemption } from '../../types'
-// Use Bun's runner at runtime while reusing the Node test types installed here.
-const bunTestModule = 'bun:test'
-const { afterAll, afterEach, test } = (await import(bunTestModule)) as {
- afterAll: typeof import('node:test').after
- afterEach: typeof import('node:test').afterEach
- test: typeof import('node:test').test
-}
-
-const domWindow = new Window()
-const domGlobals = [
- 'window',
- 'document',
- 'navigator',
- 'HTMLElement',
- 'HTMLButtonElement',
- 'HTMLInputElement',
- 'HTMLFormElement',
- 'HTMLLabelElement',
- 'HTMLFieldSetElement',
- 'SVGElement',
- 'Node',
- 'Element',
- 'Event',
- 'KeyboardEvent',
- 'PointerEvent',
- 'MouseEvent',
- 'FocusEvent',
- 'CustomEvent',
- 'MutationObserver',
- 'ResizeObserver',
- 'requestAnimationFrame',
- 'cancelAnimationFrame',
- 'getComputedStyle',
-] as const
-
-for (const key of domGlobals) {
- Object.defineProperty(globalThis, key, {
- configurable: true,
- value: domWindow[key],
- })
-}
-
-const { act } = await import('react')
-const { createRoot } = await import('react-dom/client')
const i18n = (await import('i18next')).default
const { I18nextProvider, initReactI18next } = await import('react-i18next')
const { Toaster, toast } = await import('sonner')
@@ -88,19 +49,13 @@ await i18n.use(initReactI18next).init({
},
})
-const reactTestGlobals = globalThis as typeof globalThis & {
- IS_REACT_ACT_ENVIRONMENT?: boolean
-}
-reactTestGlobals.IS_REACT_ACT_ENVIRONMENT = true
-
type ApiMethod = (url: string, data?: unknown) => Promise<{ data: unknown }>
type MockableApi = {
get: ApiMethod
put: ApiMethod
}
type RenderedDrawer = {
- host: HTMLDivElement
- root: ReturnType
+ result: RenderResult
}
type CurrencyFixture = {
quotaDisplayType: 'USD' | 'CNY'
@@ -171,269 +126,189 @@ async function renderDrawer(
},
})
- const host = document.createElement('div')
- document.body.append(host)
- const root = createRoot(host)
- renderedDrawer = { host, root }
-
- await act(async () => root.render(drawerTree(currentRow)))
+ renderedDrawer = { result: render(drawerTree(currentRow)) }
}
async function rerenderDrawer(currentRow: Redemption): Promise {
- assert.ok(renderedDrawer)
- await act(async () => renderedDrawer?.root.render(drawerTree(currentRow)))
-}
-
-async function waitForCondition(
- condition: () => boolean,
- failureMessage: string
-): Promise {
- if (condition()) return
-
- await new Promise((resolve, reject) => {
- const observer = new MutationObserver(() => {
- if (!condition()) return
- clearTimeout(timeoutId)
- observer.disconnect()
- resolve()
- })
- const timeoutId = setTimeout(() => {
- observer.disconnect()
- reject(new Error(`${failureMessage}: ${document.body.textContent}`))
- }, 1500)
-
- observer.observe(document, {
- attributes: true,
- childList: true,
- characterData: true,
- subtree: true,
- })
- })
+ if (!renderedDrawer) {
+ throw new Error('Expected a rendered redemption drawer')
+ }
+ renderedDrawer.result.rerender(drawerTree(currentRow))
}
function getSaveButton(): HTMLButtonElement {
- const button = document.querySelector(
- 'button[form="redemption-form"][type="submit"]'
- )
- assert.ok(button)
- return button
+ return screen.getByRole('button', { name: 'Save changes' })
}
-function getControlByLabel(labelText: string): T {
+function getControlByLabel(labelText: 'Name'): HTMLInputElement
+function getControlByLabel(labelText: 'Quota (CNY)'): HTMLInputElement
+function getControlByLabel(labelText: 'Quota (USD)'): HTMLInputElement
+function getControlByLabel(labelText: string): HTMLElement {
const label = [...document.querySelectorAll('label')].find(
(candidate) => candidate.textContent?.trim() === labelText
)
- assert.ok(label, `Expected label "${labelText}"`)
- assert.ok(label.htmlFor)
+ if (!label) {
+ throw new Error(`Expected label "${labelText}"`)
+ }
const control =
label.control ??
label
.closest('[data-slot="form-item"]')
?.querySelector('[data-slot="form-control"], input')
- assert.ok(control)
- return control as T
+ if (!control) {
+ throw new Error(`Expected control for label "${labelText}"`)
+ }
+ return control
}
-async function changeInput(input: HTMLInputElement, value: string) {
- await act(async () => {
- const valueSetter = Object.getOwnPropertyDescriptor(
- domWindow.HTMLInputElement.prototype,
- 'value'
- )?.set
- assert.ok(valueSetter)
- valueSetter.call(input, value)
- input.dispatchEvent(
- new domWindow.Event('input', { bubbles: true }) as unknown as Event
- )
- })
+function changeInput(input: HTMLInputElement, value: string): void {
+ fireEvent.input(input, { target: { value } })
}
-async function submitForm(): Promise {
+function submitForm(): void {
const form = document.querySelector('#redemption-form')
- assert.ok(form)
- await act(async () =>
- form.dispatchEvent(
- new domWindow.Event('submit', {
- bubbles: true,
- cancelable: true,
- }) as unknown as Event
- )
- )
+ if (!form) {
+ throw new Error('Expected redemption form')
+ }
+ fireEvent.submit(form)
}
async function waitForLoadedForm(): Promise {
- await act(async () =>
- waitForCondition(() => {
- const saveButton = getSaveButton()
- return (
- saveButton.textContent?.includes('Save changes') === true &&
- !saveButton.disabled
- )
- }, 'redemption drawer did not finish loading')
- )
+ await waitFor(() => expect(getSaveButton()).toBeEnabled())
}
-afterEach(async () => {
+afterEach(() => {
apiClient.get = originalGet
apiClient.put = originalPut
Reflect.set(console, 'log', originalConsoleLog)
toast.dismiss()
- domWindow.localStorage.clear()
- if (renderedDrawer) {
- await act(async () => renderedDrawer?.root.unmount())
- renderedDrawer.host.remove()
- renderedDrawer = null
- }
- document.body.replaceChildren()
+ localStorage.clear()
+ renderedDrawer = null
})
-afterAll(() => {
- domWindow.close()
-})
+describe('redemption drawer', () => {
+ test('shows the reported CNY quota without floating-point noise', async () => {
+ const original = redemption(1, 13888889)
+ apiClient.get = async () => ({ data: { success: true, data: original } })
-test('redemption drawer shows the reported CNY quota without floating-point noise', async () => {
- const original = redemption(1, 13888889)
- apiClient.get = async () => ({ data: { success: true, data: original } })
+ await renderDrawer(original, {
+ quotaDisplayType: 'CNY',
+ usdExchangeRate: 7.2,
+ })
+ await waitForLoadedForm()
- await renderDrawer(original, {
- quotaDisplayType: 'CNY',
- usdExchangeRate: 7.2,
- })
- await waitForLoadedForm()
-
- assert.equal(getControlByLabel('Quota (CNY)').value, '200')
-})
-
-test('redemption drawer blocks updates and reports an error when loading rejects', async () => {
- const updates: unknown[] = []
- Reflect.set(console, 'log', () => undefined)
- apiClient.get = async () => {
- throw new Error('network failure')
- }
- apiClient.put = async (_url, data) => {
- updates.push(data)
- return { data: { success: true } }
- }
-
- await renderDrawer(redemption(1))
- await act(async () =>
- waitForCondition(
- () =>
- document.body.textContent?.includes('Something went wrong!') === true,
- 'load error toast was not shown'
- )
- )
-
- assert.equal(getSaveButton().disabled, true)
- await submitForm()
- assert.deepEqual(updates, [])
-})
-
-test('redemption drawer blocks updates and uses localized feedback for unsuccessful responses', async () => {
- apiClient.get = async () => ({
- data: { success: false, message: 'raw server message' },
+ expect(getControlByLabel('Quota (CNY)').value).toBe('200')
})
- await renderDrawer(redemption(1))
- await act(async () =>
- waitForCondition(
- () => document.body.textContent?.includes('Failed to load') === true,
- 'unsuccessful-load toast was not shown'
+ test('blocks updates and reports an error when loading rejects', async () => {
+ const updates: unknown[] = []
+ Reflect.set(console, 'log', () => undefined)
+ apiClient.get = async () => {
+ throw new Error('network failure')
+ }
+ apiClient.put = async (_url, data) => {
+ updates.push(data)
+ return { data: { success: true } }
+ }
+
+ await renderDrawer(redemption(1))
+ await waitFor(() =>
+ expect(document.body).toHaveTextContent('Something went wrong!')
)
- )
- assert.equal(getSaveButton().disabled, true)
- assert.equal(document.body.textContent?.includes('raw server message'), false)
-})
+ expect(getSaveButton()).toBeDisabled()
+ submitForm()
+ expect(updates).toEqual([])
+ })
-test('redemption drawer keeps the original quota when another field changes', async () => {
- const original = redemption(1)
- const updates: Array> = []
- apiClient.get = async () => ({ data: { success: true, data: original } })
- apiClient.put = async (_url, data) => {
- assert.ok(data && typeof data === 'object')
- updates.push(data as Record)
- return { data: { success: true, data: original } }
- }
+ test('blocks updates and uses localized feedback for unsuccessful responses', async () => {
+ apiClient.get = async () => ({
+ data: { success: false, message: 'raw server message' },
+ })
- await renderDrawer(original)
- await waitForLoadedForm()
- assert.equal(getControlByLabel('Quota (USD)').value, '1')
-
- await changeInput(getControlByLabel('Name'), 'renamed')
- await submitForm()
- await act(async () =>
- waitForCondition(() => updates.length === 1, 'update was not submitted')
- )
-
- assert.equal(updates[0]?.name, 'renamed')
- assert.equal(updates[0]?.quota, 500001)
-})
-
-test('redemption drawer recalculates quota when the quota field changes', async () => {
- const original = redemption(1)
- const updates: Array> = []
- apiClient.get = async () => ({ data: { success: true, data: original } })
- apiClient.put = async (_url, data) => {
- assert.ok(data && typeof data === 'object')
- updates.push(data as Record)
- return { data: { success: true, data: original } }
- }
-
- await renderDrawer(original)
- await waitForLoadedForm()
- await changeInput(getControlByLabel('Quota (USD)'), '2')
- await submitForm()
- await act(async () =>
- waitForCondition(() => updates.length === 1, 'update was not submitted')
- )
-
- assert.equal(updates[0]?.quota, 1000000)
-})
-
-test('redemption drawer ignores an older response after switching records', async () => {
- const first = redemption(1, 500001)
- const second = redemption(2, 1000001)
- const firstRequest = deferred<{ data: unknown }>()
- const secondRequest = deferred<{ data: unknown }>()
- const requestedUrls: string[] = []
- const updates: Array> = []
- apiClient.get = (url) => {
- requestedUrls.push(url)
- if (url === '/api/redemption/1') return firstRequest.promise
- if (url === '/api/redemption/2') return secondRequest.promise
- throw new Error(`Unexpected GET ${url}`)
- }
- apiClient.put = async (_url, data) => {
- assert.ok(data && typeof data === 'object')
- updates.push(data as Record)
- return { data: { success: true, data: second } }
- }
-
- await renderDrawer(first)
- await rerenderDrawer(second)
- await act(async () =>
- waitForCondition(
- () => requestedUrls.includes('/api/redemption/2'),
- 'second redemption was not requested'
+ await renderDrawer(redemption(1))
+ await waitFor(() =>
+ expect(document.body).toHaveTextContent('Failed to load')
)
- )
- await act(async () =>
+
+ expect(getSaveButton()).toBeDisabled()
+ expect(document.body).not.toHaveTextContent('raw server message')
+ })
+
+ test('keeps the original quota when another field changes', async () => {
+ const original = redemption(1)
+ const updates: Array> = []
+ apiClient.get = async () => ({ data: { success: true, data: original } })
+ apiClient.put = async (_url, data) => {
+ expect(data && typeof data === 'object').toBeTruthy()
+ updates.push(data as Record)
+ return { data: { success: true, data: original } }
+ }
+
+ await renderDrawer(original)
+ await waitForLoadedForm()
+ expect(getControlByLabel('Quota (USD)').value).toBe('1')
+
+ changeInput(getControlByLabel('Name'), 'renamed')
+ submitForm()
+ await waitFor(() => expect(updates).toHaveLength(1))
+
+ expect(updates[0]?.name).toBe('renamed')
+ expect(updates[0]?.quota).toBe(500001)
+ })
+
+ test('recalculates quota when the quota field changes', async () => {
+ const original = redemption(1)
+ const updates: Array> = []
+ apiClient.get = async () => ({ data: { success: true, data: original } })
+ apiClient.put = async (_url, data) => {
+ expect(data && typeof data === 'object').toBeTruthy()
+ updates.push(data as Record)
+ return { data: { success: true, data: original } }
+ }
+
+ await renderDrawer(original)
+ await waitForLoadedForm()
+ changeInput(getControlByLabel('Quota (USD)'), '2')
+ submitForm()
+ await waitFor(() => expect(updates).toHaveLength(1))
+
+ expect(updates[0]?.quota).toBe(1000000)
+ })
+
+ test('ignores an older response after switching records', async () => {
+ const first = redemption(1, 500001)
+ const second = redemption(2, 1000001)
+ const firstRequest = deferred<{ data: unknown }>()
+ const secondRequest = deferred<{ data: unknown }>()
+ const requestedUrls: string[] = []
+ const updates: Array> = []
+ apiClient.get = (url) => {
+ requestedUrls.push(url)
+ if (url === '/api/redemption/1') return firstRequest.promise
+ if (url === '/api/redemption/2') return secondRequest.promise
+ throw new Error(`Unexpected GET ${url}`)
+ }
+ apiClient.put = async (_url, data) => {
+ expect(data && typeof data === 'object').toBeTruthy()
+ updates.push(data as Record)
+ return { data: { success: true, data: second } }
+ }
+
+ await renderDrawer(first)
+ await rerenderDrawer(second)
+ await waitFor(() => expect(requestedUrls).toContain('/api/redemption/2'))
secondRequest.resolve({ data: { success: true, data: second } })
- )
- await waitForLoadedForm()
+ await waitForLoadedForm()
- await act(async () =>
firstRequest.resolve({ data: { success: true, data: first } })
- )
- assert.equal(getControlByLabel('Name').value, 'code-2')
+ expect(getControlByLabel('Name').value).toBe('code-2')
- await changeInput(getControlByLabel('Name'), 'second')
- await submitForm()
- await act(async () =>
- waitForCondition(() => updates.length === 1, 'update was not submitted')
- )
+ changeInput(getControlByLabel('Name'), 'second')
+ submitForm()
+ await waitFor(() => expect(updates).toHaveLength(1))
- assert.equal(updates[0]?.id, 2)
- assert.equal(updates[0]?.quota, 1000001)
+ expect(updates[0]?.id).toBe(2)
+ expect(updates[0]?.quota).toBe(1000001)
+ })
})
diff --git a/web/src/features/system-settings/models/__tests__/group-auto-limit-validation.test.ts b/web/src/features/system-settings/models/__tests__/group-auto-limit-validation.test.ts
index 4b2a5b6e..62e60e9f 100644
--- a/web/src/features/system-settings/models/__tests__/group-auto-limit-validation.test.ts
+++ b/web/src/features/system-settings/models/__tests__/group-auto-limit-validation.test.ts
@@ -16,8 +16,7 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { describe, test } from 'node:test'
+import { describe, expect, test } from 'vitest'
import { positiveIntegerSchema } from '../../utils/numeric-field'
@@ -26,15 +25,15 @@ const schema = positiveIntegerSchema(t('Enter a positive integer'))
describe('per-token Auto group limit validation', () => {
test('accepts any positive integer without a product upper bound', () => {
- assert.equal(schema.safeParse(1000).success, true)
+ expect(schema.safeParse(1000).success).toBe(true)
})
test('rejects zero, negative, and fractional limits', () => {
for (const maxTokenAutoGroups of [0, -1, 1.5]) {
const result = schema.safeParse(maxTokenAutoGroups)
- assert.equal(result.success, false)
+ expect(result.success).toBe(false)
if (result.success) continue
- assert.equal(result.error.issues[0]?.message, 'Enter a positive integer')
+ expect(result.error.issues[0]?.message).toBe('Enter a positive integer')
}
})
})
diff --git a/web/src/features/system-settings/models/__tests__/tool-price-validation.test.tsx b/web/src/features/system-settings/models/__tests__/tool-price-validation.test.tsx
index fc6b8a88..5544d4ff 100644
--- a/web/src/features/system-settings/models/__tests__/tool-price-validation.test.tsx
+++ b/web/src/features/system-settings/models/__tests__/tool-price-validation.test.tsx
@@ -16,128 +16,49 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { after, describe, test } from 'node:test'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { fireEvent, render, screen } from '@testing-library/react'
+import i18next from 'i18next'
+import { beforeAll, describe, expect, test } from 'vitest'
-import { Window } from 'happy-dom'
-
-const domWindow = new Window()
-const domGlobals = [
- 'window',
- 'document',
- 'navigator',
- 'HTMLElement',
- 'HTMLInputElement',
- 'SVGElement',
- 'Node',
- 'Element',
- 'Event',
- 'CustomEvent',
- 'MutationObserver',
- 'requestAnimationFrame',
- 'cancelAnimationFrame',
- 'getComputedStyle',
-] as const
-
-for (const key of domGlobals) {
- Object.defineProperty(globalThis, key, {
- configurable: true,
- value: domWindow[key],
- })
-}
-
-const { act } = await import('react')
-const { createRoot } = await import('react-dom/client')
-const { QueryClient, QueryClientProvider } =
- await import('@tanstack/react-query')
-const { createInstance } = await import('i18next')
-const { I18nextProvider, initReactI18next } = await import('react-i18next')
-const { ToolPriceSettings } = await import('../tool-price-settings')
-
-const i18n = createInstance()
-await i18n.use(initReactI18next).init({
- lng: 'en',
- resources: {
- en: {
- translation: {
- 'Price ($/1K calls)': 'Price ($/1K calls)',
- 'Please enter a valid number': 'Please enter a valid number',
- 'Tool identifier': 'Tool identifier',
- },
- },
- },
-})
-
-const reactTestGlobals = globalThis as typeof globalThis & {
- IS_REACT_ACT_ENVIRONMENT?: boolean
-}
-reactTestGlobals.IS_REACT_ACT_ENVIRONMENT = true
-
-function changeInputValue(input: HTMLInputElement, value: string) {
- const valueSetter = Object.getOwnPropertyDescriptor(
- domWindow.HTMLInputElement.prototype,
- 'value'
- )?.set
- assert.ok(valueSetter)
- valueSetter.call(input, value)
- input.dispatchEvent(
- new domWindow.Event('input', { bubbles: true }) as unknown as Event
- )
-}
+import { ToolPriceSettings } from '../tool-price-settings'
describe('tool price validation', () => {
- after(() => {
- domWindow.close()
+ beforeAll(() => {
+ i18next.addResourceBundle('en', 'translation', {
+ 'Price ($/1K calls)': 'Price ($/1K calls)',
+ 'Please enter a valid number': 'Please enter a valid number',
+ 'Tool identifier': 'Tool identifier',
+ })
})
- test('blocks an empty price without converting it to an explicit zero', async () => {
- const container = document.createElement('div')
- document.body.append(container)
- const root = createRoot(container)
+ test('blocks an empty price without converting it to an explicit zero', () => {
const queryClient = new QueryClient({
defaultOptions: { mutations: { retry: false } },
})
- await act(async () => {
- root.render(
-
-
-
-
-
- )
- })
-
- const priceInput = container.querySelector(
- 'input[aria-label="Price ($/1K calls): web_search"]'
+ render(
+
+
+
)
- assert.ok(priceInput)
- await act(async () => {
- changeInputValue(priceInput, '')
+ const priceInput = screen.getByRole('spinbutton', {
+ name: 'Price ($/1K calls): web_search',
})
+ const saveButton = screen.getByRole('button', { name: 'Save tool prices' })
- assert.equal(priceInput.getAttribute('aria-invalid'), 'true')
- assert.equal(
- priceInput.closest('[data-slot="field"]')?.querySelector('[role="alert"]')
- ?.textContent,
- 'Please enter a valid number'
- )
- const saveButton = [...container.querySelectorAll('button')].find(
- (button) => button.textContent === 'Save tool prices'
- )
- assert.ok(saveButton)
- assert.equal(saveButton.disabled, true)
+ fireEvent.change(priceInput, { target: { value: '' } })
- await act(async () => {
- changeInputValue(priceInput, '0')
- })
+ expect(priceInput).toHaveAttribute('aria-invalid', 'true')
+ expect(screen.getByText('Please enter a valid number')).toBeInTheDocument()
+ expect(saveButton).toBeDisabled()
- assert.equal(priceInput.getAttribute('aria-invalid'), 'false')
- assert.equal(saveButton.disabled, false)
+ fireEvent.change(priceInput, { target: { value: '0' } })
+
+ expect(priceInput).toHaveAttribute('aria-invalid', 'false')
+ expect(saveButton).toBeEnabled()
- await act(async () => root.unmount())
- container.remove()
queryClient.clear()
})
})
diff --git a/web/src/features/usage-logs/components/__tests__/cost-display.test.tsx b/web/src/features/usage-logs/components/__tests__/cost-display.test.tsx
index fc6294f6..457a48da 100644
--- a/web/src/features/usage-logs/components/__tests__/cost-display.test.tsx
+++ b/web/src/features/usage-logs/components/__tests__/cost-display.test.tsx
@@ -16,88 +16,19 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { after, describe, test } from 'node:test'
-
-import { Window } from 'happy-dom'
+import { render, screen } from '@testing-library/react'
+import i18next from 'i18next'
import type React from 'react'
+import { beforeAll, describe, expect, test } from 'vitest'
-const domWindow = new Window()
-const domGlobals = [
- 'window',
- 'document',
- 'navigator',
- 'HTMLElement',
- 'SVGElement',
- 'Node',
- 'Element',
- 'Event',
- 'CustomEvent',
- 'MutationObserver',
- 'requestAnimationFrame',
- 'cancelAnimationFrame',
- 'getComputedStyle',
-] as const
+import { formatLogQuota } from '@/lib/format'
-for (const key of domGlobals) {
- Object.defineProperty(globalThis, key, {
- configurable: true,
- value: domWindow[key],
- })
-}
+import { LogCostDisplay } from '../log-cost-display'
-const { act } = await import('react')
-const { createRoot } = await import('react-dom/client')
-const { createInstance } = await import('i18next')
-const { I18nextProvider, initReactI18next } = await import('react-i18next')
-
-const i18n = createInstance()
-await i18n.use(initReactI18next).init({
- lng: 'en',
- resources: {
- en: {
- translation: {
- Subscription: 'Subscription',
- 'Deducted by subscription': 'Deducted by subscription',
- 'Includes tool-call surcharge': 'Includes tool-call surcharge',
- },
- },
- },
-})
-
-const { LogCostDisplay } = await import('../log-cost-display')
-const { formatLogQuota } = await import('@/lib/format')
-const reactTestGlobals = globalThis as typeof globalThis & {
- IS_REACT_ACT_ENVIRONMENT?: boolean
-}
-reactTestGlobals.IS_REACT_ACT_ENVIRONMENT = true
-
-type RenderedCost = {
- container: HTMLDivElement
- root: ReturnType
-}
-
-async function renderCost(
+function renderCost(
props: React.ComponentProps
-): Promise {
- const container = document.createElement('div')
- document.body.append(container)
- const root = createRoot(container)
-
- await act(async () => {
- root.render(
-
-
-
- )
- })
-
- return { container, root }
-}
-
-async function unmountCost(rendered: RenderedCost) {
- await act(async () => rendered.root.unmount())
- rendered.container.remove()
+): ReturnType {
+ return render()
}
function normalizedText(value: string | null): string {
@@ -105,39 +36,36 @@ function normalizedText(value: string | null): string {
}
describe('log cost display', () => {
- after(() => {
- domWindow.close()
+ beforeAll(() => {
+ i18next.addResourceBundle('en', 'translation', {
+ Subscription: 'Subscription',
+ 'Deducted by subscription': 'Deducted by subscription',
+ 'Includes tool-call surcharge': 'Includes tool-call surcharge',
+ })
})
- test('keeps the regular cost visible and adds an accessible surcharge marker', async () => {
- const rendered = await renderCost({
+ test('keeps the regular cost visible and adds an accessible surcharge marker', () => {
+ const rendered = renderCost({
quota: 12500,
other: {
tool_surcharges: [{ name: 'lookup_customer', count: 1, price: 5 }],
},
})
- assert.equal(
+ expect(
normalizedText(rendered.container.textContent).includes(
normalizedText(formatLogQuota(12500))
- ),
- true
- )
- const marker = rendered.container.querySelector(
- '[data-tool-surcharge-indicator="true"]'
- )
- assert.ok(marker)
- assert.equal(
- marker.getAttribute('aria-label'),
- 'Includes tool-call surcharge'
- )
- assert.equal(marker.getAttribute('tabindex'), '0')
-
- await unmountCost(rendered)
+ )
+ ).toBe(true)
+ const marker = screen.getByRole('img', {
+ name: 'Includes tool-call surcharge',
+ })
+ expect(marker).toHaveAttribute('data-tool-surcharge-indicator', 'true')
+ expect(marker).toHaveAttribute('tabindex', '0')
})
- test('preserves the subscription badge and adds the same legacy surcharge marker', async () => {
- const rendered = await renderCost({
+ test('preserves the subscription badge and adds the same legacy surcharge marker', () => {
+ renderCost({
quota: 5000,
other: {
billing_source: 'subscription',
@@ -147,11 +75,9 @@ describe('log cost display', () => {
},
})
- assert.equal(rendered.container.textContent?.includes('Subscription'), true)
- assert.ok(
- rendered.container.querySelector('[data-tool-surcharge-indicator="true"]')
- )
-
- await unmountCost(rendered)
+ expect(screen.getByText('Subscription')).toBeInTheDocument()
+ expect(
+ screen.getByRole('img', { name: 'Includes tool-call surcharge' })
+ ).toHaveAttribute('data-tool-surcharge-indicator', 'true')
})
})
diff --git a/web/src/features/usage-logs/lib/__tests__/tool-surcharge.test.ts b/web/src/features/usage-logs/lib/__tests__/tool-surcharge.test.ts
index fb3ea348..f5b80949 100644
--- a/web/src/features/usage-logs/lib/__tests__/tool-surcharge.test.ts
+++ b/web/src/features/usage-logs/lib/__tests__/tool-surcharge.test.ts
@@ -16,20 +16,18 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { describe, test } from 'node:test'
+import { describe, expect, test } from 'vitest'
import type { LogOtherData } from '../../types'
import { hasToolSurcharge } from '../format'
describe('tool surcharge detection', () => {
test('shows the marker for a charged structured tool surcharge', () => {
- assert.equal(
+ expect(
hasToolSurcharge({
tool_surcharges: [{ name: 'lookup_customer', count: 2, price: 5 }],
- }),
- true
- )
+ })
+ ).toBe(true)
})
const legacyCases: Array<{
@@ -63,7 +61,7 @@ describe('tool surcharge detection', () => {
for (const scenario of legacyCases) {
test(`keeps the marker visible for legacy ${scenario.name} charges`, () => {
- assert.equal(hasToolSurcharge(scenario.other), true)
+ expect(hasToolSurcharge(scenario.other)).toBe(true)
})
}
@@ -93,7 +91,7 @@ describe('tool surcharge detection', () => {
]
for (const other of invalidCases) {
- assert.equal(hasToolSurcharge(other), false)
+ expect(hasToolSurcharge(other)).toBe(false)
}
})
})
diff --git a/web/src/features/wallet/hooks/use-payment.test.ts b/web/src/features/wallet/hooks/use-payment.test.ts
index ebd90a14..210e873f 100644
--- a/web/src/features/wallet/hooks/use-payment.test.ts
+++ b/web/src/features/wallet/hooks/use-payment.test.ts
@@ -16,8 +16,7 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { describe, test } from 'node:test'
+import { describe, expect, test } from 'vitest'
import { PAYMENT_TYPES } from '../constants'
import { requestPaymentAmount } from './use-payment'
@@ -44,7 +43,7 @@ describe('payment amount routing', () => {
},
})
- assert.equal(amount, 18.75)
- assert.deepEqual(calls, ['waffo:120'])
+ expect(amount).toBe(18.75)
+ expect(calls).toEqual(['waffo:120'])
})
})
diff --git a/web/src/features/wallet/lib/payment.test.ts b/web/src/features/wallet/lib/payment.test.ts
index 10e8eaba..99f22ce3 100644
--- a/web/src/features/wallet/lib/payment.test.ts
+++ b/web/src/features/wallet/lib/payment.test.ts
@@ -16,8 +16,7 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { describe, test } from 'node:test'
+import { describe, expect, test } from 'vitest'
import { PAYMENT_TYPES } from '../constants'
import {
@@ -29,11 +28,11 @@ import {
describe('payment type classification', () => {
test('keeps Waffo and Waffo Pancake on their dedicated flows', () => {
- assert.equal(isWaffoPayment(PAYMENT_TYPES.WAFFO), true)
- assert.equal(isWaffoPayment(PAYMENT_TYPES.WAFFO_PANCAKE), false)
- assert.equal(isWaffoPancakePayment(PAYMENT_TYPES.WAFFO_PANCAKE), true)
- assert.equal(isWaffoPancakePayment(PAYMENT_TYPES.WAFFO), false)
- assert.equal(isStripePayment(PAYMENT_TYPES.STRIPE), true)
+ expect(isWaffoPayment(PAYMENT_TYPES.WAFFO)).toBe(true)
+ expect(isWaffoPayment(PAYMENT_TYPES.WAFFO_PANCAKE)).toBe(false)
+ expect(isWaffoPancakePayment(PAYMENT_TYPES.WAFFO_PANCAKE)).toBe(true)
+ expect(isWaffoPancakePayment(PAYMENT_TYPES.WAFFO)).toBe(false)
+ expect(isStripePayment(PAYMENT_TYPES.STRIPE)).toBe(true)
})
})
@@ -60,8 +59,8 @@ describe('payment dispatch', () => {
}
)
- assert.equal(success, true)
- assert.deepEqual(calls, ['waffo:120:3'])
+ expect(success).toBe(true)
+ expect(calls).toEqual(['waffo:120:3'])
})
test('does not create a Waffo order without a selected method index', async () => {
@@ -80,7 +79,7 @@ describe('payment dispatch', () => {
}
)
- assert.equal(success, false)
- assert.equal(called, false)
+ expect(success).toBe(false)
+ expect(called).toBe(false)
})
})
diff --git a/web/src/lib/auth-session.test.ts b/web/src/lib/auth-session.test.ts
index 4a97d3c2..17055ee4 100644
--- a/web/src/lib/auth-session.test.ts
+++ b/web/src/lib/auth-session.test.ts
@@ -16,10 +16,8 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { afterEach, describe, test } from 'node:test'
-
import { QueryClient } from '@tanstack/react-query'
+import { afterEach, describe, expect, test } from 'vitest'
import { useAuthStore, type AuthBundle } from '../stores/auth-store'
import {
@@ -59,10 +57,10 @@ afterEach(() => {
describe('authentication session coordination', () => {
test('bootstrap distinguishes a completed anonymous check from an active session', async () => {
useAuthStore.getState().auth.reset('complete')
- assert.deepEqual(await bootstrapAuthentication(), { kind: 'anonymous' })
+ expect(await bootstrapAuthentication()).toEqual({ kind: 'anonymous' })
useAuthStore.getState().auth.setBundle(bundle)
- assert.deepEqual(await bootstrapAuthentication(), {
+ expect(await bootstrapAuthentication()).toEqual({
kind: 'authenticated',
bundle,
})
@@ -97,10 +95,10 @@ describe('authentication session coordination', () => {
const outcome = await createRefreshRunner(runtime)()
- assert.equal(outcome.kind, 'authenticated')
- assert.deepEqual(requestedSIDs, [bundle.session.sid, undefined])
- assert.deepEqual(clears, [[false, 'idle']])
- assert.deepEqual(accepted, [bundle])
+ expect(outcome.kind).toBe('authenticated')
+ expect(requestedSIDs).toEqual([bundle.session.sid, undefined])
+ expect(clears).toEqual([[false, 'idle']])
+ expect(accepted).toEqual([bundle])
})
test('a rejected refresh confirms anonymous state and synchronizes sign-out', async () => {
@@ -117,10 +115,10 @@ describe('authentication session coordination', () => {
wait: async () => undefined,
}
- assert.deepEqual(await createRefreshRunner(runtime)(), {
+ expect(await createRefreshRunner(runtime)()).toEqual({
kind: 'anonymous',
})
- assert.deepEqual(clears, [[true, undefined]])
+ expect(clears).toEqual([[true, undefined]])
})
test('a temporary refresh failure remains retryable without clearing the session', async () => {
@@ -142,9 +140,9 @@ describe('authentication session coordination', () => {
const outcome = await createRefreshRunner(runtime)()
- assert.equal(outcome.kind, 'transient_error')
- assert.equal(clearCount, 0)
- assert.equal(transientCount, 1)
+ expect(outcome.kind).toBe('transient_error')
+ expect(clearCount).toBe(0)
+ expect(transientCount).toBe(1)
})
test('a rate limited refresh remains retryable without clearing the session', async () => {
@@ -166,9 +164,9 @@ describe('authentication session coordination', () => {
const outcome = await createRefreshRunner(runtime)()
- assert.equal(outcome.kind, 'transient_error')
- assert.equal(clearCount, 0)
- assert.equal(transientCount, 1)
+ expect(outcome.kind).toBe('transient_error')
+ expect(clearCount).toBe(0)
+ expect(transientCount).toBe(1)
})
test('an exhausted refresh race clears the unusable local session', async () => {
@@ -191,12 +189,12 @@ describe('authentication session coordination', () => {
},
}
- assert.deepEqual(await createRefreshRunner(runtime)(), {
+ expect(await createRefreshRunner(runtime)()).toEqual({
kind: 'out_of_sync',
code: 'AUTH_REFRESH_RACE',
})
- assert.deepEqual(requestedDelays, [80, 200, 500])
- assert.deepEqual(clears, [[false, undefined]])
+ expect(requestedDelays).toEqual([80, 200, 500])
+ expect(clears).toEqual([[false, undefined]])
})
test('an unexpected successful response is treated as out of sync', async () => {
@@ -213,11 +211,11 @@ describe('authentication session coordination', () => {
wait: async () => undefined,
}
- assert.deepEqual(await createRefreshRunner(runtime)(), {
+ expect(await createRefreshRunner(runtime)()).toEqual({
kind: 'out_of_sync',
code: 'AUTH_INVALID_REFRESH_RESPONSE',
})
- assert.equal(cleared, true)
+ expect(cleared).toBe(true)
})
test('a refresh response cannot restore credentials after a newer auth operation', async () => {
@@ -241,8 +239,8 @@ describe('authentication session coordination', () => {
const outcome = await createRefreshRunner(runtime)()
- assert.equal(outcome.kind, 'transient_error')
- assert.equal(accepted, false)
+ expect(outcome.kind).toBe('transient_error')
+ expect(accepted).toBe(false)
})
test('explicit rotations update only the current session', () => {
@@ -254,41 +252,35 @@ describe('authentication session coordination', () => {
session: { ...bundle.session, last_active_at: 200 },
})
- assert.equal(useAuthStore.getState().auth.accessToken, 'rotated-token')
- assert.strictEqual(useAuthStore.getState().auth.user, bundle.user)
+ expect(useAuthStore.getState().auth.accessToken).toBe('rotated-token')
+ expect(useAuthStore.getState().auth.user).toBe(bundle.user)
- assert.throws(
- () =>
- applyAuthRotation({
- access_token: 'non-bearer-token',
- token_type: 'Custom',
- access_expires_at: bundle.access_expires_at + 120,
- session: bundle.session,
- }),
- /Invalid authentication rotation response/
- )
- assert.throws(
- () =>
- applyAuthRotation({
- access_token: 'non-current-token',
- token_type: 'Bearer',
- access_expires_at: bundle.access_expires_at + 120,
- session: { ...bundle.session, current: false },
- }),
- /Invalid authentication rotation response/
- )
+ expect(() =>
+ applyAuthRotation({
+ access_token: 'non-bearer-token',
+ token_type: 'Custom',
+ access_expires_at: bundle.access_expires_at + 120,
+ session: bundle.session,
+ })
+ ).toThrow(/Invalid authentication rotation response/)
+ expect(() =>
+ applyAuthRotation({
+ access_token: 'non-current-token',
+ token_type: 'Bearer',
+ access_expires_at: bundle.access_expires_at + 120,
+ session: { ...bundle.session, current: false },
+ })
+ ).toThrow(/Invalid authentication rotation response/)
- assert.throws(
- () =>
- applyAuthRotation({
- access_token: 'wrong-session-token',
- token_type: 'Bearer',
- access_expires_at: bundle.access_expires_at + 120,
- session: { ...bundle.session, sid: 'session-b' },
- }),
- /session mismatch/
- )
- assert.equal(useAuthStore.getState().auth.accessToken, 'rotated-token')
+ expect(() =>
+ applyAuthRotation({
+ access_token: 'wrong-session-token',
+ token_type: 'Bearer',
+ access_expires_at: bundle.access_expires_at + 120,
+ session: { ...bundle.session, sid: 'session-b' },
+ })
+ ).toThrow(/session mismatch/)
+ expect(useAuthStore.getState().auth.accessToken).toBe('rotated-token')
})
test('sign-out clears user-scoped query, mutation, and authentication state', () => {
@@ -305,13 +297,13 @@ describe('authentication session coordination', () => {
clearAuthenticatedClientState(queryClient, false)
- assert.equal(queryClient.getQueryCache().getAll().length, 0)
- assert.equal(queryClient.getMutationCache().getAll().length, 0)
- assert.equal(useAuthStore.getState().auth.user, null)
- assert.equal(useAuthStore.getState().auth.accessToken, null)
- assert.equal(useAuthStore.getState().auth.session, null)
- assert.equal(useAuthStore.getState().auth.pending2FAFlowToken, null)
- assert.equal(useAuthStore.getState().auth.bootstrapState, 'complete')
+ expect(queryClient.getQueryCache().getAll().length).toBe(0)
+ expect(queryClient.getMutationCache().getAll().length).toBe(0)
+ expect(useAuthStore.getState().auth.user).toBe(null)
+ expect(useAuthStore.getState().auth.accessToken).toBe(null)
+ expect(useAuthStore.getState().auth.session).toBe(null)
+ expect(useAuthStore.getState().auth.pending2FAFlowToken).toBe(null)
+ expect(useAuthStore.getState().auth.bootstrapState).toBe('complete')
const nextBundle: AuthBundle = {
...bundle,
@@ -320,8 +312,7 @@ describe('authentication session coordination', () => {
session: { ...bundle.session, sid: 'session-b' },
}
useAuthStore.getState().auth.setBundle(nextBundle)
- assert.equal(
- queryClient.getQueryData(['account', bundle.user.id]),
+ expect(queryClient.getQueryData(['account', bundle.user.id])).toBe(
undefined
)
})
diff --git a/web/src/lib/legacy-route.test.ts b/web/src/lib/legacy-route.test.ts
index f14a94b1..737a4850 100644
--- a/web/src/lib/legacy-route.test.ts
+++ b/web/src/lib/legacy-route.test.ts
@@ -16,8 +16,7 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { describe, test } from 'node:test'
+import { describe, expect, test } from 'vitest'
import { resolveLegacyRoute } from './legacy-route'
@@ -43,17 +42,15 @@ describe('legacy frontend route migration', () => {
}
for (const [source, target] of Object.entries(routes)) {
- assert.equal(resolveLegacyRoute(source), target)
+ expect(resolveLegacyRoute(source)).toBe(target)
}
})
test('preserves search and hash while applying route-specific behavior', () => {
- assert.equal(
- resolveLegacyRoute('/login?redirect=%2Fkeys#continue'),
+ expect(resolveLegacyRoute('/login?redirect=%2Fkeys#continue')).toBe(
'/sign-in?redirect=%2Fkeys#continue'
)
- assert.equal(
- resolveLegacyRoute('/console/topup?source=email#orders'),
+ expect(resolveLegacyRoute('/console/topup?source=email#orders')).toBe(
'/wallet?source=email#orders'
)
})
@@ -75,23 +72,20 @@ describe('legacy frontend route migration', () => {
}
for (const [tab, target] of Object.entries(settingsTabs)) {
- assert.equal(
- resolveLegacyRoute(`/console/setting?tab=${tab}&from=bookmark#form`),
- `${target}?tab=${tab}&from=bookmark#form`
- )
+ expect(
+ resolveLegacyRoute(`/console/setting?tab=${tab}&from=bookmark#form`)
+ ).toBe(`${target}?tab=${tab}&from=bookmark#form`)
}
- assert.equal(
- resolveLegacyRoute('/console/setting?tab=unknown'),
+ expect(resolveLegacyRoute('/console/setting?tab=unknown')).toBe(
'/system-settings?tab=unknown'
)
})
test('safely redirects unknown console locations without touching new routes', () => {
- assert.equal(
- resolveLegacyRoute('/console/removed?page=2#old'),
+ expect(resolveLegacyRoute('/console/removed?page=2#old')).toBe(
'/dashboard?page=2#old'
)
- assert.equal(resolveLegacyRoute('/dashboard'), null)
- assert.equal(resolveLegacyRoute('/api/status'), null)
+ expect(resolveLegacyRoute('/dashboard')).toBe(null)
+ expect(resolveLegacyRoute('/api/status')).toBe(null)
})
})
diff --git a/web/src/lib/server-error-message.test.ts b/web/src/lib/server-error-message.test.ts
index 9e59f423..f3b1eb84 100644
--- a/web/src/lib/server-error-message.test.ts
+++ b/web/src/lib/server-error-message.test.ts
@@ -16,8 +16,7 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import assert from 'node:assert/strict'
-import { describe, test } from 'node:test'
+import { describe, expect, test } from 'vitest'
import { getServerErrorMessageKey } from './server-error-message'
@@ -25,8 +24,8 @@ describe('server error message mapping', () => {
test('maps the active-session limit to recovery instructions', () => {
const message = getServerErrorMessageKey({ code: 'AUTH_SESSION_LIMIT' })
- assert.match(message ?? '', /Sign out other sessions/)
- assert.match(message ?? '', /reset your password/)
+ expect(message ?? '').toMatch(/Sign out other sessions/)
+ expect(message ?? '').toMatch(/reset your password/)
})
test('maps an Axios-shaped issuance limit to rolling-window guidance', () => {
@@ -34,8 +33,8 @@ describe('server error message mapping', () => {
response: { data: { code: 'AUTH_SESSION_ISSUANCE_LIMIT' } },
})
- assert.match(message ?? '', /rolling window/)
- assert.equal(getServerErrorMessageKey({ code: 'UNKNOWN_CODE' }), null)
+ expect(message ?? '').toMatch(/rolling window/)
+ expect(getServerErrorMessageKey({ code: 'UNKNOWN_CODE' })).toBe(null)
})
test('maps stable Telegram bind errors without exposing server text', () => {
@@ -55,16 +54,15 @@ describe('server error message mapping', () => {
}
for (const [code, message] of Object.entries(expected)) {
- assert.equal(getServerErrorMessageKey({ code }), message)
+ expect(getServerErrorMessageKey({ code })).toBe(message)
}
- assert.equal(
+ expect(
getServerErrorMessageKey({
response: {
data: { code: 'TELEGRAM_BIND_INTERNAL_ERROR', message: 'raw detail' },
},
- }),
- expected.TELEGRAM_BIND_INTERNAL_ERROR
- )
+ })
+ ).toBe(expected.TELEGRAM_BIND_INTERNAL_ERROR)
})
})
diff --git a/web/src/test-setup.ts b/web/src/test-setup.ts
new file mode 100644
index 00000000..cf898360
--- /dev/null
+++ b/web/src/test-setup.ts
@@ -0,0 +1,73 @@
+/*
+Copyright (C) 2023-2026 QuantumNous
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact support@quantumnous.com
+*/
+import '@testing-library/jest-dom/vitest'
+import { cleanup } from '@testing-library/react'
+import i18next from 'i18next'
+import { initReactI18next } from 'react-i18next'
+import { afterEach, beforeAll } from 'vitest'
+
+beforeAll(async () => {
+ await i18next.use(initReactI18next).init({
+ lng: 'en',
+ fallbackLng: 'en',
+ resources: {
+ en: {
+ translation: {},
+ },
+ },
+ })
+})
+
+afterEach(() => {
+ cleanup()
+})
+
+Object.defineProperty(window, 'matchMedia', {
+ configurable: true,
+ value: (query: string): MediaQueryList => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: () => undefined,
+ removeListener: () => undefined,
+ addEventListener: () => undefined,
+ removeEventListener: () => undefined,
+ dispatchEvent: () => false,
+ }),
+})
+
+window.requestAnimationFrame = (callback: FrameRequestCallback) =>
+ window.setTimeout(() => callback(performance.now()), 0)
+window.cancelAnimationFrame = (handle: number) => window.clearTimeout(handle)
+
+class ResizeObserverMock {
+ observe(): void {}
+ unobserve(): void {}
+ disconnect(): void {}
+}
+
+Object.defineProperty(globalThis, 'ResizeObserver', {
+ configurable: true,
+ value: ResizeObserverMock,
+})
+
+Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
+ configurable: true,
+ value: () => undefined,
+})
diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json
index e5b90396..f576ef0d 100644
--- a/web/tsconfig.node.json
+++ b/web/tsconfig.node.json
@@ -20,5 +20,5 @@
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
- "include": ["rsbuild.config.ts"]
+ "include": ["rsbuild.config.ts", "vitest.config.ts"]
}
diff --git a/web/vitest.config.ts b/web/vitest.config.ts
new file mode 100644
index 00000000..144e0f44
--- /dev/null
+++ b/web/vitest.config.ts
@@ -0,0 +1,39 @@
+/*
+Copyright (C) 2023-2026 QuantumNous
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact support@quantumnous.com
+*/
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+import { defineConfig } from 'vitest/config'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+
+export default defineConfig({
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ },
+ },
+ test: {
+ environment: 'jsdom',
+ setupFiles: ['./src/test-setup.ts'],
+ clearMocks: true,
+ restoreMocks: true,
+ include: ['src/**/*.{test,spec}.{ts,tsx}'],
+ },
+})