test(web): standardize frontend tests on Vitest (#6569)

* test(web): standardize frontend tests on Vitest

- configure Vitest, jsdom, and React Testing Library with shared test scripts.
- migrate existing node:test suites to the Vitest runner.
- rewrite JsonCodeEditor component tests with RTL and remove the direct happy-dom dependency.

* fix(ci): run frontend tests with Vitest

- invoke the configured Vitest script so browser test setup loads in CI.
- migrate remaining node:test suites to Vitest lifecycle APIs.

* test(web): use shared jsdom environment for component tests

- migrate usage cost and tool price tests to React Testing Library.
- remove duplicate happy-dom globals and rely on the configured Vitest setup.

* test(web): verify behavior with shared vitest setup

- replace Node test assertions with Vitest expect across frontend suites.
- migrate Keys component tests to React Testing Library interactions.
- centralize jsdom browser mocks for consistent component execution.

* fix(web): unblock frozen installs and Vitest CI

- sync dompurify 3.4.13 metadata into the Bun lockfile.
- replace the bun:test and happy-dom redemption harness with Vitest and RTL.
- preserve quota conversion, error feedback, and stale-response coverage in jsdom.
This commit is contained in:
QuentinHsu
2026-08-15 14:18:10 +08:00
committed by GitHub
parent 116255f076
commit e2c7aa7b10
37 changed files with 1569 additions and 2173 deletions
+1 -1
View File
@@ -85,4 +85,4 @@ jobs:
run: bun run typecheck run: bun run typecheck
- name: Test - name: Test
run: bun test run: bun run test
+213 -13
View File
@@ -30,7 +30,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"dayjs": "^1.11.21", "dayjs": "^1.11.21",
"dompurify": "3.4.11", "dompurify": "3.4.13",
"i18next": "^26.3.4", "i18next": "^26.3.4",
"i18next-browser-languagedetector": "^8.2.1", "i18next-browser-languagedetector": "^8.2.1",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
@@ -71,23 +71,27 @@
"@tanstack/react-query-devtools": "^5.101.2", "@tanstack/react-query-devtools": "^5.101.2",
"@tanstack/react-router-devtools": "^1.167.0", "@tanstack/react-router-devtools": "^1.167.0",
"@tanstack/router-plugin": "^1.168.19", "@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/node": "^26.1.0",
"@types/react": "^19.2.17", "@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@typescript/native-preview": "^7.0.0-dev.20260702.3", "@typescript/native-preview": "^7.0.0-dev.20260702.3",
"@xyflow/react": "^12.11.1", "@xyflow/react": "^12.11.1",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"happy-dom": "^20.11.1", "jsdom": "^29.1.1",
"knip": "^6.24.0", "knip": "^6.24.0",
"oxfmt": "^0.57.0", "oxfmt": "^0.57.0",
"oxlint": "^1.72.0", "oxlint": "^1.72.0",
"shadcn": "^4.12.0", "shadcn": "^4.12.0",
"vitest": "^4.1.10",
}, },
}, },
}, },
"overrides": { "overrides": {
"brace-expansion": "2.1.1", "brace-expansion": "2.1.1",
"dompurify": "3.4.11", "dompurify": "3.4.13",
"fast-uri": "3.1.2", "fast-uri": "3.1.2",
"hono": "4.12.22", "hono": "4.12.22",
"ip-address": "10.2.0", "ip-address": "10.2.0",
@@ -99,6 +103,8 @@
"uuid": "14.0.0", "uuid": "14.0.0",
}, },
"packages": { "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/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=="], "@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=="], "@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/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=="], "@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=="], "@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=="], "@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=="], "@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=="], "@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=="], "@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=="], "@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=="], "@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/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=="], "@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=="], "@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/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=="], "@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=="], "@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/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=="], "@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=="], "@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": ["@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=="], "@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/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": ["@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=="], "@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=="], "@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=="], "@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=="], "@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-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=="], "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-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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "date-fns": ["date-fns@4.4.0", "", {}, "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w=="],
"dayjs": ["dayjs@1.11.21", "", {}, "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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-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-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=="], "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=="], "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": ["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=="], "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=="], "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=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="], "fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="],
@@ -1487,6 +1603,8 @@
"hono": ["hono@4.12.22", "", {}, "sha512-7fvVPbB92zNRsQke+uiRGwtTuef0tB2Dg4hWxYfFNvkQhIltWoyi0ONReM5LWA+jJWS3nfT5lTq+qbsIpX0IQw=="], "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-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=="], "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=="], "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=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], "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-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-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"is-regexp": ["is-regexp@3.1.0", "", {}, "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
@@ -1841,6 +1969,8 @@
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], "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=="], "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
@@ -1873,6 +2003,8 @@
"object-treeify": ["object-treeify@1.1.33", "", {}, "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A=="], "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-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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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": ["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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"screenfull": ["screenfull@5.2.0", "", {}, "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA=="], "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=="], "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=="], "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"simple-statistics": ["simple-statistics@7.9.3", "", {}, "sha512-WXpxUfo7BJCRpyl4besiuMV7wNj9xiPIq7IKmUQO4upIaF8pK2AXwhjttHN5L8KXZrLkGMCHGHq4p+pJXiIahQ=="], "simple-statistics": ["simple-statistics@7.9.3", "", {}, "sha512-WXpxUfo7BJCRpyl4besiuMV7wNj9xiPIq7IKmUQO4upIaF8pK2AXwhjttHN5L8KXZrLkGMCHGHq4p+pJXiIahQ=="],
@@ -2221,8 +2365,12 @@
"sse.js": ["sse.js@2.8.0", "", {}, "sha512-35RyyFYpzzHZgMw9D5GxwADbL6gnntSwW/rKXcuIy1KkYCPjW6oia0moNdNRhs34oVHU1Sjgovj3l7uIEZjrKA=="], "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=="], "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=="], "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=="], "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-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=="], "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="],
"style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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-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=="], "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=="], "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=="], "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], "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=="], "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=="], "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-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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "yace": ["yace@1.1.0", "", {}, "sha512-jB29trAxPBvTIR/lXsgh97q22n/Rq5ZHbDT4DT6WUvXVlJLR7jdwiBKD+0zi3XZAD5N+YKa3GrdIrelqIXj2oQ=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
@@ -2421,6 +2601,8 @@
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], "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/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=="], "@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=="], "@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/vdataset/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
"@visactor/vlayouts/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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "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=="], "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=="], "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=="], "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=="], "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-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=="], "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=="], "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=="], "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=="], "mermaid/katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
"micromark-extension-math/katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], "micromark-extension-math/katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
+8 -2
View File
@@ -11,6 +11,8 @@
"lint": "oxlint -c .oxlintrc.json .", "lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint -c .oxlintrc.json . --fix", "lint:fix": "oxlint -c .oxlintrc.json . --fix",
"preview": "rsbuild preview", "preview": "rsbuild preview",
"test": "vitest run",
"test:watch": "vitest",
"format:check": "node scripts/format-with-protected-headers.mjs --check", "format:check": "node scripts/format-with-protected-headers.mjs --check",
"format": "node scripts/format-with-protected-headers.mjs --write", "format": "node scripts/format-with-protected-headers.mjs --write",
"copyright:check": "node scripts/add-copyright.mjs --check", "copyright:check": "node scripts/add-copyright.mjs --check",
@@ -85,17 +87,21 @@
"@tanstack/react-query-devtools": "^5.101.2", "@tanstack/react-query-devtools": "^5.101.2",
"@tanstack/react-router-devtools": "^1.167.0", "@tanstack/react-router-devtools": "^1.167.0",
"@tanstack/router-plugin": "^1.168.19", "@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/node": "^26.1.0",
"@types/react": "^19.2.17", "@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@typescript/native-preview": "^7.0.0-dev.20260702.3", "@typescript/native-preview": "^7.0.0-dev.20260702.3",
"@xyflow/react": "^12.11.1", "@xyflow/react": "^12.11.1",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"happy-dom": "^20.11.1", "jsdom": "^29.1.1",
"knip": "^6.24.0", "knip": "^6.24.0",
"oxfmt": "^0.57.0", "oxfmt": "^0.57.0",
"oxlint": "^1.72.0", "oxlint": "^1.72.0",
"shadcn": "^4.12.0" "shadcn": "^4.12.0",
"vitest": "^4.1.10"
}, },
"overrides": { "overrides": {
"brace-expansion": "2.1.1", "brace-expansion": "2.1.1",
@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { import {
applyJsonSmartEnter, applyJsonSmartEnter,
@@ -29,42 +28,42 @@ import {
describe('json code editor utils', () => { describe('json code editor utils', () => {
test('treats empty drafts as valid editable JSON drafts', () => { test('treats empty drafts as valid editable JSON drafts', () => {
assert.deepEqual(getJsonValidationState(' \n'), { expect(getJsonValidationState(' \n')).toEqual({
isValid: true, isValid: true,
messageKey: 'JSON', messageKey: 'JSON',
}) })
}) })
test('reports invalid JSON without throwing away the draft', () => { test('reports invalid JSON without throwing away the draft', () => {
assert.deepEqual(getJsonValidationState('{"model": }'), { expect(getJsonValidationState('{"model": }')).toEqual({
isValid: false, isValid: false,
messageKey: 'Invalid JSON', messageKey: 'Invalid JSON',
}) })
}) })
test('formats valid JSON with stable two-space indentation', () => { test('formats valid JSON with stable two-space indentation', () => {
assert.deepEqual(formatJsonDraft('{"model":{"ratio":2}}'), { expect(formatJsonDraft('{"model":{"ratio":2}}')).toEqual({
didFormat: true, didFormat: true,
value: '{\n "model": {\n "ratio": 2\n }\n}', value: '{\n "model": {\n "ratio": 2\n }\n}',
}) })
}) })
test('keeps invalid JSON drafts unchanged when formatting is requested', () => { test('keeps invalid JSON drafts unchanged when formatting is requested', () => {
assert.deepEqual(formatJsonDraft('{"model": }'), { expect(formatJsonDraft('{"model": }')).toEqual({
didFormat: false, didFormat: false,
value: '{"model": }', value: '{"model": }',
}) })
}) })
test('derives the one-based cursor line and column from text offsets', () => { 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, line: 2,
column: 4, column: 4,
}) })
}) })
test('expands paired JSON brackets with a nested indentation line', () => { test('expands paired JSON brackets with a nested indentation line', () => {
assert.deepEqual(applyJsonSmartEnter('{}', 1, 1), { expect(applyJsonSmartEnter('{}', 1, 1)).toEqual({
value: '{\n \n}', value: '{\n \n}',
selectionStart: 4, selectionStart: 4,
selectionEnd: 4, selectionEnd: 4,
@@ -90,11 +89,11 @@ describe('json code editor utils', () => {
source.scrollTop = 80 source.scrollTop = 80
synchronizer.sync() synchronizer.sync()
assert.equal(queuedFrames.length, 1) expect(queuedFrames.length).toBe(1)
queuedFrames[0]() queuedFrames[0]()
assert.equal(contentLayer.style.transform, 'translate3d(-24px, -80px, 0)') expect(contentLayer.style.transform).toBe('translate3d(-24px, -80px, 0)')
assert.equal(lineNumberLayer.style.transform, 'translate3d(0, -80px, 0)') expect(lineNumberLayer.style.transform).toBe('translate3d(0, -80px, 0)')
}) })
}) })
@@ -16,165 +16,98 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { fireEvent, render, screen } from '@testing-library/react'
import { after, describe, test } from 'node:test' import userEvent from '@testing-library/user-event'
import { describe, expect, test, vi } from 'vitest'
import { Window } from 'happy-dom' import { JsonCodeEditor } from '../../json-code-editor'
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<typeof createRoot>
}
async function renderEditor(
props: React.ComponentProps<typeof JsonCodeEditor>
): Promise<RenderedEditor> {
const container = document.createElement('div')
document.body.append(container)
const root = createRoot(container)
await act(async () => {
root.render(<JsonCodeEditor {...props} />)
})
return { container, root }
}
async function unmountEditor(rendered: RenderedEditor) {
await act(async () => rendered.root.unmount())
rendered.container.remove()
}
describe('JsonCodeEditor component', () => { describe('JsonCodeEditor component', () => {
after(() => { test('forwards form attributes and the textarea ref', () => {
domWindow.close() const textareaRef = vi.fn()
const rendered = render(
<JsonCodeEditor
value='{"model":"gpt"}'
onChange={() => 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 () => { test('calls onBlur when focus leaves the editor', () => {
const blurCalls: number[] = [] const onBlur = vi.fn()
const refValues: Array<HTMLTextAreaElement | null> = [] render(
const rendered = await renderEditor({ <JsonCodeEditor
value: '{"model":"gpt"}', value='{}'
onChange: () => undefined, onChange={() => undefined}
id: 'json-input', onBlur={onBlur}
name: 'model_config', ariaLabel='Model configuration'
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')
assert.ok(textarea) fireEvent.blur(screen.getByRole('textbox', { name: 'Model configuration' }))
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')
await act(async () => textarea.dispatchEvent(new Event('blur'))) expect(onBlur).toHaveBeenCalledOnce()
assert.deepEqual(blurCalls, [1])
assert.equal(refValues[0], textarea)
await unmountEditor(rendered)
assert.equal(refValues.at(-1), null)
}) })
test('emits user edits and synchronizes a controlled value', async () => { test('emits user edits and synchronizes a controlled value', () => {
const changes: string[] = [] const onChange = vi.fn()
const rendered = await renderEditor({ const rendered = render(
value: '{"count":1}', <JsonCodeEditor
onChange: (value) => changes.push(value), value='{"count":1}'
onChange={onChange}
ariaLabel='Model configuration'
/>
)
const textarea = screen.getByRole('textbox', {
name: 'Model configuration',
}) })
const textarea = rendered.container.querySelector('textarea')
assert.ok(textarea) fireEvent.input(textarea, { target: { value: '{"count":2}' } })
await act(async () => { expect(onChange).toHaveBeenCalledWith('{"count":2}')
textarea.value = '{"count":2}'
textarea.dispatchEvent(new Event('input', { bubbles: true }))
})
assert.deepEqual(changes, ['{"count":2}'])
await act(async () => { rendered.rerender(
rendered.root.render( <JsonCodeEditor
<JsonCodeEditor value='{"count":3}'
value='{"count":3}' onChange={onChange}
onChange={(value) => changes.push(value)} ariaLabel='Model configuration'
/> />
) )
}) expect(textarea).toHaveValue('{"count":3}')
assert.equal(textarea.value, '{"count":3}')
await unmountEditor(rendered)
}) })
test('formats valid JSON through the public toolbar action', async () => { test('formats valid JSON through the public toolbar action', async () => {
const changes: string[] = [] const user = userEvent.setup()
const rendered = await renderEditor({ const onChange = vi.fn()
value: '{"model":{"ratio":2}}', render(<JsonCodeEditor value='{"model":{"ratio":2}}' onChange={onChange} />)
onChange: (value) => changes.push(value),
})
const formatButton = [
...rendered.container.querySelectorAll('button'),
].find((button) => button.textContent?.includes('Format JSON'))
assert.ok(formatButton) await user.click(screen.getByRole('button', { name: 'Format JSON' }))
await act(async () => formatButton.click())
assert.deepEqual(changes, ['{\n "model": {\n "ratio": 2\n }\n}'])
await unmountEditor(rendered) expect(onChange).toHaveBeenCalledWith(
'{\n "model": {\n "ratio": 2\n }\n}'
)
}) })
}) })
@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { import {
modelGroupSelectorLayoutClasses, modelGroupSelectorLayoutClasses,
@@ -29,8 +28,8 @@ describe('model group selector layout', () => {
const groupScrollClasses = const groupScrollClasses =
modelGroupSelectorLayoutClasses.groupScroll.split(' ') modelGroupSelectorLayoutClasses.groupScroll.split(' ')
assert.ok(groupScrollClasses.includes('auto-rows-[2rem]')) expect(groupScrollClasses.includes('auto-rows-[2rem]')).toBeTruthy()
assert.ok(groupScrollClasses.includes('content-start')) expect(groupScrollClasses.includes('content-start')).toBeTruthy()
}) })
test('centers the selected group inside its own scroll container', () => { test('centers the selected group inside its own scroll container', () => {
@@ -50,7 +49,7 @@ describe('model group selector layout', () => {
scrollSelectedOptionIntoView(selectedOption, scrollContainer) 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', () => { test('falls back to scrollIntoView when no group container is provided', () => {
@@ -63,6 +62,6 @@ describe('model group selector layout', () => {
scrollSelectedOptionIntoView(selectedOption) scrollSelectedOptionIntoView(selectedOption)
assert.deepEqual(scrollCalls, [{ block: 'center', inline: 'nearest' }]) expect(scrollCalls).toEqual([{ block: 'center', inline: 'nearest' }])
}) })
}) })
+5 -6
View File
@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { handleDropdownMenuItemSelect } from './dropdown-menu-events' import { handleDropdownMenuItemSelect } from './dropdown-menu-events'
@@ -52,8 +51,8 @@ describe('DropdownMenuItem onSelect compatibility', () => {
selected = true selected = true
}) })
assert.equal(selected, true) expect(selected).toBe(true)
assert.equal(event.baseUIHandlerPrevented, false) expect(event.baseUIHandlerPrevented).toBe(false)
}) })
test('keeps the Base UI menu open when onSelect prevents default', () => { test('keeps the Base UI menu open when onSelect prevents default', () => {
@@ -63,7 +62,7 @@ describe('DropdownMenuItem onSelect compatibility', () => {
selectEvent.preventDefault() selectEvent.preventDefault()
}) })
assert.equal(event.defaultPrevented, true) expect(event.defaultPrevented).toBe(true)
assert.equal(event.baseUIHandlerPrevented, true) expect(event.baseUIHandlerPrevented).toBe(true)
}) })
}) })
+9 -11
View File
@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import type { RefreshOutcome } from '@/lib/api' import type { RefreshOutcome } from '@/lib/api'
import type { AuthBundle } from '@/stores/auth-store' import type { AuthBundle } from '@/stores/auth-store'
@@ -63,8 +62,8 @@ describe('logout coordination', () => {
}, },
}) })
assert.deepEqual(result, { success: false, message: 'not revoked' }) expect(result).toEqual({ success: false, message: 'not revoked' })
assert.equal(refreshCount, 0) expect(refreshCount).toBe(0)
}) })
test('recovers a cookie mismatch and retries with the refreshed SID', async () => { 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: '' }) expect(result).toEqual({ success: true, message: '' })
assert.deepEqual(requestedSIDs, ['session-a', 'session-b']) expect(requestedSIDs).toEqual(['session-a', 'session-b'])
}) })
test('treats a mismatch that refresh confirms anonymous as signed out', async () => { test('treats a mismatch that refresh confirms anonymous as signed out', async () => {
@@ -96,7 +95,7 @@ describe('logout coordination', () => {
refresh: async () => ({ kind: 'anonymous' }), 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 () => { test('preserves the active session when mismatch recovery is temporary', async () => {
@@ -106,15 +105,14 @@ describe('logout coordination', () => {
error: new Error('offline'), error: new Error('offline'),
} }
await assert.rejects( await expect(
executeLogout({ executeLogout({
getExpectedSID: () => 'session-a', getExpectedSID: () => 'session-a',
request: async () => { request: async () => {
throw originalError throw originalError
}, },
refresh: async () => transient, refresh: async () => transient,
}), })
(error) => error === originalError ).rejects.toBe(originalError)
)
}) })
}) })
@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { import {
getOAuthSessionStorage, getOAuthSessionStorage,
@@ -40,15 +39,14 @@ const bindState = 'bind-state'
describe('resolveOAuthCallbackMode', () => { describe('resolveOAuthCallbackMode', () => {
test('matching provider and state mark is treated as a bind flow', () => { test('matching provider and state mark is treated as a bind flow', () => {
const storage = fakeStorage() const storage = fakeStorage()
assert.equal(markOAuthBindPopup(storage, 'oidc', bindState), true) expect(markOAuthBindPopup(storage, 'oidc', bindState)).toBe(true)
assert.equal( expect(
resolveOAuthCallbackMode('oidc', bindState, { resolveOAuthCallbackMode('oidc', bindState, {
opener: openOpener, opener: openOpener,
storage, storage,
}), })
'bind' ).toBe('bind')
)
}) })
// Regression: a tab opened from an external link (Slack, e-mail, another // 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', () => { test('login redirect in a tab with a foreign opener stays a login flow', () => {
const storage = fakeStorage() const storage = fakeStorage()
assert.equal( expect(
resolveOAuthCallbackMode('oidc', bindState, { resolveOAuthCallbackMode('oidc', bindState, {
opener: openOpener, opener: openOpener,
storage, storage,
}), })
'login' ).toBe('login')
)
}) })
test('bind marker for another provider does not hijack this callback', () => { test('bind marker for another provider does not hijack this callback', () => {
const storage = fakeStorage() const storage = fakeStorage()
markOAuthBindPopup(storage, 'github', bindState) markOAuthBindPopup(storage, 'github', bindState)
assert.equal( expect(
resolveOAuthCallbackMode('oidc', bindState, { resolveOAuthCallbackMode('oidc', bindState, {
opener: openOpener, opener: openOpener,
storage, storage,
}), })
'login' ).toBe('login')
)
}) })
test('stale bind marker does not hijack a later callback', () => { test('stale bind marker does not hijack a later callback', () => {
const storage = fakeStorage() const storage = fakeStorage()
markOAuthBindPopup(storage, 'oidc', 'previous-state') markOAuthBindPopup(storage, 'oidc', 'previous-state')
assert.equal( expect(
resolveOAuthCallbackMode('oidc', bindState, { resolveOAuthCallbackMode('oidc', bindState, {
opener: openOpener, opener: openOpener,
storage, storage,
}), })
'login' ).toBe('login')
)
}) })
test('bind marker without an opener falls back to login', () => { test('bind marker without an opener falls back to login', () => {
const storage = fakeStorage() const storage = fakeStorage()
markOAuthBindPopup(storage, 'oidc', bindState) markOAuthBindPopup(storage, 'oidc', bindState)
assert.equal( expect(
resolveOAuthCallbackMode('oidc', bindState, { resolveOAuthCallbackMode('oidc', bindState, {
opener: null, opener: null,
storage, storage,
}), })
'login' ).toBe('login')
)
}) })
test('closed opener falls back to login', () => { test('closed opener falls back to login', () => {
const storage = fakeStorage() const storage = fakeStorage()
markOAuthBindPopup(storage, 'oidc', bindState) markOAuthBindPopup(storage, 'oidc', bindState)
assert.equal( expect(
resolveOAuthCallbackMode('oidc', bindState, { resolveOAuthCallbackMode('oidc', bindState, {
opener: { closed: true }, opener: { closed: true },
storage, storage,
}), })
'login' ).toBe('login')
)
}) })
test('missing storage degrades to login instead of throwing', () => { test('missing storage degrades to login instead of throwing', () => {
assert.equal( expect(
resolveOAuthCallbackMode('oidc', bindState, { resolveOAuthCallbackMode('oidc', bindState, {
opener: openOpener, opener: openOpener,
storage: null, storage: null,
}), })
'login' ).toBe('login')
)
}) })
test('storage read failure degrades to login instead of throwing', () => { test('storage read failure degrades to login instead of throwing', () => {
@@ -137,13 +129,12 @@ describe('resolveOAuthCallbackMode', () => {
setItem: () => undefined, setItem: () => undefined,
} }
assert.equal( expect(
resolveOAuthCallbackMode('oidc', bindState, { resolveOAuthCallbackMode('oidc', bindState, {
opener: openOpener, opener: openOpener,
storage, 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', () => { test('marking reports unavailable or unwritable storage', () => {
@@ -166,9 +157,9 @@ describe('OAuth bind popup storage', () => {
}, },
} }
assert.equal(markOAuthBindPopup(null, 'oidc', bindState), false) expect(markOAuthBindPopup(null, 'oidc', bindState)).toBe(false)
assert.equal(markOAuthBindPopup(storage, 'oidc', bindState), false) expect(markOAuthBindPopup(storage, 'oidc', bindState)).toBe(false)
assert.equal( expect(
markOAuthBindPopup( markOAuthBindPopup(
{ {
getItem: () => null, getItem: () => null,
@@ -176,8 +167,7 @@ describe('OAuth bind popup storage', () => {
}, },
'oidc', 'oidc',
bindState bindState
), )
false ).toBe(false)
)
}) })
}) })
+15 -22
View File
@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import type { AuthUser } from '@/stores/auth-store' import type { AuthUser } from '@/stores/auth-store'
@@ -27,17 +26,15 @@ const origin = 'https://dashboard.example.com'
describe('authentication redirect validation', () => { describe('authentication redirect validation', () => {
test('preserves safe internal paths, search parameters, and fragments', () => { test('preserves safe internal paths, search parameters, and fragments', () => {
assert.equal( expect(sanitizeAuthRedirect('/console?tab=usage#recent', origin)).toBe(
sanitizeAuthRedirect('/console?tab=usage#recent', origin),
'/console?tab=usage#recent' '/console?tab=usage#recent'
) )
assert.equal( expect(
sanitizeAuthRedirect( sanitizeAuthRedirect(
'https://dashboard.example.com/dashboard?tab=quota#daily', 'https://dashboard.example.com/dashboard?tab=quota#daily',
origin origin
), )
'/dashboard?tab=quota#daily' ).toBe('/dashboard?tab=quota#daily')
)
}) })
test('rejects external and ambiguously parsed redirect targets', () => { test('rejects external and ambiguously parsed redirect targets', () => {
@@ -53,13 +50,13 @@ describe('authentication redirect validation', () => {
] ]
for (const target of unsafeTargets) { 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', () => { test('rejects invalid or non-HTTP application origins', () => {
assert.equal(sanitizeAuthRedirect('/dashboard', 'not-an-origin'), null) expect(sanitizeAuthRedirect('/dashboard', 'not-an-origin')).toBe(null)
assert.equal(sanitizeAuthRedirect('/dashboard', 'file:///tmp/app'), 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 } const user: AuthUser = { id: 1, username: 'user', role: 1 }
test('prefers the explicit user language', () => { test('prefers the explicit user language', () => {
assert.equal( expect(
getSavedLanguage({ getSavedLanguage({
...user, ...user,
language: 'ja', language: 'ja',
setting: { language: 'fr' }, setting: { language: 'fr' },
}), })
'ja' ).toBe('ja')
)
}) })
test('reads object and JSON string settings', () => { test('reads object and JSON string settings', () => {
assert.equal( expect(getSavedLanguage({ ...user, setting: { language: 'fr' } })).toBe(
getSavedLanguage({ ...user, setting: { language: 'fr' } }),
'fr' 'fr'
) )
assert.equal( expect(getSavedLanguage({ ...user, setting: '{"language":"ru"}' })).toBe(
getSavedLanguage({ ...user, setting: '{"language":"ru"}' }),
'ru' 'ru'
) )
}) })
test('ignores malformed and non-string setting languages', () => { test('ignores malformed and non-string setting languages', () => {
assert.equal(getSavedLanguage({ ...user, setting: '{' }), undefined) expect(getSavedLanguage({ ...user, setting: '{' })).toBe(undefined)
assert.equal( expect(getSavedLanguage({ ...user, setting: { language: 123 } })).toBe(
getSavedLanguage({ ...user, setting: { language: 123 } }),
undefined undefined
) )
}) })
@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { import {
parseTelegramBindCallback, parseTelegramBindCallback,
@@ -51,51 +50,48 @@ function fakeTimerRuntime() {
describe('OAuth bind popup lifecycle', () => { describe('OAuth bind popup lifecycle', () => {
test('parses Telegram success and stable error callbacks', () => { test('parses Telegram success and stable error callbacks', () => {
assert.deepEqual( expect(
parseTelegramBindCallback({ parseTelegramBindCallback({
telegram_bind: 'success', telegram_bind: 'success',
flow_token: 'flow-success', flow_token: 'flow-success',
}), })
{ ).toEqual({
kind: 'result', kind: 'result',
flowToken: 'flow-success', flowToken: 'flow-success',
success: true, success: true,
} })
) expect(
assert.deepEqual(
parseTelegramBindCallback({ parseTelegramBindCallback({
telegram_bind: 'error', telegram_bind: 'error',
flow_token: 'flow-error', flow_token: 'flow-error',
error_code: 'TELEGRAM_BIND_ALREADY_BOUND', error_code: 'TELEGRAM_BIND_ALREADY_BOUND',
}), })
{ ).toEqual({
kind: 'result', kind: 'result',
flowToken: 'flow-error', flowToken: 'flow-error',
success: false, success: false,
code: 'TELEGRAM_BIND_ALREADY_BOUND', code: 'TELEGRAM_BIND_ALREADY_BOUND',
} })
)
}) })
test('rejects Telegram callbacks without a flow token and ignores descriptions', () => { 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', kind: 'invalid',
}) })
assert.deepEqual( expect(
parseTelegramBindCallback({ parseTelegramBindCallback({
telegram_bind: 'error', telegram_bind: 'error',
flow_token: 'flow-error', flow_token: 'flow-error',
error_code: 'UNKNOWN_CODE', error_code: 'UNKNOWN_CODE',
error_description: 'untrusted message', error_description: 'untrusted message',
} as Parameters<typeof parseTelegramBindCallback>[0]), } as Parameters<typeof parseTelegramBindCallback>[0])
{ ).toEqual({
kind: 'result', kind: 'result',
flowToken: 'flow-error', flowToken: 'flow-error',
success: false, success: false,
code: 'UNKNOWN_CODE', code: 'UNKNOWN_CODE',
} })
) expect(parseTelegramBindCallback({})).toBe(null)
assert.equal(parseTelegramBindCallback({}), null)
}) })
test('posts only complete Telegram bind results to an available opener', () => { test('posts only complete Telegram bind results to an available opener', () => {
@@ -112,11 +108,10 @@ describe('OAuth bind popup lifecycle', () => {
error_code: 'UNKNOWN_CODE', error_code: 'UNKNOWN_CODE',
}) })
assert.equal( expect(
postTelegramBindResult(callback, opener, 'https://dashboard.example.com'), postTelegramBindResult(callback, opener, 'https://dashboard.example.com')
true ).toBe(true)
) expect(messages).toEqual([
assert.deepEqual(messages, [
{ {
message: { message: {
type: 'telegram:binding:result', type: 'telegram:binding:result',
@@ -128,23 +123,17 @@ describe('OAuth bind popup lifecycle', () => {
}, },
]) ])
assert.equal( expect(
postTelegramBindResult( postTelegramBindResult({ kind: 'invalid' }, opener, 'https://example.com')
{ kind: 'invalid' }, ).toBe(false)
opener, expect(
'https://example.com'
),
false
)
assert.equal(
postTelegramBindResult( postTelegramBindResult(
callback, callback,
{ ...opener, closed: true }, { ...opener, closed: true },
'https://example.com' 'https://example.com'
), )
false ).toBe(false)
) expect(messages.length).toBe(1)
assert.equal(messages.length, 1)
}) })
test('waits 30 seconds for the opener response and can be cancelled', () => { test('waits 30 seconds for the opener response and can be cancelled', () => {
@@ -158,11 +147,11 @@ describe('OAuth bind popup lifecycle', () => {
timer.runtime timer.runtime
) )
assert.equal(timer.delay, 30_000) expect(timer.delay).toBe(30_000)
cancel() cancel()
timer.fire() timer.fire()
assert.equal(timedOut, false) expect(timedOut).toBe(false)
assert.deepEqual(timer.cancelled, [timer.handle]) expect(timer.cancelled).toEqual([timer.handle])
}) })
test('reports a closed popup once and clears its poller', () => { test('reports a closed popup once and clears its poller', () => {
@@ -178,13 +167,13 @@ describe('OAuth bind popup lifecycle', () => {
timer.runtime timer.runtime
) )
assert.equal(timer.delay, 500) expect(timer.delay).toBe(500)
timer.fire() timer.fire()
assert.equal(closedCount, 0) expect(closedCount).toBe(0)
popup.closed = true popup.closed = true
timer.fire() timer.fire()
timer.fire() timer.fire()
assert.equal(closedCount, 1) expect(closedCount).toBe(1)
assert.deepEqual(timer.cancelled, [timer.handle]) expect(timer.cancelled).toEqual([timer.handle])
}) })
}) })
@@ -16,14 +16,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { pickTelegramAuthorization } from './telegram-login' import { pickTelegramAuthorization } from './telegram-login'
describe('Telegram login authorization', () => { describe('Telegram login authorization', () => {
test('keeps only fields signed by the Telegram login contract', () => { test('keeps only fields signed by the Telegram login contract', () => {
assert.deepEqual( expect(
pickTelegramAuthorization({ pickTelegramAuthorization({
id: 12345, id: 12345,
first_name: 'Test', first_name: 'Test',
@@ -35,33 +34,27 @@ describe('Telegram login authorization', () => {
lang: 'en', lang: 'en',
admin: true, admin: true,
redirect: 'https://attacker.example', redirect: 'https://attacker.example',
}), })
{ ).toEqual({
id: 12345, id: 12345,
first_name: 'Test', first_name: 'Test',
last_name: 'User', last_name: 'User',
username: 'test_user', username: 'test_user',
photo_url: 'https://t.me/i/userpic/320/test.jpg', photo_url: 'https://t.me/i/userpic/320/test.jpg',
auth_date: 1_900_000_000, auth_date: 1_900_000_000,
hash: 'signed-hash', hash: 'signed-hash',
lang: 'en', lang: 'en',
} })
)
}) })
test('rejects incomplete or structurally invalid callbacks', () => { test('rejects incomplete or structurally invalid callbacks', () => {
assert.equal(pickTelegramAuthorization(null), null) expect(pickTelegramAuthorization(null)).toBe(null)
assert.equal( expect(pickTelegramAuthorization({ auth_date: 1, hash: 'hash' })).toBe(null)
pickTelegramAuthorization({ auth_date: 1, hash: 'hash' }), expect(pickTelegramAuthorization({ id: 1, auth_date: 1, hash: '' })).toBe(
null
)
assert.equal(
pickTelegramAuthorization({ id: 1, auth_date: 1, hash: '' }),
null
)
assert.equal(
pickTelegramAuthorization({ id: {}, auth_date: 1, hash: 'hash' }),
null null
) )
expect(
pickTelegramAuthorization({ id: {}, auth_date: 1, hash: 'hash' })
).toBe(null)
}) })
}) })
@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { import {
CHANNEL_FIELD_UPDATE_DELAY_MS, CHANNEL_FIELD_UPDATE_DELAY_MS,
@@ -31,7 +30,7 @@ function createFakeTimers() {
return { return {
timers: { timers: {
setTimeout: (callback: () => void, delay: number) => { setTimeout: (callback: () => void, delay: number) => {
assert.equal(delay, CHANNEL_FIELD_UPDATE_DELAY_MS) expect(delay).toBe(CHANNEL_FIELD_UPDATE_DELAY_MS)
const id = nextId++ const id = nextId++
pending.set(id, callback) pending.set(id, callback)
return id return id
@@ -63,11 +62,11 @@ describe('channel field update scheduler', () => {
scheduler.schedule(1) scheduler.schedule(1)
scheduler.schedule(2) scheduler.schedule(2)
scheduler.schedule(3) scheduler.schedule(3)
assert.deepEqual(updates, []) expect(updates).toEqual([])
assert.equal(fake.pendingCount, 1) expect(fake.pendingCount).toBe(1)
fake.fireAll() fake.fireAll()
assert.deepEqual(updates, [3]) expect(updates).toEqual([3])
}) })
test('flush commits the pending value immediately and cancels the timer', () => { test('flush commits the pending value immediately and cancels the timer', () => {
@@ -80,11 +79,11 @@ describe('channel field update scheduler', () => {
scheduler.schedule(7) scheduler.schedule(7)
scheduler.flush() scheduler.flush()
assert.deepEqual(updates, [7]) expect(updates).toEqual([7])
assert.equal(fake.pendingCount, 0) expect(fake.pendingCount).toBe(0)
fake.fireAll() fake.fireAll()
assert.deepEqual(updates, [7]) expect(updates).toEqual([7])
}) })
test('flush without a pending value does nothing', () => { test('flush without a pending value does nothing', () => {
@@ -99,7 +98,7 @@ describe('channel field update scheduler', () => {
scheduler.schedule(5) scheduler.schedule(5)
scheduler.flush() scheduler.flush()
scheduler.flush() scheduler.flush()
assert.deepEqual(updates, [5]) expect(updates).toEqual([5])
}) })
test('preserves a pending value of 0', () => { test('preserves a pending value of 0', () => {
@@ -112,6 +111,6 @@ describe('channel field update scheduler', () => {
scheduler.schedule(0) scheduler.schedule(0)
scheduler.flush() scheduler.flush()
assert.deepEqual(updates, [0]) expect(updates).toEqual([0])
}) })
}) })
@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import type { Channel } from '../../types' import type { Channel } from '../../types'
import { getChannelTableRowId, type TagRow } from '../channel-utils' import { getChannelTableRowId, type TagRow } from '../channel-utils'
@@ -35,12 +34,8 @@ describe('channel table row identity', () => {
const beforeUpdate = [first, updated, third].map(getChannelTableRowId) const beforeUpdate = [first, updated, third].map(getChannelTableRowId)
const afterUpdate = [updated, first, third].map(getChannelTableRowId) const afterUpdate = [updated, first, third].map(getChannelTableRowId)
assert.deepEqual(beforeUpdate, [ expect(beforeUpdate).toEqual(['channel:101', 'channel:202', 'channel:303'])
'channel:101', expect(afterUpdate).toEqual(['channel:202', 'channel:101', 'channel:303'])
'channel:202',
'channel:303',
])
assert.deepEqual(afterUpdate, ['channel:202', 'channel:101', 'channel:303'])
}) })
test('uses separate namespaces for tag and channel rows', () => { test('uses separate namespaces for tag and channel rows', () => {
@@ -50,7 +45,7 @@ describe('channel table row identity', () => {
children: [channel(202)], children: [channel(202)],
} as TagRow } as TagRow
assert.equal(getChannelTableRowId(tagRow), 'tag:202') expect(getChannelTableRowId(tagRow)).toBe('tag:202')
assert.equal(getChannelTableRowId(channel(202)), 'channel:202') expect(getChannelTableRowId(channel(202))).toBe('channel:202')
}) })
}) })
@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { import {
CHANNEL_TYPE_NEW_API, CHANNEL_TYPE_NEW_API,
@@ -45,45 +44,40 @@ describe('New API channel', () => {
(item) => item.value === CHANNEL_TYPE_NEW_API (item) => item.value === CHANNEL_TYPE_NEW_API
) )
assert.deepEqual(option, { expect(option).toEqual({
value: CHANNEL_TYPE_NEW_API, value: CHANNEL_TYPE_NEW_API,
label: 'New API', label: 'New API',
}) })
assert.equal( expect(
CHANNEL_TYPE_OPTIONS.findIndex( CHANNEL_TYPE_OPTIONS.findIndex(
(item) => item.value === CHANNEL_TYPE_NEW_API (item) => item.value === CHANNEL_TYPE_NEW_API
) + 1, ) + 1
CHANNEL_TYPE_OPTIONS.findIndex((item) => item.value === 58) ).toBe(CHANNEL_TYPE_OPTIONS.findIndex((item) => item.value === 58))
) expect(MODEL_FETCHABLE_TYPES.has(CHANNEL_TYPE_NEW_API)).toBe(true)
assert.equal(MODEL_FETCHABLE_TYPES.has(CHANNEL_TYPE_NEW_API), true) expect(getChannelTypeIcon(CHANNEL_TYPE_NEW_API)).toBe('NewAPI')
assert.equal(getChannelTypeIcon(CHANNEL_TYPE_NEW_API), 'NewAPI') expect(getKeyPromptForType(CHANNEL_TYPE_NEW_API)).toBe(
assert.equal(
getKeyPromptForType(CHANNEL_TYPE_NEW_API),
'Enter API key for this channel' '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', () => { test('requires a non-blank Base URL', () => {
const blankResult = channelFormSchema.safeParse(newAPIForm(' ')) const blankResult = channelFormSchema.safeParse(newAPIForm(' '))
assert.equal(blankResult.success, false) expect(blankResult.success).toBe(false)
if (!blankResult.success) { if (!blankResult.success) {
assert.equal( expect(
blankResult.error.issues.some( blankResult.error.issues.some(
(issue) => (issue) =>
issue.path[0] === 'base_url' && issue.path[0] === 'base_url' &&
issue.message === 'Base URL is required for this channel type' issue.message === 'Base URL is required for this channel type'
), )
true ).toBe(true)
)
} }
assert.equal( expect(
channelFormSchema.safeParse(newAPIForm('https://new-api.example')) channelFormSchema.safeParse(newAPIForm('https://new-api.example')).success
.success, ).toBe(true)
true
)
}) })
test('keeps Sub2API Base URL validation unchanged', () => { test('keeps Sub2API Base URL validation unchanged', () => {
@@ -92,6 +86,6 @@ describe('New API channel', () => {
type: 59, type: 59,
}) })
assert.equal(result.success, true) expect(result.success).toBe(true)
}) })
}) })
@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import type { FlowUserFilterOption } from '../types' import type { FlowUserFilterOption } from '../types'
import { import {
@@ -46,89 +45,75 @@ const users: FlowUserFilterOption[] = [
describe('dashboard flow selection helpers', () => { describe('dashboard flow selection helpers', () => {
test('limits user chips to currently visible users', () => { test('limits user chips to currently visible users', () => {
assert.deepEqual( expect(visibleFlowUsers(users, []).map((user) => user.value)).toEqual([
visibleFlowUsers(users, []).map((user) => user.value), 'user:1',
['user:1', 'user:2'] 'user:2',
) ])
assert.deepEqual( expect(
visibleFlowUsers(users, ['user:2']).map((user) => user.value), visibleFlowUsers(users, ['user:2']).map((user) => user.value)
['user:2'] ).toEqual(['user:2'])
)
}) })
test('filters visible users without mutating the source options', () => { test('filters visible users without mutating the source options', () => {
const visible = visibleFlowUsers(users, ['user:1']) const visible = visibleFlowUsers(users, ['user:1'])
assert.deepEqual( expect(visible.map((user) => user.value)).toEqual(['user:1'])
visible.map((user) => user.value), expect(users.map((user) => user.value)).toEqual(['user:1', 'user:2'])
['user:1']
)
assert.deepEqual(
users.map((user) => user.value),
['user:1', 'user:2']
)
}) })
test('formats compact selected counts for flow multiselect summaries', () => { test('formats compact selected counts for flow multiselect summaries', () => {
assert.equal(compactFlowSelectionLabel(0), '*') expect(compactFlowSelectionLabel(0)).toBe('*')
assert.equal(compactFlowSelectionLabel(1), '1') expect(compactFlowSelectionLabel(1)).toBe('1')
assert.equal(compactFlowSelectionLabel(23), '23') expect(compactFlowSelectionLabel(23)).toBe('23')
}) })
test('prioritizes loading and error states before empty flow data', () => { test('prioritizes loading and error states before empty flow data', () => {
assert.equal( expect(
flowDisplayState({ flowDisplayState({
isLoading: true, isLoading: true,
isError: true, isError: true,
linkCount: 0, linkCount: 0,
themeReady: true, themeReady: true,
}), })
'loading' ).toBe('loading')
) expect(
assert.equal(
flowDisplayState({ flowDisplayState({
isLoading: false, isLoading: false,
isError: true, isError: true,
linkCount: 0, linkCount: 0,
themeReady: true, themeReady: true,
}), })
'error' ).toBe('error')
) expect(
assert.equal(
flowDisplayState({ flowDisplayState({
isLoading: false, isLoading: false,
isError: false, isError: false,
linkCount: 0, linkCount: 0,
themeReady: true, themeReady: true,
}), })
'empty' ).toBe('empty')
) expect(
assert.equal(
flowDisplayState({ flowDisplayState({
isLoading: false, isLoading: false,
isError: false, isError: false,
linkCount: 1, linkCount: 1,
themeReady: false, themeReady: false,
}), })
'loading' ).toBe('loading')
)
}) })
test('throws unsuccessful flow responses instead of treating them as empty data', () => { test('throws unsuccessful flow responses instead of treating them as empty data', () => {
assert.throws( expect(() =>
() => requireSuccessfulFlowRows(
requireSuccessfulFlowRows( { success: false, data: [], message: 'database unavailable' },
{ success: false, data: [], message: 'database unavailable' }, 'Failed to load'
'Failed to load' )
), ).toThrow(/database unavailable/)
/database unavailable/ expect(
)
assert.deepEqual(
requireSuccessfulFlowRows( requireSuccessfulFlowRows(
{ success: true, data: [{ user_id: 1, quota: 10 }] }, { success: true, data: [{ user_id: 1, quota: 10 }] },
'Failed to load' 'Failed to load'
), )
[{ user_id: 1, quota: 10 }] ).toEqual([{ user_id: 1, quota: 10 }])
)
}) })
}) })
+243 -282
View File
@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import type { FlowQuotaDataItem } from '../types' import type { FlowQuotaDataItem } from '../types'
import { import {
@@ -113,18 +112,16 @@ describe('dashboard flow data', () => {
role: 'user', role: 'user',
}) })
assert.equal(result.summary.quota, 150) expect(result.summary.quota).toBe(150)
assert.equal(result.summary.tokens, 60) expect(result.summary.tokens).toBe(60)
assert.equal(result.summary.requests, 3) expect(result.summary.requests).toBe(3)
assert.deepEqual( expect(
result.flow.links.map((link) => [link.source, link.target, link.value]), result.flow.links.map((link) => [link.source, link.target, link.value])
[ ).toEqual([
['group:vip', 'model:gpt-4.1', 150], ['group:vip', 'model:gpt-4.1', 150],
['token:11', 'group:vip', 150], ['token:11', 'group:vip', 150],
] ])
) expect(result.flow.nodes.some((node) => node.kind === 'channel')).toBe(
assert.equal(
result.flow.nodes.some((node) => node.kind === 'channel'),
false false
) )
}) })
@@ -134,18 +131,17 @@ describe('dashboard flow data', () => {
role: 'admin', role: 'admin',
}) })
assert.deepEqual( expect(
result.flow.links.map((link) => [link.source, link.target, link.value]), result.flow.links.map((link) => [link.source, link.target, link.value])
[ ).toEqual([
['group:default', 'model:claude-4-sonnet', 70], ['group:default', 'model:claude-4-sonnet', 70],
['group:vip', 'model:gpt-4.1', 150], ['group:vip', 'model:gpt-4.1', 150],
['model:claude-4-sonnet', 'channel:101', 70], ['model:claude-4-sonnet', 'channel:101', 70],
['model:gpt-4.1', 'channel:101', 100], ['model:gpt-4.1', 'channel:101', 100],
['model:gpt-4.1', 'channel:102', 50], ['model:gpt-4.1', 'channel:102', 50],
['user:1', 'group:vip', 150], ['user:1', 'group:vip', 150],
['user:2', 'group:default', 70], ['user:2', 'group:default', 70],
] ])
)
}) })
test('builds root user-node-token-group-model-channel flow', () => { test('builds root user-node-token-group-model-channel flow', () => {
@@ -153,22 +149,21 @@ describe('dashboard flow data', () => {
role: 'root', role: 'root',
}) })
assert.deepEqual( expect(
result.flow.links.map((link) => [link.source, link.target, link.value]), result.flow.links.map((link) => [link.source, link.target, link.value])
[ ).toEqual([
['group:default', 'model:claude-4-sonnet', 3], ['group:default', 'model:claude-4-sonnet', 3],
['group:vip', 'model:gpt-4.1', 3], ['group:vip', 'model:gpt-4.1', 3],
['model:claude-4-sonnet', 'channel:101', 3], ['model:claude-4-sonnet', 'channel:101', 3],
['model:gpt-4.1', 'channel:101', 2], ['model:gpt-4.1', 'channel:101', 2],
['model:gpt-4.1', 'channel:102', 1], ['model:gpt-4.1', 'channel:102', 1],
['node:node-a', 'token:11', 3], ['node:node-a', 'token:11', 3],
['node:node-b', 'token:22', 3], ['node:node-b', 'token:22', 3],
['token:11', 'group:vip', 3], ['token:11', 'group:vip', 3],
['token:22', 'group:default', 3], ['token:22', 'group:default', 3],
['user:1', 'node:node-a', 3], ['user:1', 'node:node-a', 3],
['user:2', 'node:node-b', 3], ['user:2', 'node:node-b', 3],
] ])
)
}) })
test('filters by selected users', () => { test('filters by selected users', () => {
@@ -177,15 +172,14 @@ describe('dashboard flow data', () => {
selectedUsers: ['user:2'], selectedUsers: ['user:2'],
}) })
assert.equal(result.summary.quota, 70) expect(result.summary.quota).toBe(70)
assert.deepEqual( expect(
result.flow.links.map((link) => [link.source, link.target, link.value]), result.flow.links.map((link) => [link.source, link.target, link.value])
[ ).toEqual([
['group:default', 'model:claude-4-sonnet', 70], ['group:default', 'model:claude-4-sonnet', 70],
['model:claude-4-sonnet', 'channel:101', 70], ['model:claude-4-sonnet', 'channel:101', 70],
['user:2', 'group:default', 70], ['user:2', 'group:default', 70],
] ])
)
}) })
test('filters rows by selected flow nodes', () => { test('filters rows by selected flow nodes', () => {
@@ -194,16 +188,15 @@ describe('dashboard flow data', () => {
selectedNodes: [{ kind: 'model', id: 'model:gpt-4.1' }], selectedNodes: [{ kind: 'model', id: 'model:gpt-4.1' }],
}) })
assert.equal(result.summary.quota, 150) expect(result.summary.quota).toBe(150)
assert.deepEqual( expect(
result.flow.links.map((link) => [link.source, link.target, link.value]), result.flow.links.map((link) => [link.source, link.target, link.value])
[ ).toEqual([
['group:vip', 'model:gpt-4.1', 150], ['group:vip', 'model:gpt-4.1', 150],
['model:gpt-4.1', 'channel:101', 100], ['model:gpt-4.1', 'channel:101', 100],
['model:gpt-4.1', 'channel:102', 50], ['model:gpt-4.1', 'channel:102', 50],
['user:1', 'group:vip', 150], ['user:1', 'group:vip', 150],
] ])
)
}) })
test('combines node filters with OR inside a column and AND across columns', () => { 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) expect(sameColumn.summary.quota).toBe(220)
assert.equal(crossColumn.summary.quota, 100) expect(crossColumn.summary.quota).toBe(100)
assert.deepEqual( expect(
crossColumn.flow.links.map((link) => [ crossColumn.flow.links.map((link) => [
link.source, link.source,
link.target, link.target,
link.value, link.value,
]), ])
[ ).toEqual([
['group:vip', 'model:gpt-4.1', 100], ['group:vip', 'model:gpt-4.1', 100],
['model:gpt-4.1', 'channel:101', 100], ['model:gpt-4.1', 'channel:101', 100],
['user:1', 'group:vip', 100], ['user:1', 'group:vip', 100],
] ])
)
}) })
test('combines user and node filters', () => { test('combines user and node filters', () => {
@@ -245,15 +237,14 @@ describe('dashboard flow data', () => {
selectedNodes: [{ kind: 'channel', id: 'channel:101' }], selectedNodes: [{ kind: 'channel', id: 'channel:101' }],
}) })
assert.equal(result.summary.quota, 100) expect(result.summary.quota).toBe(100)
assert.deepEqual( expect(
result.flow.links.map((link) => [link.source, link.target, link.value]), result.flow.links.map((link) => [link.source, link.target, link.value])
[ ).toEqual([
['group:vip', 'model:gpt-4.1', 100], ['group:vip', 'model:gpt-4.1', 100],
['model:gpt-4.1', 'channel:101', 100], ['model:gpt-4.1', 'channel:101', 100],
['user:1', 'group:vip', 100], ['user:1', 'group:vip', 100],
] ])
)
}) })
test('reconnects links when a middle stage is hidden', () => { test('reconnects links when a middle stage is hidden', () => {
@@ -262,20 +253,16 @@ describe('dashboard flow data', () => {
visibleStages: ['user', 'model', 'channel'], visibleStages: ['user', 'model', 'channel'],
}) })
assert.deepEqual( expect(
result.flow.links.map((link) => [link.source, link.target, link.value]), result.flow.links.map((link) => [link.source, link.target, link.value])
[ ).toEqual([
['model:claude-4-sonnet', 'channel:101', 70], ['model:claude-4-sonnet', 'channel:101', 70],
['model:gpt-4.1', 'channel:101', 100], ['model:gpt-4.1', 'channel:101', 100],
['model:gpt-4.1', 'channel:102', 50], ['model:gpt-4.1', 'channel:102', 50],
['user:1', 'model:gpt-4.1', 150], ['user:1', 'model:gpt-4.1', 150],
['user:2', 'model:claude-4-sonnet', 70], ['user:2', 'model:claude-4-sonnet', 70],
] ])
) expect(result.flow.nodes.some((node) => node.kind === 'group')).toBe(false)
assert.equal(
result.flow.nodes.some((node) => node.kind === 'group'),
false
)
}) })
test('ignores stage filters that would leave fewer than two columns', () => { test('ignores stage filters that would leave fewer than two columns', () => {
@@ -284,26 +271,24 @@ describe('dashboard flow data', () => {
visibleStages: ['model'], visibleStages: ['model'],
}) })
assert.deepEqual( expect(
result.flow.links.map((link) => [link.source, link.target, link.value]), result.flow.links.map((link) => [link.source, link.target, link.value])
[ ).toEqual([
['group:vip', 'model:gpt-4.1', 150], ['group:vip', 'model:gpt-4.1', 150],
['token:11', 'group:vip', 150], ['token:11', 'group:vip', 150],
] ])
)
}) })
test('builds user filter options with stable values', () => { test('builds user filter options with stable values', () => {
const options = buildFlowFilterOptions(rows, 'quota') const options = buildFlowFilterOptions(rows, 'quota')
assert.deepEqual( expect(
options.users.map((user) => [user.value, user.label, user.valueLabel]), options.users.map((user) => [user.value, user.label, user.valueLabel])
[ ).toEqual([
['user:1', 'alice', '150'], ['user:1', 'alice', '150'],
['user:2', 'bob', '70'], ['user:2', 'bob', '70'],
] ])
) expect(options.users[0].color).not.toBe(options.users[1].color)
assert.notEqual(options.users[0].color, options.users[1].color)
}) })
test('builds node filter options without applying top limits', () => { test('builds node filter options without applying top limits', () => {
@@ -313,22 +298,20 @@ describe('dashboard flow data', () => {
overflowMode: 'aggregate', overflowMode: 'aggregate',
}) })
assert.equal( expect(
result.filterOptions.nodes.some( result.filterOptions.nodes.some(
(option) => option.kind === 'model' && option.value === 'model:model-c' (option) => option.kind === 'model' && option.value === 'model:model-c'
), )
true ).toBe(true)
) expect(
assert.deepEqual(
result.filterOptions.nodes result.filterOptions.nodes
.filter((option) => option.kind === 'model') .filter((option) => option.kind === 'model')
.map((option) => [option.value, option.valueLabel]), .map((option) => [option.value, option.valueLabel])
[ ).toEqual([
['model:model-a', '100'], ['model:model-a', '100'],
['model:model-b', '80'], ['model:model-b', '80'],
['model:model-c', '10'], ['model:model-c', '10'],
] ])
)
}) })
test('facets node filter options by selected nodes from other columns', () => { test('facets node filter options by selected nodes from other columns', () => {
@@ -338,30 +321,27 @@ describe('dashboard flow data', () => {
}) })
const nodeOptions = result.filterOptions.nodes const nodeOptions = result.filterOptions.nodes
assert.deepEqual( expect(
nodeOptions nodeOptions
.filter((option) => option.kind === 'node') .filter((option) => option.kind === 'node')
.map((option) => [option.value, option.valueLabel]), .map((option) => [option.value, option.valueLabel])
[ ).toEqual([
['node:node-a', '150'], ['node:node-a', '150'],
['node:node-b', '70'], ['node:node-b', '70'],
] ])
) expect(
assert.deepEqual(
nodeOptions nodeOptions
.filter((option) => option.kind === 'token') .filter((option) => option.kind === 'token')
.map((option) => [option.value, option.valueLabel]), .map((option) => [option.value, option.valueLabel])
[['token:11', '150']] ).toEqual([['token:11', '150']])
) expect(
assert.deepEqual(
nodeOptions nodeOptions
.filter((option) => option.kind === 'channel') .filter((option) => option.kind === 'channel')
.map((option) => [option.value, option.valueLabel]), .map((option) => [option.value, option.valueLabel])
[ ).toEqual([
['channel:101', '100'], ['channel:101', '100'],
['channel:102', '50'], ['channel:102', '50'],
] ])
)
}) })
test('keeps same-column node options available for OR filtering', () => { 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' }], selectedNodes: [{ kind: 'model', id: 'model:gpt-4.1' }],
}) })
assert.deepEqual( expect(
result.filterOptions.nodes result.filterOptions.nodes
.filter((option) => option.kind === 'model') .filter((option) => option.kind === 'model')
.map((option) => [option.value, option.valueLabel]), .map((option) => [option.value, option.valueLabel])
[ ).toEqual([
['model:gpt-4.1', '150'], ['model:gpt-4.1', '150'],
['model:claude-4-sonnet', '70'], ['model:claude-4-sonnet', '70'],
] ])
) expect(
assert.deepEqual(
result.filterOptions.nodes result.filterOptions.nodes
.filter((option) => option.kind === 'channel') .filter((option) => option.kind === 'channel')
.map((option) => [option.value, option.valueLabel]), .map((option) => [option.value, option.valueLabel])
[ ).toEqual([
['channel:101', '100'], ['channel:101', '100'],
['channel:102', '50'], ['channel:102', '50'],
] ])
)
}) })
test('combines user filters with faceted node filter options', () => { test('combines user filters with faceted node filter options', () => {
@@ -397,22 +375,20 @@ describe('dashboard flow data', () => {
selectedNodes: [{ kind: 'channel', id: 'channel:101' }], selectedNodes: [{ kind: 'channel', id: 'channel:101' }],
}) })
assert.equal(result.summary.quota, 100) expect(result.summary.quota).toBe(100)
assert.deepEqual( expect(
result.filterOptions.nodes result.filterOptions.nodes
.filter((option) => option.kind === 'model') .filter((option) => option.kind === 'model')
.map((option) => [option.value, option.valueLabel]), .map((option) => [option.value, option.valueLabel])
[['model:gpt-4.1', '100']] ).toEqual([['model:gpt-4.1', '100']])
) expect(
assert.deepEqual(
result.filterOptions.nodes result.filterOptions.nodes
.filter((option) => option.kind === 'channel') .filter((option) => option.kind === 'channel')
.map((option) => [option.value, option.valueLabel]), .map((option) => [option.value, option.valueLabel])
[ ).toEqual([
['channel:101', '100'], ['channel:101', '100'],
['channel:102', '50'], ['channel:102', '50'],
] ])
)
}) })
test('aggregates overflow nodes into per-column Other buckets', () => { test('aggregates overflow nodes into per-column Other buckets', () => {
@@ -434,18 +410,18 @@ describe('dashboard flow data', () => {
.filter((link) => link.source.startsWith('user:')) .filter((link) => link.source.startsWith('user:'))
.reduce((sum, link) => sum + link.value, 0) .reduce((sum, link) => sum + link.value, 0)
assert.equal(result.summary.quota, 190) expect(result.summary.quota).toBe(190)
assert.equal(firstStepTotal, 190) expect(firstStepTotal).toBe(190)
assert.equal(otherUser?.label, 'Other user') expect(otherUser?.label).toBe('Other user')
assert.equal(otherFirstStepLink?.value, 10) expect(otherFirstStepLink?.value).toBe(10)
assert.equal(nodeIds.has('user:3'), false) expect(nodeIds.has('user:3')).toBe(false)
assert.equal(nodeIds.has('group:free'), false) expect(nodeIds.has('group:free')).toBe(false)
assert.equal(nodeIds.has('model:model-c'), false) expect(nodeIds.has('model:model-c')).toBe(false)
assert.equal(nodeIds.has('channel:203'), false) expect(nodeIds.has('channel:203')).toBe(false)
assert.equal(nodeIds.has('user:__other__'), true) expect(nodeIds.has('user:__other__')).toBe(true)
assert.equal(nodeIds.has('group:__other__'), true) expect(nodeIds.has('group:__other__')).toBe(true)
assert.equal(nodeIds.has('model:__other__'), true) expect(nodeIds.has('model:__other__')).toBe(true)
assert.equal(nodeIds.has('channel:__other__'), true) expect(nodeIds.has('channel:__other__')).toBe(true)
}) })
test('hides overflow paths when overflow mode is hide', () => { test('hides overflow paths when overflow mode is hide', () => {
@@ -460,11 +436,11 @@ describe('dashboard flow data', () => {
.filter((link) => link.source.startsWith('user:')) .filter((link) => link.source.startsWith('user:'))
.reduce((sum, link) => sum + link.value, 0) .reduce((sum, link) => sum + link.value, 0)
assert.equal(result.summary.quota, 190) expect(result.summary.quota).toBe(190)
assert.equal(firstStepTotal, 180) expect(firstStepTotal).toBe(180)
assert.equal(nodeIds.has('user:3'), false) expect(nodeIds.has('user:3')).toBe(false)
assert.equal(nodeIds.has('user:__other__'), false) expect(nodeIds.has('user:__other__')).toBe(false)
assert.equal(nodeIds.has('model:__other__'), false) expect(nodeIds.has('model:__other__')).toBe(false)
}) })
test('ranks top nodes using the selected flow metric', () => { test('ranks top nodes using the selected flow metric', () => {
@@ -484,18 +460,11 @@ describe('dashboard flow data', () => {
overflowMode: 'aggregate', overflowMode: 'aggregate',
}) })
assert.equal( expect(byQuota.flow.nodes.some((node) => node.id === 'user:1')).toBe(true)
byQuota.flow.nodes.some((node) => node.id === 'user:1'), expect(byRequests.flow.nodes.some((node) => node.id === 'user:2')).toBe(
true
)
assert.equal(
byRequests.flow.nodes.some((node) => node.id === 'user:2'),
true
)
assert.equal(
byTokens.flow.nodes.some((node) => node.id === 'user:3'),
true true
) )
expect(byTokens.flow.nodes.some((node) => node.id === 'user:3')).toBe(true)
}) })
test('applies top limits only to visible stages', () => { 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)) const nodeIds = new Set(result.flow.nodes.map((node) => node.id))
assert.equal(nodeIds.has('user:1'), true) expect(nodeIds.has('user:1')).toBe(true)
assert.equal(nodeIds.has('user:__other__'), true) expect(nodeIds.has('user:__other__')).toBe(true)
assert.equal(nodeIds.has('model:model-a'), true) expect(nodeIds.has('model:model-a')).toBe(true)
assert.equal(nodeIds.has('model:__other__'), true) expect(nodeIds.has('model:__other__')).toBe(true)
assert.equal(nodeIds.has('group:__other__'), false) expect(nodeIds.has('group:__other__')).toBe(false)
assert.equal(nodeIds.has('channel:__other__'), false) expect(nodeIds.has('channel:__other__')).toBe(false)
assert.deepEqual( expect(
result.flow.links.map((link) => [link.source, link.target, link.value]), result.flow.links.map((link) => [link.source, link.target, link.value])
[ ).toEqual([
['user:__other__', 'model:__other__', 90], ['user:__other__', 'model:__other__', 90],
['user:1', 'model:model-a', 100], ['user:1', 'model:model-a', 100],
] ])
)
}) })
test('applies top limits after node filters', () => { 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)) const nodeIds = new Set(result.flow.nodes.map((node) => node.id))
assert.equal(result.summary.quota, 10) expect(result.summary.quota).toBe(10)
assert.equal(nodeIds.has('model:model-c'), true) expect(nodeIds.has('model:model-c')).toBe(true)
assert.equal(nodeIds.has('model:__other__'), false) expect(nodeIds.has('model:__other__')).toBe(false)
assert.deepEqual( expect(
result.flow.links.map((link) => [link.source, link.target, link.value]), result.flow.links.map((link) => [link.source, link.target, link.value])
[ ).toEqual([
['group:free', 'model:model-c', 10], ['group:free', 'model:model-c', 10],
['model:model-c', 'channel:203', 10], ['model:model-c', 'channel:203', 10],
['user:3', 'group:free', 10], ['user:3', 'group:free', 10],
] ])
)
}) })
test('ignores selected node filters for hidden stages', () => { test('ignores selected node filters for hidden stages', () => {
@@ -551,9 +518,8 @@ describe('dashboard flow data', () => {
selectedNodes: [{ kind: 'group', id: 'group:vip' }], selectedNodes: [{ kind: 'group', id: 'group:vip' }],
}) })
assert.equal(result.summary.quota, 220) expect(result.summary.quota).toBe(220)
assert.equal( expect(result.flow.nodes.some((node) => node.id === 'group:vip')).toBe(
result.flow.nodes.some((node) => node.id === 'group:vip'),
false false
) )
}) })
@@ -576,35 +542,35 @@ describe('dashboard flow data', () => {
]) ])
) )
assert.deepEqual(nodeState.get('user:1'), { expect(nodeState.get('user:1')).toEqual({
highlighted: true, highlighted: true,
dimmed: false, dimmed: false,
}) })
assert.deepEqual(nodeState.get('node:node-a'), { expect(nodeState.get('node:node-a')).toEqual({
highlighted: true, highlighted: true,
dimmed: false, dimmed: false,
}) })
assert.deepEqual(nodeState.get('model:gpt-4.1'), { expect(nodeState.get('model:gpt-4.1')).toEqual({
highlighted: true, highlighted: true,
dimmed: false, dimmed: false,
}) })
assert.deepEqual(nodeState.get('channel:101'), { expect(nodeState.get('channel:101')).toEqual({
highlighted: true, highlighted: true,
dimmed: false, dimmed: false,
}) })
assert.deepEqual(nodeState.get('user:2'), { expect(nodeState.get('user:2')).toEqual({
highlighted: false, highlighted: false,
dimmed: true, dimmed: true,
}) })
assert.deepEqual(linkState.get('user:1->node:node-a'), { expect(linkState.get('user:1->node:node-a')).toEqual({
highlighted: true, highlighted: true,
dimmed: false, dimmed: false,
}) })
assert.deepEqual(linkState.get('model:gpt-4.1->channel:101'), { expect(linkState.get('model:gpt-4.1->channel:101')).toEqual({
highlighted: true, highlighted: true,
dimmed: false, dimmed: false,
}) })
assert.deepEqual(linkState.get('model:claude-4-sonnet->channel:101'), { expect(linkState.get('model:claude-4-sonnet->channel:101')).toEqual({
highlighted: false, highlighted: false,
dimmed: true, 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, highlighted: true,
dimmed: false, dimmed: false,
}) })
assert.deepEqual(linkState.get('model:gpt-4.1->channel:102'), { expect(linkState.get('model:gpt-4.1->channel:102')).toEqual({
highlighted: false, highlighted: false,
dimmed: true, dimmed: true,
}) })
assert.deepEqual(nodeState.get('user:1'), { expect(nodeState.get('user:1')).toEqual({
highlighted: true, highlighted: true,
dimmed: false, dimmed: false,
}) })
assert.deepEqual(nodeState.get('node:node-a'), { expect(nodeState.get('node:node-a')).toEqual({
highlighted: true, highlighted: true,
dimmed: false, dimmed: false,
}) })
assert.deepEqual(nodeState.get('user:2'), { expect(nodeState.get('user:2')).toEqual({
highlighted: false, highlighted: false,
dimmed: true, dimmed: true,
}) })
@@ -686,11 +652,11 @@ describe('dashboard flow data', () => {
(link) => link.source === 'user:2' && link.target === 'group:vip' (link) => link.source === 'user:2' && link.target === 'group:vip'
) )
assert.equal(sharedLink?.value, 150) expect(sharedLink?.value).toBe(150)
assert.equal(sharedLink?.highlighted, true) expect(sharedLink?.highlighted).toBe(true)
assert.equal(sharedLink?.dimmed, false) expect(sharedLink?.dimmed).toBe(false)
assert.equal(inactiveUserLink?.highlighted, false) expect(inactiveUserLink?.highlighted).toBe(false)
assert.equal(inactiveUserLink?.dimmed, true) expect(inactiveUserLink?.dimmed).toBe(true)
}) })
test('does not emit highlight states without a visible active node', () => { 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' }, activeNode: { kind: 'user', id: 'user:1' },
}) })
assert.equal( expect(
withoutActive.flow.nodes.every( withoutActive.flow.nodes.every(
(node) => node.highlighted === undefined && node.dimmed === undefined (node) => node.highlighted === undefined && node.dimmed === undefined
), )
true ).toBe(true)
) expect(
assert.equal(
withoutActive.flow.links.every( withoutActive.flow.links.every(
(link) => link.highlighted === undefined && link.dimmed === undefined (link) => link.highlighted === undefined && link.dimmed === undefined
), )
true ).toBe(true)
) expect(
assert.equal(
hiddenActive.flow.nodes.every( hiddenActive.flow.nodes.every(
(node) => node.highlighted === undefined && node.dimmed === undefined (node) => node.highlighted === undefined && node.dimmed === undefined
), )
true ).toBe(true)
) expect(
assert.equal(
hiddenActive.flow.links.every( hiddenActive.flow.links.every(
(link) => link.highlighted === undefined && link.dimmed === undefined (link) => link.highlighted === undefined && link.dimmed === undefined
), )
true ).toBe(true)
)
}) })
test('builds Sankey spec with quota token request tooltips', () => { 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' link.source === 'user:1' && link.target === 'node:node-a'
) )
assert.equal(flowSpec.type, 'sankey') expect(flowSpec.type).toBe('sankey')
assert.equal(flowSpec.title.text, 'Flow') expect(flowSpec.title.text).toBe('Flow')
assert.deepEqual(flowSpec.emphasis, { enable: false }) expect(flowSpec.emphasis).toEqual({ enable: false })
assert.equal(flowSpec.tooltip.mark.visible({ datum: aliceNode }), true) expect(flowSpec.tooltip.mark.visible({ datum: aliceNode })).toBe(true)
assert.equal(flowSpec.tooltip.mark.visible({ datum: userNodeLink }), true) expect(flowSpec.tooltip.mark.visible({ datum: userNodeLink })).toBe(true)
assert.equal(flowSpec.animation, false) expect(flowSpec.animation).toBe(false)
assert.equal(values.nodes.length, 6) expect(values.nodes.length).toBe(6)
assert.equal(values.links.length, 5) expect(values.links.length).toBe(5)
assert.equal(aliceNode.name, 'alice') expect(aliceNode.name).toBe('alice')
assert.match(userNodeLink.linkColor, /^rgba\(/) expect(userNodeLink.linkColor).toMatch(/^rgba\(/)
const tooltipRows = flowSpec.tooltip.mark.content const tooltipRows = flowSpec.tooltip.mark.content
assert.deepEqual( expect(
tooltipRows tooltipRows
.filter((row: Record<string, unknown>) => .filter((row: Record<string, unknown>) =>
typeof row.visible === 'function' typeof row.visible === 'function'
@@ -767,14 +729,13 @@ describe('dashboard flow data', () => {
typeof row.value === 'function' typeof row.value === 'function'
? row.value({ datum: userNodeLink }) ? row.value({ datum: userNodeLink })
: row.value, : row.value,
]), ])
[ ).toEqual([
['Quota', '100'], ['Quota', '100'],
['Tokens', '40'], ['Tokens', '40'],
['Requests', '2'], ['Requests', '2'],
['Share', '100.0%'], ['Share', '100.0%'],
] ])
)
}) })
test('maps active flow highlight states into the Sankey spec', () => { 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 nodeOpacity = flowSpec.node.style.fillOpacity
const linkOpacity = flowSpec.link.style.fillOpacity const linkOpacity = flowSpec.link.style.fillOpacity
assert.deepEqual(flowSpec.emphasis, { enable: false }) expect(flowSpec.emphasis).toEqual({ enable: false })
assert.equal(aliceNode.highlighted, true) expect(aliceNode.highlighted).toBe(true)
assert.equal(bobNode.dimmed, true) expect(bobNode.dimmed).toBe(true)
assert.equal(highlightedLink.highlighted, true) expect(highlightedLink.highlighted).toBe(true)
assert.equal(dimmedLink.dimmed, true) expect(dimmedLink.dimmed).toBe(true)
assert.equal(nodeOpacity(aliceNode), 1) expect(nodeOpacity(aliceNode)).toBe(1)
assert.equal(nodeOpacity(bobNode), 0.18) expect(nodeOpacity(bobNode)).toBe(0.18)
assert.equal(linkOpacity(highlightedLink), 0.86) expect(linkOpacity(highlightedLink)).toBe(0.86)
assert.equal(linkOpacity(dimmedLink), 0.08) expect(linkOpacity(dimmedLink)).toBe(0.08)
assert.equal(highlightedLink.zIndex > dimmedLink.zIndex, true) expect(highlightedLink.zIndex > dimmedLink.zIndex).toBe(true)
}) })
}) })
@@ -16,39 +16,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { render } from '@testing-library/react'
import { after, describe, test } from 'node:test' 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 { createInstance } = await import('i18next')
const { I18nextProvider, initReactI18next } = await import('react-i18next') const { I18nextProvider, initReactI18next } = await import('react-i18next')
const { TooltipProvider } = await import('@/components/ui/tooltip') 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: { function CellHarness(props: {
group: string group: string
ratio?: number | string ratio?: number | string
@@ -96,141 +61,93 @@ function CellHarness(props: {
} }
describe('API key group table cell', () => { describe('API key group table cell', () => {
after(() => { test('renders an unclipped ring and a localized Auto ratio when API data uses a nonlocalized string', () => {
domWindow.close() const { container } = render(
}) <CellHarness
group='auto'
test('renders two unclipped rings and a localized Auto ratio when API data uses a nonlocalized string', async () => { ratio='自动'
const container = document.createElement('div') crossGroupRetry
document.body.append(container) shouldReduceMotion={false}
const root = createRoot(container) />
await act(async () =>
root.render(
<CellHarness
group='auto'
ratio='自动'
crossGroupRetry
shouldReduceMotion={false}
/>
)
) )
const badgeCell = container.querySelector<HTMLElement>( const badgeCell = container.querySelector<HTMLElement>(
'[data-api-key-group-cell="auto"]' '[data-api-key-group-cell="auto"]'
) )
assert.ok(badgeCell) expect(badgeCell).toHaveClass('overflow-visible')
assert.equal(badgeCell.classList.contains('overflow-visible'), true) expect(badgeCell).not.toHaveClass('overflow-hidden')
assert.equal(badgeCell.classList.contains('overflow-hidden'), false)
const frames = container.querySelectorAll('[data-auto-group-frame]') const frames = container.querySelectorAll('[data-auto-group-frame]')
const movingRings = container.querySelectorAll( const movingRings = container.querySelectorAll(
'[data-auto-group-flow-border]' '[data-auto-group-flow-border]'
) )
assert.equal(frames.length, 2) expect(frames.length).toBe(1)
assert.equal(movingRings.length, 2) expect(movingRings.length).toBe(1)
for (const frame of frames) { for (const frame of frames) {
assert.equal(frame.classList.contains('relative'), true) expect(frame).toHaveClass(
assert.equal(frame.classList.contains('overflow-visible'), true) 'relative',
assert.equal(frame.classList.contains('rounded-4xl'), true) 'overflow-visible',
assert.equal(frame.classList.contains('p-px'), true) 'rounded-4xl',
'p-px'
)
} }
const ratio = container.querySelector<HTMLElement>( const ratio = container.querySelector<HTMLElement>(
'[data-auto-group-effect="ratio"]' '[data-auto-group-effect="ratio"]'
) )
assert.ok(ratio) expect(ratio).toHaveTextContent('Auto Ratio')
assert.equal(ratio.textContent, 'Auto Ratio') expect(ratio).not.toHaveTextContent('x')
assert.equal(ratio.textContent?.includes('x'), false) expect(container).not.toHaveTextContent('自动')
assert.equal(container.textContent?.includes('自动'), false) expect(container).toHaveTextContent('Cross-group')
assert.equal(container.textContent?.includes('Cross-group'), true)
const crossGroupBadge = [ const crossGroupBadge = [
...container.querySelectorAll<HTMLElement>('[data-slot="status-badge"]'), ...container.querySelectorAll<HTMLElement>('[data-slot="status-badge"]'),
].find((badge) => badge.textContent === 'Cross-group') ].find((badge) => badge.textContent === 'Cross-group')
assert.ok(crossGroupBadge) expect(crossGroupBadge).not.toBeUndefined()
assert.equal(crossGroupBadge.closest('[data-auto-group-frame]'), null) expect(crossGroupBadge?.closest('[data-auto-group-frame]')).toBeNull()
await act(async () => root.unmount())
container.remove()
}) })
test('keeps static Auto frames but omits both moving layers for reduced motion', async () => { test('keeps the static Auto ratio frame but omits its moving layer for reduced motion', () => {
const container = document.createElement('div') const { container } = render(
document.body.append(container) <CellHarness group='auto' ratio='Auto' shouldReduceMotion />
const root = createRoot(container)
await act(async () =>
root.render(<CellHarness group='auto' ratio='Auto' shouldReduceMotion />)
) )
assert.equal( expect(container.querySelectorAll('[data-auto-group-frame]').length).toBe(1)
container.querySelectorAll('[data-auto-group-frame]').length, expect(
2 container.querySelectorAll('[data-auto-group-flow-border]').length
) ).toBe(0)
assert.equal(
container.querySelectorAll('[data-auto-group-flow-border]').length,
0
)
await act(async () => root.unmount())
container.remove()
}) })
test('shows only the Auto badge when ratio data is unavailable', async () => { test('shows only the cross-group badge when ratio data is unavailable', () => {
const container = document.createElement('div') const { container } = render(
document.body.append(container) <CellHarness group='auto' shouldReduceMotion={false} />
const root = createRoot(container)
await act(async () =>
root.render(<CellHarness group='auto' shouldReduceMotion={false} />)
) )
assert.equal( expect(container.querySelectorAll('[data-auto-group-frame]').length).toBe(0)
container.querySelectorAll('[data-auto-group-frame]').length, expect(
1 container.querySelectorAll('[data-auto-group-flow-border]').length
) ).toBe(0)
assert.equal( expect(container.querySelector('[data-auto-group-effect="ratio"]')).toBe(
container.querySelectorAll('[data-auto-group-flow-border]').length,
1
)
assert.equal(
container.querySelector('[data-auto-group-effect="ratio"]'),
null null
) )
assert.equal(container.textContent?.includes('Auto'), true) expect(container).toHaveTextContent('Cross-group')
assert.equal(container.textContent?.includes('Ratio'), false) expect(container).not.toHaveTextContent('Auto')
expect(container).not.toHaveTextContent('Ratio')
await act(async () => root.unmount())
container.remove()
}) })
test('narrows normal group ratios to numbers and never applies Auto rings', async () => { test('narrows normal group ratios to numbers and never applies Auto rings', () => {
const container = document.createElement('div') const { container, rerender } = render(
document.body.append(container) <CellHarness group='vip' ratio='自动' shouldReduceMotion={false} />
const root = createRoot(container)
await act(async () =>
root.render(
<CellHarness group='vip' ratio='自动' shouldReduceMotion={false} />
)
) )
assert.equal(container.textContent?.includes('vip'), true) expect(container).toHaveTextContent('vip')
assert.equal(container.textContent?.includes('自动'), false) expect(container).not.toHaveTextContent('自动')
assert.equal(container.querySelector('[data-auto-group-frame]'), null) expect(container.querySelector('[data-auto-group-frame]')).toBe(null)
assert.equal(container.querySelector('[data-auto-group-flow-border]'), null) expect(container.querySelector('[data-auto-group-flow-border]')).toBe(null)
await act(async () => rerender(<CellHarness group='vip' ratio={3} shouldReduceMotion={false} />)
root.render(
<CellHarness group='vip' ratio={3} shouldReduceMotion={false} />
)
)
assert.equal(container.textContent?.includes('3x'), true) expect(container).toHaveTextContent('3x')
assert.equal(container.querySelector('[data-auto-group-frame]'), null) expect(container.querySelector('[data-auto-group-frame]')).toBe(null)
await act(async () => root.unmount())
container.remove()
}) })
}) })
@@ -16,58 +16,26 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { fireEvent, render, screen, within } from '@testing-library/react'
import { after, describe, test } from 'node:test' 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],
})
}
let shouldReduceMotion = false let shouldReduceMotion = false
const reducedMotionMediaQuery = domWindow.matchMedia('(prefers-reduced-motion)') const reducedMotionMediaQuery = window.matchMedia('(prefers-reduced-motion)')
Object.defineProperty(reducedMotionMediaQuery, 'matches', { Object.defineProperty(reducedMotionMediaQuery, 'matches', {
configurable: true, configurable: true,
get: () => shouldReduceMotion, get: () => shouldReduceMotion,
}) })
Object.defineProperty(domWindow, 'matchMedia', { Object.defineProperty(window, 'matchMedia', {
configurable: true, configurable: true,
value: () => reducedMotionMediaQuery, value: () => reducedMotionMediaQuery,
}) })
function setReducedMotion(value: boolean) { function setReducedMotion(value: boolean) {
shouldReduceMotion = value shouldReduceMotion = value
reducedMotionMediaQuery.dispatchEvent(new domWindow.Event('change')) reducedMotionMediaQuery.dispatchEvent(new Event('change'))
} }
const { act, useState } = await import('react') const { useState } = await import('react')
const { createRoot } = await import('react-dom/client')
const { createInstance } = await import('i18next') const { createInstance } = await import('i18next')
const { I18nextProvider, initReactI18next } = await import('react-i18next') const { I18nextProvider, initReactI18next } = await import('react-i18next')
const { ApiKeyGroupCombobox } = await import('../api-key-group-combobox') 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 = [ const options = [
{ {
value: 'auto', value: 'auto',
@@ -119,176 +82,133 @@ function Harness(props: { initialValue: string }) {
) )
} }
function getTrigger(container: ParentNode): HTMLButtonElement { function getTrigger(): HTMLButtonElement {
const trigger = container.querySelector<HTMLButtonElement>( return screen.getByRole('combobox')
'button[role="combobox"]'
)
assert.ok(trigger)
return trigger
} }
function getCommandItem(label: string): HTMLElement { function getCommandItem(label: string): HTMLElement {
const item = [ const item = [
...document.querySelectorAll<HTMLElement>('[data-slot="command-item"]'), ...document.querySelectorAll<HTMLElement>('[data-slot="command-item"]'),
].find((candidate) => candidate.textContent?.includes(label)) ].find((candidate) => candidate.textContent?.includes(label))
assert.ok(item) if (!item) {
throw new Error(`Expected command item containing "${label}"`)
}
return item return item
} }
describe('API key group combobox Auto effect', () => { describe('API key group combobox Auto effect', () => {
after(() => { test('rings the selected Auto trigger and its localized ratio without rendering the API ratio text', () => {
domWindow.close()
})
test('rings the selected Auto trigger and its localized ratio without rendering the API ratio text', async () => {
setReducedMotion(false) setReducedMotion(false)
const container = document.createElement('div') render(<Harness initialValue='auto' />)
document.body.append(container)
const root = createRoot(container)
await act(async () => root.render(<Harness initialValue='auto' />)) const trigger = getTrigger()
expect(trigger).toHaveAttribute('aria-expanded', 'false')
const trigger = getTrigger(container) expect(trigger).toHaveAttribute('data-auto-group-effect', 'trigger')
assert.equal(trigger.getAttribute('aria-expanded'), 'false') expect(trigger).not.toHaveClass('bg-linear-to-r', 'overflow-hidden')
assert.equal(trigger.dataset.autoGroupEffect, 'trigger') expect(trigger).toHaveClass('overflow-visible')
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 triggerFlowBorder = trigger.querySelector<HTMLElement>( const triggerFlowBorder = trigger.querySelector<HTMLElement>(
'[data-auto-group-flow-border]' '[data-auto-group-flow-border]'
) )
assert.ok(triggerFlowBorder) expect(triggerFlowBorder).toHaveAttribute('aria-hidden', 'true')
assert.equal(triggerFlowBorder.getAttribute('aria-hidden'), 'true') expect(triggerFlowBorder).toHaveClass(
assert.equal( 'pointer-events-none',
triggerFlowBorder.classList.contains('pointer-events-none'), 'auto-group-flow-border'
true
)
assert.equal(
triggerFlowBorder.classList.contains('auto-group-flow-border'),
true
) )
const triggerRatio = trigger.querySelector<HTMLElement>( const triggerRatio = trigger.querySelector<HTMLElement>(
'[data-auto-group-effect="ratio"]' '[data-auto-group-effect="ratio"]'
) )
assert.ok(triggerRatio) expect(triggerRatio).toHaveTextContent('Auto Ratio')
assert.equal(triggerRatio.textContent, 'Auto Ratio') expect(triggerRatio).not.toHaveTextContent('x')
assert.equal(triggerRatio.textContent?.includes('Auto'), true) expect(trigger).not.toHaveTextContent('自动')
assert.equal(triggerRatio.textContent?.includes('x'), false) expect(triggerRatio).toHaveClass(
assert.equal(trigger.textContent?.includes('自动'), false) 'relative',
assert.equal(triggerRatio.classList.contains('relative'), true) 'overflow-visible',
assert.equal(triggerRatio.classList.contains('overflow-visible'), true) 'rounded-4xl'
assert.equal(triggerRatio.classList.contains('rounded-4xl'), true) )
assert.ok(triggerRatio.querySelector('[data-auto-group-flow-border]')) expect(
triggerRatio?.querySelector('[data-auto-group-flow-border]')
).toBeInTheDocument()
await act(async () => trigger.click()) fireEvent.click(trigger)
assert.equal(trigger.getAttribute('aria-expanded'), 'true') expect(trigger).toHaveAttribute('aria-expanded', 'true')
const autoOption = getCommandItem('Global automatic routing') const autoOption = getCommandItem('Global automatic routing')
assert.equal(autoOption.dataset.autoGroupEffect, 'option') expect(autoOption).toHaveAttribute('data-auto-group-effect', 'option')
assert.equal(autoOption.getAttribute('aria-selected'), 'true') expect(autoOption).toHaveAttribute('aria-selected', 'true')
assert.equal(autoOption.classList.contains('bg-linear-to-r'), false) expect(autoOption).not.toHaveClass('bg-linear-to-r')
assert.equal(autoOption.classList.contains('overflow-visible'), true) expect(autoOption).toHaveClass('overflow-visible')
assert.ok(autoOption.querySelector('[data-auto-group-flow-border]')) expect(
autoOption.querySelector('[data-auto-group-flow-border]')
).toBeInTheDocument()
const optionRatio = autoOption.querySelector<HTMLElement>( const optionRatio = autoOption.querySelector<HTMLElement>(
'[data-auto-group-effect="ratio"]' '[data-auto-group-effect="ratio"]'
) )
assert.ok(optionRatio) expect(optionRatio).toHaveTextContent('Auto Ratio')
assert.equal(optionRatio.textContent, 'Auto Ratio') expect(
assert.ok(optionRatio.querySelector('[data-auto-group-flow-border]')) optionRatio?.querySelector('[data-auto-group-flow-border]')
).toBeInTheDocument()
const defaultOption = getCommandItem('User group') const defaultOption = getCommandItem('User group')
assert.equal(defaultOption.hasAttribute('data-auto-group-effect'), false) expect(defaultOption).not.toHaveAttribute('data-auto-group-effect')
assert.equal( expect(defaultOption.querySelector('[data-auto-group-flow-border]')).toBe(
defaultOption.querySelector('[data-auto-group-flow-border]'),
null null
) )
assert.equal(defaultOption.textContent?.includes('1x Ratio'), true) expect(defaultOption).toHaveTextContent('1x Ratio')
assert.equal( expect(
defaultOption.querySelector('[data-auto-group-effect="ratio"]'), defaultOption.querySelector('[data-auto-group-effect="ratio"]')
null ).toBe(null)
)
await act(async () => root.unmount())
container.remove()
}) })
test('keeps search and selection behavior while leaving normal groups unstyled', async () => { test('keeps search and selection behavior while leaving normal groups unstyled', async () => {
setReducedMotion(false) setReducedMotion(false)
const container = document.createElement('div') const { container } = render(<Harness initialValue='auto' />)
document.body.append(container)
const root = createRoot(container)
await act(async () => root.render(<Harness initialValue='auto' />)) const trigger = getTrigger()
fireEvent.click(trigger)
const trigger = getTrigger(container) fireEvent.input(screen.getByPlaceholderText('Search...'), {
await act(async () => trigger.click()) target: { value: 'vip' },
const searchInput = document.querySelector<HTMLInputElement>(
'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
)
}) })
const visibleOptions = [ const visibleOptions = [
...document.querySelectorAll<HTMLElement>('[data-slot="command-item"]'), ...document.querySelectorAll<HTMLElement>('[data-slot="command-item"]'),
] ]
assert.equal( expect(
visibleOptions.some((option) => visibleOptions.some((option) =>
option.textContent?.includes('Global automatic routing') option.textContent?.includes('Global automatic routing')
), )
false ).toBe(false)
)
const vipOption = getCommandItem('Priority group') const vipOption = getCommandItem('Priority group')
await act(async () => vipOption.click()) fireEvent.click(vipOption)
assert.equal( expect(within(container).getByTestId('selected-group')).toHaveTextContent(
container.querySelector('[data-testid="selected-group"]')?.textContent,
'vip' 'vip'
) )
assert.equal(trigger.getAttribute('aria-expanded'), 'false') expect(trigger).toHaveAttribute('aria-expanded', 'false')
assert.equal(trigger.hasAttribute('data-auto-group-effect'), false) expect(trigger).not.toHaveAttribute('data-auto-group-effect')
assert.equal(trigger.querySelector('[data-auto-group-flow-border]'), null) expect(trigger.querySelector('[data-auto-group-flow-border]')).toBe(null)
await act(async () => root.unmount())
container.remove()
}) })
test('preserves the static Auto treatment but omits moving layers for reduced motion', async () => { test('preserves the static Auto treatment but omits moving layers for reduced motion', async () => {
setReducedMotion(true) setReducedMotion(true)
const container = document.createElement('div') render(<Harness initialValue='auto' />)
document.body.append(container)
const root = createRoot(container)
await act(async () => root.render(<Harness initialValue='auto' />)) 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) fireEvent.click(trigger)
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())
const autoOption = getCommandItem('Global automatic routing') const autoOption = getCommandItem('Global automatic routing')
assert.equal(autoOption.dataset.autoGroupEffect, 'option') expect(autoOption).toHaveAttribute('data-auto-group-effect', 'option')
assert.equal( expect(autoOption.querySelector('[data-auto-group-flow-border]')).toBe(null)
autoOption.querySelector('[data-auto-group-flow-border]'), expect(
null autoOption.querySelector('[data-auto-group-effect="ratio"]')
) ).toBeInTheDocument()
assert.ok(autoOption.querySelector('[data-auto-group-effect="ratio"]'))
await act(async () => root.unmount())
container.remove()
setReducedMotion(false) setReducedMotion(false)
}) })
}) })
@@ -16,45 +16,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { after, afterEach, describe, test } from 'node:test' 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 { createInstance } = await import('i18next')
const { I18nextProvider, initReactI18next } = await import('react-i18next') const { I18nextProvider, initReactI18next } = await import('react-i18next')
const { QueryClient, QueryClientProvider } = const { QueryClient, QueryClientProvider } =
@@ -69,20 +33,13 @@ await i18n.use(initReactI18next).init({
resources: { en: { translation: {} } }, 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 ApiMethod = (url: string, data?: unknown) => Promise<{ data: unknown }>
type MockableApi = { type MockableApi = {
get: ApiMethod get: ApiMethod
post: ApiMethod post: ApiMethod
} }
type RenderedDrawer = { type RenderedDrawer = {
host: HTMLDivElement
queryClient: InstanceType<typeof QueryClient> queryClient: InstanceType<typeof QueryClient>
root: ReturnType<typeof createRoot>
} }
const apiClient = api as unknown as MockableApi const apiClient = api as unknown as MockableApi
@@ -120,44 +77,14 @@ function installApiFixtures(createdPayloads: Array<Record<string, unknown>>) {
} }
} }
apiClient.post = async (url, data) => { apiClient.post = async (url, data) => {
assert.equal(url, '/api/token/') expect(url).toBe('/api/token/')
assert.ok(data && typeof data === 'object') expect(data && typeof data === 'object').toBeTruthy()
createdPayloads.push(data as Record<string, unknown>) createdPayloads.push(data as Record<string, unknown>)
return { data: { success: true, data: {} } } return { data: { success: true, data: {} } }
} }
} }
async function waitForCondition(
condition: () => boolean,
failureMessage: string
): Promise<void> {
if (condition()) return
await new Promise<void>((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<void> { async function renderCreateDrawer(): Promise<void> {
const host = document.createElement('div')
document.body.append(host)
const root = createRoot(host)
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } }, defaultOptions: { queries: { retry: false } },
}) })
@@ -192,43 +119,49 @@ async function renderCreateDrawer(): Promise<void> {
}, },
{ updatedAt: freshAt } { updatedAt: freshAt }
) )
renderedDrawer = { host, queryClient, root } renderedDrawer = { queryClient }
await act(async () => render(
root.render( <QueryClientProvider client={queryClient}>
<QueryClientProvider client={queryClient}> <I18nextProvider i18n={i18n}>
<I18nextProvider i18n={i18n}> <ApiKeysProvider>
<ApiKeysProvider> <ApiKeysMutateDrawer open onOpenChange={() => undefined} />
<ApiKeysMutateDrawer open onOpenChange={() => undefined} /> </ApiKeysProvider>
</ApiKeysProvider> </I18nextProvider>
</I18nextProvider> </QueryClientProvider>
</QueryClientProvider>
)
) )
await act(async () => await waitFor(
waitForCondition(() => { () => {
const saveButton = findButton('Save changes', false) const saveButton = findButton('Save changes', false)
return saveButton !== null && !saveButton.disabled expect(saveButton).toBeEnabled()
}, 'API key drawer did not finish initializing') },
{ timeout: 1500 }
) )
} }
function findButton(text: string, required: true): HTMLButtonElement function findButton(text: string, required: true): HTMLButtonElement
function findButton(text: string, required: false): HTMLButtonElement | null function findButton(text: string, required: false): HTMLButtonElement | null
function findButton(text: string, required = true): HTMLButtonElement | null { function findButton(text: string, required = true): HTMLButtonElement | null {
const button = [ const button = screen
...document.querySelectorAll<HTMLButtonElement>('button'), .queryAllByRole<HTMLButtonElement>('button')
].find((candidate) => candidate.textContent?.includes(text)) .find((candidate) => candidate.textContent?.includes(text))
if (required) assert.ok(button, `Expected button containing "${text}"`) if (required && !button) {
throw new Error(`Expected button containing "${text}"`)
}
return button ?? null return button ?? null
} }
function getControlByLabel<T extends HTMLElement>(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<HTMLLabelElement>('label')].find( const label = [...document.querySelectorAll<HTMLLabelElement>('label')].find(
(candidate) => candidate.textContent?.trim() === labelText (candidate) => candidate.textContent?.trim() === labelText
) )
assert.ok(label, `Expected label "${labelText}"`) if (!label) {
assert.ok(label.htmlFor) throw new Error(`Expected label "${labelText}"`)
}
const control = const control =
label.control ?? label.control ??
label label
@@ -236,51 +169,38 @@ function getControlByLabel<T extends HTMLElement>(labelText: string): T {
?.querySelector<HTMLElement>( ?.querySelector<HTMLElement>(
'[data-slot="form-control"], input, textarea, button[role="combobox"], [role="group"]' '[data-slot="form-control"], input, textarea, button[role="combobox"], [role="group"]'
) )
assert.ok(control) if (!control) {
return control as T throw new Error(`Expected control for label "${labelText}"`)
}
return control
} }
async function changeInput(input: HTMLInputElement, value: string) { function changeInput(input: HTMLInputElement, value: string): void {
await act(async () => { fireEvent.input(input, { target: { value } })
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
)
})
} }
async function selectComboboxOption( function selectComboboxOption(
trigger: HTMLButtonElement, trigger: HTMLButtonElement,
optionDescription: string optionDescription: string
) { ): void {
await act(async () => trigger.click()) fireEvent.click(trigger)
const option = [ const option = [
...document.querySelectorAll<HTMLElement>('[data-slot="command-item"]'), ...document.querySelectorAll<HTMLElement>('[data-slot="command-item"]'),
].find((candidate) => candidate.textContent?.includes(optionDescription)) ].find((candidate) => candidate.textContent?.includes(optionDescription))
assert.ok(option, `Expected option containing "${optionDescription}"`) if (!option) {
await act(async () => option.click()) throw new Error(`Expected option containing "${optionDescription}"`)
}
fireEvent.click(option)
} }
afterEach(async () => { afterEach(() => {
apiClient.get = originalGet apiClient.get = originalGet
apiClient.post = originalPost apiClient.post = originalPost
domWindow.localStorage.clear() localStorage.clear()
if (renderedDrawer) { if (renderedDrawer) {
await act(async () => renderedDrawer?.root.unmount())
renderedDrawer.queryClient.clear() renderedDrawer.queryClient.clear()
renderedDrawer.host.remove()
renderedDrawer = null renderedDrawer = null
} }
document.body.replaceChildren()
})
after(() => {
domWindow.close()
}) })
describe('API keys mutate drawer Auto group integration', () => { describe('API keys mutate drawer Auto group integration', () => {
@@ -289,38 +209,31 @@ describe('API keys mutate drawer Auto group integration', () => {
installApiFixtures(createdPayloads) installApiFixtures(createdPayloads)
await renderCreateDrawer() await renderCreateDrawer()
const groupTrigger = getControlByLabel<HTMLButtonElement>('Group') const groupTrigger = getControlByLabel('Group')
assert.equal(groupTrigger.textContent?.includes('auto'), true) expect(groupTrigger.textContent?.includes('auto')).toBe(true)
assert.equal( expect(
document.body.textContent?.includes( document.body.textContent?.includes(
'Using the complete global Auto order (2 groups)' 'Using the complete global Auto order (2 groups)'
), )
true ).toBe(true)
) expect(
assert.deepEqual(
[ [
...document.querySelectorAll('[data-slot="global-auto-order-name"]'), ...document.querySelectorAll('[data-slot="global-auto-order-name"]'),
].map((item) => item.textContent), ].map((item) => item.textContent)
['vip', 'default'] ).toEqual(['vip', 'default'])
) expect(findButton('Restore global Auto', true).disabled).toBe(true)
assert.equal(findButton('Restore global Auto', true).disabled, true)
await changeInput(getControlByLabel<HTMLInputElement>('Name'), 'batch') changeInput(getControlByLabel('Name'), 'batch')
await changeInput(getControlByLabel<HTMLInputElement>('Quantity'), '2') changeInput(getControlByLabel('Quantity'), '2')
await act(async () => findButton('Save changes', true).click()) fireEvent.click(findButton('Save changes', true))
await act(async () => await waitFor(() => expect(createdPayloads).toHaveLength(2))
waitForCondition(
() => createdPayloads.length === 2,
'batch API keys were not created'
)
)
assert.equal(createdPayloads.length, 2) expect(createdPayloads.length).toBe(2)
assert.equal(createdPayloads[0]?.name, 'batch') expect(createdPayloads[0]?.name).toBe('batch')
for (const payload of createdPayloads) { for (const payload of createdPayloads) {
assert.equal(payload.group, 'auto') expect(payload.group).toBe('auto')
assert.deepEqual(payload.auto_groups, []) expect(payload.auto_groups).toEqual([])
assert.equal(payload.cross_group_retry, true) expect(payload.cross_group_retry).toBe(true)
} }
}) })
@@ -329,43 +242,39 @@ describe('API keys mutate drawer Auto group integration', () => {
installApiFixtures(createdPayloads) installApiFixtures(createdPayloads)
await renderCreateDrawer() await renderCreateDrawer()
const autoOrderControl = getControlByLabel<HTMLElement>('Auto group order') const autoOrderControl = getControlByLabel('Auto group order')
const addGroupTrigger = autoOrderControl.querySelector<HTMLButtonElement>( const addGroupTrigger = autoOrderControl.querySelector<HTMLButtonElement>(
'button[role="combobox"]' 'button[role="combobox"]'
) )
assert.ok(addGroupTrigger) if (!addGroupTrigger) {
await selectComboboxOption(addGroupTrigger, 'Priority access') throw new Error('Expected Auto group order combobox')
}
selectComboboxOption(addGroupTrigger, 'Priority access')
assert.ok(document.querySelector('button[aria-label="Remove vip"]')) expect(
assert.equal( document.querySelector('button[aria-label="Remove vip"]')
document.body.textContent?.includes('1 / 3 groups selected'), ).toBeTruthy()
expect(document.body.textContent?.includes('1 / 3 groups selected')).toBe(
true true
) )
assert.equal(findButton('Restore global Auto', true).disabled, false) expect(findButton('Restore global Auto', true).disabled).toBe(false)
const groupTrigger = getControlByLabel<HTMLButtonElement>('Group') const groupTrigger = getControlByLabel('Group')
await selectComboboxOption(groupTrigger, 'Standard access') selectComboboxOption(groupTrigger, 'Standard access')
assert.equal( expect(document.querySelector('button[aria-label="Remove vip"]')).toBe(null)
document.querySelector('button[aria-label="Remove vip"]'), selectComboboxOption(groupTrigger, 'Automatic routing')
null
)
await selectComboboxOption(groupTrigger, 'Automatic routing')
assert.ok(document.querySelector('button[aria-label="Remove vip"]')) expect(
assert.equal( document.querySelector('button[aria-label="Remove vip"]')
document.body.textContent?.includes('1 / 3 groups selected'), ).toBeTruthy()
expect(document.body.textContent?.includes('1 / 3 groups selected')).toBe(
true true
) )
assert.equal(findButton('Restore global Auto', true).disabled, false) expect(findButton('Restore global Auto', true).disabled).toBe(false)
await changeInput(getControlByLabel<HTMLInputElement>('Name'), 'custom') changeInput(getControlByLabel('Name'), 'custom')
await act(async () => findButton('Save changes', true).click()) fireEvent.click(findButton('Save changes', true))
await act(async () => await waitFor(() => expect(createdPayloads).toHaveLength(1))
waitForCondition( expect(createdPayloads[0]?.auto_groups).toEqual(['vip'])
() => createdPayloads.length === 1,
'custom-order API key was not created'
)
)
assert.deepEqual(createdPayloads[0]?.auto_groups, ['vip'])
}) })
}) })
@@ -16,42 +16,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { fireEvent, render, within } from '@testing-library/react'
import { after, describe, test } from 'node:test' import { describe, expect, test } from 'vitest'
import { Window } from 'happy-dom' const { useState } = await import('react')
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 { createInstance } = await import('i18next') const { createInstance } = await import('i18next')
const { I18nextProvider, initReactI18next } = await import('react-i18next') const { I18nextProvider, initReactI18next } = await import('react-i18next')
const { AutoGroupOrderEditor } = await import('../auto-group-order-editor') 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 = [ const globalOptions = [
{ value: 'vip', label: 'VIP', desc: 'Priority access', ratio: 3 }, { value: 'vip', label: 'VIP', desc: 'Priority access', ratio: 3 },
{ value: 'default', label: 'Default', desc: 'Standard access', ratio: 1 }, { value: 'default', label: 'Default', desc: 'Standard access', ratio: 1 },
@@ -176,163 +139,104 @@ function CustomEmptyHarness() {
) )
} }
function findButton(container: ParentNode, label: string): HTMLButtonElement { function findButton(container: HTMLElement, label: string): HTMLButtonElement {
const button = container.querySelector<HTMLButtonElement>( return within(container).getByRole('button', { name: label })
`button[aria-label="${label}"]` }
)
assert.ok(button) function getCommandItem(label: string): HTMLElement {
return button const item = [
...document.querySelectorAll<HTMLElement>('[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', () => { describe('Auto group order editor', () => {
after(() => { test('enforces the limit and exposes accessible reorder controls', () => {
domWindow.close() const { container } = render(<Harness />)
})
test('enforces the limit and exposes accessible reorder controls', async () => { const addButton = within(container).getByRole('combobox')
const container = document.createElement('div') expect(addButton).toBeDisabled()
document.body.append(container) expect(container).toHaveTextContent('2 / 2 groups selected')
const root = createRoot(container) 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(<Harness />)) fireEvent.click(findButton(container, 'Move default down'))
expect(within(container).getByTestId('order')).toHaveTextContent(
const addButton = container.querySelector<HTMLButtonElement>(
'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,
'vip,default' 'vip,default'
) )
await act(async () => { fireEvent.keyDown(findButton(container, 'Drag vip to reorder'), {
findButton(container, 'Drag vip to reorder').dispatchEvent( key: 'ArrowDown',
new domWindow.KeyboardEvent('keydown', {
key: 'ArrowDown',
bubbles: true,
}) as unknown as KeyboardEvent
)
}) })
assert.equal( expect(within(container).getByTestId('order')).toHaveTextContent(
container.querySelector('[data-testid="order"]')?.textContent,
'default,vip' 'default,vip'
) )
await act(async () => root.unmount())
container.remove()
}) })
test('adds and removes groups, then restores inheritance as an empty value', async () => { test('adds and removes groups, then restores inheritance as an empty value', () => {
const container = document.createElement('div') const { container } = render(<Harness />)
document.body.append(container) fireEvent.click(findButton(container, 'Remove vip'))
const root = createRoot(container)
await act(async () => root.render(<Harness />)) expect(within(container).getByTestId('order')).toHaveTextContent('default')
await act(async () => findButton(container, 'Remove vip').click()) const addButton = within(container).getByRole('combobox')
expect(addButton).toBeEnabled()
assert.equal( fireEvent.click(addButton)
container.querySelector('[data-testid="order"]')?.textContent, fireEvent.click(getCommandItem('team'))
'default' expect(within(container).getByTestId('order')).toHaveTextContent(
)
const addButton = container.querySelector<HTMLButtonElement>(
'button[role="combobox"]'
)
assert.ok(addButton)
assert.equal(addButton.disabled, false)
await act(async () => addButton.click())
const teamOption = [
...document.querySelectorAll<HTMLElement>('[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,
'default,team' 'default,team'
) )
assert.equal(addButton.disabled, true) expect(addButton).toBeDisabled()
const restoreButton = [...container.querySelectorAll('button')].find( fireEvent.click(
(button) => button.textContent?.includes('Restore global Auto') within(container).getByRole('button', { name: 'Restore global Auto' })
) )
assert.ok(restoreButton)
await act(async () => restoreButton.click())
assert.equal( expect(within(container).getByTestId('order')).toBeEmptyDOMElement()
container.querySelector('[data-testid="order"]')?.textContent, expect(within(container).getByTestId('mode')).toHaveTextContent('inherit')
'' expect(container).toHaveTextContent(
) 'Using the complete global Auto order (3 groups)'
assert.equal(
container.querySelector('[data-testid="mode"]')?.textContent,
'inherit'
)
assert.equal(
container.textContent?.includes(
'Using the complete global Auto order (3 groups)'
),
true
) )
const inheritedItems = container.querySelectorAll( const inheritedItems = container.querySelectorAll(
'[data-slot="global-auto-order"] > li' '[data-slot="global-auto-order"] > li'
) )
assert.deepEqual( expect(
[...inheritedItems].map( [...inheritedItems].map(
(item) => (item) =>
item.querySelector('[data-slot="global-auto-order-name"]') item.querySelector('[data-slot="global-auto-order-name"]')
?.textContent ?.textContent
), )
['VIP', 'Default', 'Team'] ).toEqual(['VIP', 'Default', 'Team'])
)
await act(async () => root.unmount())
container.remove()
}) })
test('shows the complete inherited order with metadata beyond the custom limit', async () => { test('shows the complete inherited order with metadata beyond the custom limit', () => {
const container = document.createElement('div') const { container } = render(<InheritanceHarness />)
document.body.append(container)
const root = createRoot(container)
await act(async () => root.render(<InheritanceHarness />)) expect(container).toHaveTextContent(
'Using the complete global Auto order (3 groups)'
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).not.toHaveTextContent('0 / 2 groups selected')
const order = container.querySelector<HTMLOListElement>( const order = container.querySelector<HTMLOListElement>(
'[data-slot="global-auto-order"]' '[data-slot="global-auto-order"]'
) )
assert.ok(order) if (!order) {
assert.equal(order.classList.contains('overflow-y-auto'), true) throw new Error('Expected inherited Auto group order')
assert.equal(order.classList.contains('flex-wrap'), true) }
expect(order).toHaveClass('overflow-y-auto', 'flex-wrap')
const items = [...order.querySelectorAll('li')] const items = [...order.querySelectorAll('li')]
assert.equal(items.length, 3) expect(items.length).toBe(3)
assert.equal( expect(
order.querySelectorAll('[data-slot="global-auto-order-connector"]') order.querySelectorAll('[data-slot="global-auto-order-connector"]').length
.length, ).toBe(2)
2 expect(
)
assert.deepEqual(
items.map((item) => ({ items.map((item) => ({
index: item.querySelector('[data-slot="global-auto-order-index"]') index: item.querySelector('[data-slot="global-auto-order-index"]')
?.textContent, ?.textContent,
@@ -345,196 +249,117 @@ describe('Auto group order editor', () => {
'[data-slot="global-auto-order-description"]' '[data-slot="global-auto-order-description"]'
)?.textContent, )?.textContent,
ratio: item.querySelector('[data-slot="badge"]')?.textContent, ratio: item.querySelector('[data-slot="badge"]')?.textContent,
})), }))
[ ).toEqual([
{ {
index: '1', index: '1',
name: 'VIP', name: 'VIP',
title: 'Priority access', title: 'Priority access',
description: 'Priority access', description: 'Priority access',
ratio: '3x Ratio', ratio: '3x Ratio',
}, },
{ {
index: '2', index: '2',
name: 'Default', name: 'Default',
title: 'Standard access', title: 'Standard access',
description: 'Standard access', description: 'Standard access',
ratio: '1x Ratio', ratio: '1x Ratio',
}, },
{ {
index: '3', index: '3',
name: 'Team', name: 'Team',
title: 'Shared access', title: 'Shared access',
description: 'Shared access', description: 'Shared access',
ratio: '2x Ratio', ratio: '2x Ratio',
}, },
] ])
)
for (const item of items) { for (const item of items) {
const chip = item.querySelector('[data-slot="global-auto-order-chip"]') const chip = item.querySelector('[data-slot="global-auto-order-chip"]')
assert.ok(chip) expect(chip).toBeInTheDocument()
const description = item.querySelector( const description = item.querySelector(
'[data-slot="global-auto-order-description"]' '[data-slot="global-auto-order-description"]'
) )
assert.ok(description) expect(description).toHaveClass('sr-only')
assert.equal(description.classList.contains('sr-only'), true)
} }
assert.equal( expect(
items[0]?.querySelector('[data-slot="global-auto-order-connector"]'), items[0]?.querySelector('[data-slot="global-auto-order-connector"]')
null ).toBe(null)
)
for (const item of items.slice(1)) { for (const item of items.slice(1)) {
const connector = item.querySelector( const connector = item.querySelector(
'[data-slot="global-auto-order-connector"]' '[data-slot="global-auto-order-connector"]'
) )
assert.ok(connector) expect(connector).toHaveAttribute('aria-hidden', 'true')
assert.equal(connector.getAttribute('aria-hidden'), 'true')
} }
assert.equal(container.querySelector('[aria-label^="Drag "]'), null) expect(container.querySelector('[aria-label^="Drag "]')).toBe(null)
assert.equal(container.querySelector('[aria-label^="Move "]'), null) expect(container.querySelector('[aria-label^="Move "]')).toBe(null)
assert.equal(container.querySelector('[aria-label^="Remove "]'), null) expect(container.querySelector('[aria-label^="Remove "]')).toBe(null)
const restoreButton = [...container.querySelectorAll('button')].find( expect(
(button) => button.textContent?.includes('Restore global Auto') within(container).getByRole('button', { name: 'Restore global Auto' })
) ).toBeDisabled()
assert.ok(restoreButton)
assert.equal(restoreButton.disabled, true)
await act(async () => root.unmount())
container.remove()
}) })
test('shows an explicit empty state when the global Auto order has no groups', async () => { test('shows an explicit empty state when the global Auto order has no groups', () => {
const container = document.createElement('div') const { container } = render(<InheritanceHarness globalOptions={[]} />)
document.body.append(container)
const root = createRoot(container)
await act(async () => expect(container).toHaveTextContent(
root.render(<InheritanceHarness globalOptions={[]} />) '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( test('keeps an empty custom order distinct from global inheritance', () => {
container.textContent?.includes( const { container } = render(<CustomEmptyHarness />)
'Using the complete global Auto order (0 groups)'
), expect(within(container).getByTestId('mode')).toHaveTextContent('custom')
true expect(container).toHaveTextContent(
'No valid custom Auto groups remain. Add a group or restore global Auto.'
) )
assert.equal( expect(container.querySelector('[data-slot="global-auto-order"]')).toBe(
container.textContent?.includes(
'No available groups in the global Auto order.'
),
true
)
assert.equal(
container.querySelector('[data-slot="global-auto-order"]'),
null null
) )
await act(async () => root.unmount()) const restoreButton = within(container).getByRole('button', {
container.remove() 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 () => { test('adding a group from inheritance explicitly creates a custom order', () => {
const container = document.createElement('div') const { container } = render(<InheritanceHarness />)
document.body.append(container)
const root = createRoot(container)
await act(async () => root.render(<CustomEmptyHarness />)) fireEvent.click(within(container).getByRole('combobox'))
fireEvent.click(getCommandItem('VIP'))
assert.equal( expect(within(container).getByTestId('mode')).toHaveTextContent('custom')
container.querySelector('[data-testid="mode"]')?.textContent, expect(within(container).getByTestId('order')).toHaveTextContent('vip')
'custom' expect(container.querySelector('[data-slot="global-auto-order"]')).toBe(
)
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"]'),
null 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 () => { test('removing the last custom group does not silently enable inheritance', () => {
const container = document.createElement('div') const { container } = render(<Harness initialGroups={['default']} />)
document.body.append(container) fireEvent.click(findButton(container, 'Remove default'))
const root = createRoot(container)
await act(async () => root.render(<InheritanceHarness />)) expect(within(container).getByTestId('order')).toBeEmptyDOMElement()
expect(within(container).getByTestId('mode')).toHaveTextContent('custom')
const addButton = container.querySelector<HTMLButtonElement>( expect(container).toHaveTextContent(
'button[role="combobox"]' '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<HTMLElement>('[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(<Harness initialGroups={['default']} />))
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()
}) })
}) })
@@ -16,10 +16,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com 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 type { TFunction } from 'i18next'
import { describe, expect, test } from 'vitest'
import { apiKeySchema, type ApiKey } from '../../types' import { apiKeySchema, type ApiKey } from '../../types'
import { import {
@@ -60,16 +58,16 @@ describe('API key Auto group form mapping', () => {
const legacyApiKey: Record<string, unknown> = { ...baseApiKey } const legacyApiKey: Record<string, unknown> = { ...baseApiKey }
delete legacyApiKey.auto_groups 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', () => { test('creates an Auto token that inherits the global order', () => {
const defaults = getApiKeyFormDefaultValues(true) const defaults = getApiKeyFormDefaultValues(true)
assert.equal(defaults.group, 'auto') expect(defaults.group).toBe('auto')
assert.equal(defaults.auto_groups_mode, 'inherit') expect(defaults.auto_groups_mode).toBe('inherit')
assert.deepEqual(defaults.auto_groups, []) expect(defaults.auto_groups).toEqual([])
assert.deepEqual(transformFormDataToPayload(defaults).auto_groups, []) expect(transformFormDataToPayload(defaults).auto_groups).toEqual([])
}) })
test('maps omitted, null, and empty snapshots to inheritance on edit', () => { test('maps omitted, null, and empty snapshots to inheritance on edit', () => {
@@ -88,8 +86,8 @@ describe('API key Auto group form mapping', () => {
2 2
) )
assert.equal(defaults.auto_groups_mode, 'inherit') expect(defaults.auto_groups_mode).toBe('inherit')
assert.deepEqual(defaults.auto_groups, []) expect(defaults.auto_groups).toEqual([])
} }
}) })
@@ -103,8 +101,8 @@ describe('API key Auto group form mapping', () => {
2 2
) )
assert.equal(defaults.auto_groups_mode, 'custom') expect(defaults.auto_groups_mode).toBe('custom')
assert.deepEqual(defaults.auto_groups, ['vip', 'default']) expect(defaults.auto_groups).toEqual(['vip', 'default'])
}) })
test('keeps a fully filtered snapshot custom and rejects it until resolved', () => { test('keeps a fully filtered snapshot custom and rejects it until resolved', () => {
@@ -114,15 +112,14 @@ describe('API key Auto group form mapping', () => {
2 2
) )
assert.equal(defaults.auto_groups_mode, 'custom') expect(defaults.auto_groups_mode).toBe('custom')
assert.deepEqual(defaults.auto_groups, []) expect(defaults.auto_groups).toEqual([])
const result = getApiKeyFormSchema(t, 2).safeParse(defaults) const result = getApiKeyFormSchema(t, 2).safeParse(defaults)
assert.equal(result.success, false) expect(result.success).toBe(false)
if (result.success) return if (result.success) return
assert.deepEqual(result.error.issues[0]?.path, ['auto_groups']) expect(result.error.issues[0]?.path).toEqual(['auto_groups'])
assert.equal( expect(result.error.issues[0]?.message).toBe(
result.error.issues[0]?.message,
'Select at least one Auto group or restore global Auto.' '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'], auto_groups: ['vip', 'default'],
} }
assert.deepEqual(transformFormDataToPayload(custom).auto_groups, [ expect(transformFormDataToPayload(custom).auto_groups).toEqual([
'vip', 'vip',
'default', 'default',
]) ])
@@ -142,7 +139,7 @@ describe('API key Auto group form mapping', () => {
test('submits an empty array for inheritance and for non-Auto groups', () => { test('submits an empty array for inheritance and for non-Auto groups', () => {
const inherited = getApiKeyFormDefaultValues(true) const inherited = getApiKeyFormDefaultValues(true)
assert.deepEqual(transformFormDataToPayload(inherited).auto_groups, []) expect(transformFormDataToPayload(inherited).auto_groups).toEqual([])
const nonAuto = { const nonAuto = {
...inherited, ...inherited,
@@ -150,8 +147,8 @@ describe('API key Auto group form mapping', () => {
auto_groups_mode: 'custom' as const, auto_groups_mode: 'custom' as const,
auto_groups: ['vip'], auto_groups: ['vip'],
} }
assert.deepEqual(transformFormDataToPayload(nonAuto).auto_groups, []) expect(transformFormDataToPayload(nonAuto).auto_groups).toEqual([])
assert.equal(transformFormDataToPayload(nonAuto).cross_group_retry, false) expect(transformFormDataToPayload(nonAuto).cross_group_retry).toBe(false)
}) })
test('rejects snapshots over the configured limit', () => { test('rejects snapshots over the configured limit', () => {
@@ -162,13 +159,10 @@ describe('API key Auto group form mapping', () => {
auto_groups: ['default', 'vip'], auto_groups: ['default', 'vip'],
}) })
assert.equal(result.success, false) expect(result.success).toBe(false)
if (result.success) return if (result.success) return
assert.equal(result.error.issues[0]?.path[0], 'auto_groups') expect(result.error.issues[0]?.path[0]).toBe('auto_groups')
assert.equal( expect(result.error.issues[0]?.message).toBe('Select at most 1 Auto groups')
result.error.issues[0]?.message,
'Select at most 1 Auto groups'
)
}) })
test('rejects duplicate custom groups', () => { test('rejects duplicate custom groups', () => {
@@ -179,10 +173,9 @@ describe('API key Auto group form mapping', () => {
auto_groups: ['vip', 'vip'], auto_groups: ['vip', 'vip'],
}) })
assert.equal(result.success, false) expect(result.success).toBe(false)
if (result.success) return if (result.success) return
assert.equal( expect(result.error.issues[0]?.message).toBe(
result.error.issues[0]?.message,
'Auto groups must not contain duplicates' 'Auto groups must not contain duplicates'
) )
}) })
@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import type { ChatCompletionRequest } from '../types' import type { ChatCompletionRequest } from '../types'
import { createStreamRequestController } from './use-stream-request' import { createStreamRequestController } from './use-stream-request'
@@ -103,12 +102,12 @@ describe('latest-wins stream request coordination', () => {
const second = controller.send(payload, noopCallbacks) const second = controller.send(payload, noopCallbacks)
firstHeaders.resolve({ Authorization: 'Bearer stale' }) firstHeaders.resolve({ Authorization: 'Bearer stale' })
await first await first
assert.equal(sources.length, 0) expect(sources.length).toBe(0)
secondHeaders.resolve({ Authorization: 'Bearer current' }) secondHeaders.resolve({ Authorization: 'Bearer current' })
await second await second
assert.equal(sources.length, 1) expect(sources.length).toBe(1)
assert.equal(sources[0]?.streamed, true) expect(sources[0]?.streamed).toBe(true)
}) })
test('stop cancels a request that is still waiting for headers', async () => { 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' }) headers.resolve({ Authorization: 'Bearer ignored' })
await request await request
assert.equal(sourceCount, 0) expect(sourceCount).toBe(0)
}) })
test('dispose cancels a pending header request without a state update', async () => { 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' }) headers.resolve({ Authorization: 'Bearer ignored' })
await request await request
assert.equal(sourceCount, 0) expect(sourceCount).toBe(0)
assert.deepEqual(streamingStates, [false]) expect(streamingStates).toEqual([false])
}) })
test('closes the previous source and ignores all of its later events', async () => { 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) await controller.send(payload, callbacks)
const second = 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( sources[0]?.emit(
'message', 'message',
JSON.stringify({ choices: [{ delta: { content: 'stale' } }] }) JSON.stringify({ choices: [{ delta: { content: 'stale' } }] })
@@ -195,6 +194,6 @@ describe('latest-wins stream request coordination', () => {
JSON.stringify({ choices: [{ delta: { content: 'current' } }] }) JSON.stringify({ choices: [{ delta: { content: 'current' } }] })
) )
assert.deepEqual(updates, ['current']) expect(updates).toEqual(['current'])
}) })
}) })
@@ -1,3 +1,4 @@
import type { TFunction } from 'i18next'
/* /*
Copyright (C) 2023-2026 QuantumNous Copyright (C) 2023-2026 QuantumNous
@@ -16,10 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import type { TFunction } from 'i18next'
import { loginMethodLabel, sessionDevice } from '../login-session-utils' import { loginMethodLabel, sessionDevice } from '../login-session-utils'
@@ -27,14 +25,10 @@ const translate = ((key: string) => key) as TFunction
describe('login session presentation', () => { describe('login session presentation', () => {
test('labels built-in and provider OAuth login methods', () => { test('labels built-in and provider OAuth login methods', () => {
assert.equal(loginMethodLabel('password', translate), 'Password') expect(loginMethodLabel('password', translate)).toBe('Password')
assert.equal( expect(loginMethodLabel('2fa', translate)).toBe('Two-factor Authentication')
loginMethodLabel('2fa', translate), expect(loginMethodLabel('oauth:github', translate)).toBe('OAuth · GitHub')
'Two-factor Authentication' expect(loginMethodLabel('oauth:custom-provider', translate)).toBe(
)
assert.equal(loginMethodLabel('oauth:github', translate), 'OAuth · GitHub')
assert.equal(
loginMethodLabel('oauth:custom-provider', translate),
'OAuth · custom-provider' 'OAuth · custom-provider'
) )
}) })
@@ -43,8 +37,7 @@ describe('login session presentation', () => {
const userAgent = 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' '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( expect(sessionDevice(userAgent, 'Unknown device', 'Browser')).toBe(
sessionDevice(userAgent, 'Unknown device', 'Browser'),
'Safari · iOS' 'Safari · iOS'
) )
}) })
@@ -53,8 +46,7 @@ describe('login session presentation', () => {
const userAgent = 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' '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( expect(sessionDevice(userAgent, 'Unknown device', 'Browser', 5)).toBe(
sessionDevice(userAgent, 'Unknown device', 'Browser', 5),
'Safari · iOS' 'Safari · iOS'
) )
}) })
@@ -63,8 +55,7 @@ describe('login session presentation', () => {
const userAgent = 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' '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( expect(sessionDevice(userAgent, 'Unknown device', 'Browser', 10)).toBe(
sessionDevice(userAgent, 'Unknown device', 'Browser', 10),
'Chrome · Windows' 'Chrome · Windows'
) )
}) })
@@ -73,8 +64,7 @@ describe('login session presentation', () => {
const userAgent = 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' '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( expect(sessionDevice(userAgent, 'Unknown device', 'Browser', 5)).toBe(
sessionDevice(userAgent, 'Unknown device', 'Browser', 5),
'Chrome · Android' 'Chrome · Android'
) )
}) })
@@ -83,15 +73,13 @@ describe('login session presentation', () => {
const userAgent = 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' '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( expect(sessionDevice(userAgent, 'Unknown device', 'Browser')).toBe(
sessionDevice(userAgent, 'Unknown device', 'Browser'),
'Safari · macOS' 'Safari · macOS'
) )
}) })
test('falls back to the unknown-device label for an empty user agent', () => { test('falls back to the unknown-device label for an empty user agent', () => {
assert.equal( expect(sessionDevice('', 'Unknown device', 'Browser')).toBe(
sessionDevice('', 'Unknown device', 'Browser'),
'Unknown device' 'Unknown device'
) )
}) })
@@ -16,56 +16,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import {
fireEvent,
import { Window } from 'happy-dom' render,
screen,
waitFor,
type RenderResult,
} from '@testing-library/react'
import { afterEach, describe, expect, test } from 'vitest'
import type { Redemption } from '../../types' 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 i18n = (await import('i18next')).default
const { I18nextProvider, initReactI18next } = await import('react-i18next') const { I18nextProvider, initReactI18next } = await import('react-i18next')
const { Toaster, toast } = await import('sonner') 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 ApiMethod = (url: string, data?: unknown) => Promise<{ data: unknown }>
type MockableApi = { type MockableApi = {
get: ApiMethod get: ApiMethod
put: ApiMethod put: ApiMethod
} }
type RenderedDrawer = { type RenderedDrawer = {
host: HTMLDivElement result: RenderResult
root: ReturnType<typeof createRoot>
} }
type CurrencyFixture = { type CurrencyFixture = {
quotaDisplayType: 'USD' | 'CNY' quotaDisplayType: 'USD' | 'CNY'
@@ -171,269 +126,189 @@ async function renderDrawer(
}, },
}) })
const host = document.createElement('div') renderedDrawer = { result: render(drawerTree(currentRow)) }
document.body.append(host)
const root = createRoot(host)
renderedDrawer = { host, root }
await act(async () => root.render(drawerTree(currentRow)))
} }
async function rerenderDrawer(currentRow: Redemption): Promise<void> { async function rerenderDrawer(currentRow: Redemption): Promise<void> {
assert.ok(renderedDrawer) if (!renderedDrawer) {
await act(async () => renderedDrawer?.root.render(drawerTree(currentRow))) throw new Error('Expected a rendered redemption drawer')
} }
renderedDrawer.result.rerender(drawerTree(currentRow))
async function waitForCondition(
condition: () => boolean,
failureMessage: string
): Promise<void> {
if (condition()) return
await new Promise<void>((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,
})
})
} }
function getSaveButton(): HTMLButtonElement { function getSaveButton(): HTMLButtonElement {
const button = document.querySelector<HTMLButtonElement>( return screen.getByRole('button', { name: 'Save changes' })
'button[form="redemption-form"][type="submit"]'
)
assert.ok(button)
return button
} }
function getControlByLabel<T extends HTMLElement>(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<HTMLLabelElement>('label')].find( const label = [...document.querySelectorAll<HTMLLabelElement>('label')].find(
(candidate) => candidate.textContent?.trim() === labelText (candidate) => candidate.textContent?.trim() === labelText
) )
assert.ok(label, `Expected label "${labelText}"`) if (!label) {
assert.ok(label.htmlFor) throw new Error(`Expected label "${labelText}"`)
}
const control = const control =
label.control ?? label.control ??
label label
.closest('[data-slot="form-item"]') .closest('[data-slot="form-item"]')
?.querySelector<HTMLElement>('[data-slot="form-control"], input') ?.querySelector<HTMLElement>('[data-slot="form-control"], input')
assert.ok(control) if (!control) {
return control as T throw new Error(`Expected control for label "${labelText}"`)
}
return control
} }
async function changeInput(input: HTMLInputElement, value: string) { function changeInput(input: HTMLInputElement, value: string): void {
await act(async () => { fireEvent.input(input, { target: { value } })
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
)
})
} }
async function submitForm(): Promise<void> { function submitForm(): void {
const form = document.querySelector<HTMLFormElement>('#redemption-form') const form = document.querySelector<HTMLFormElement>('#redemption-form')
assert.ok(form) if (!form) {
await act(async () => throw new Error('Expected redemption form')
form.dispatchEvent( }
new domWindow.Event('submit', { fireEvent.submit(form)
bubbles: true,
cancelable: true,
}) as unknown as Event
)
)
} }
async function waitForLoadedForm(): Promise<void> { async function waitForLoadedForm(): Promise<void> {
await act(async () => await waitFor(() => expect(getSaveButton()).toBeEnabled())
waitForCondition(() => {
const saveButton = getSaveButton()
return (
saveButton.textContent?.includes('Save changes') === true &&
!saveButton.disabled
)
}, 'redemption drawer did not finish loading')
)
} }
afterEach(async () => { afterEach(() => {
apiClient.get = originalGet apiClient.get = originalGet
apiClient.put = originalPut apiClient.put = originalPut
Reflect.set(console, 'log', originalConsoleLog) Reflect.set(console, 'log', originalConsoleLog)
toast.dismiss() toast.dismiss()
domWindow.localStorage.clear() localStorage.clear()
if (renderedDrawer) { renderedDrawer = null
await act(async () => renderedDrawer?.root.unmount())
renderedDrawer.host.remove()
renderedDrawer = null
}
document.body.replaceChildren()
}) })
afterAll(() => { describe('redemption drawer', () => {
domWindow.close() 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 () => { await renderDrawer(original, {
const original = redemption(1, 13888889) quotaDisplayType: 'CNY',
apiClient.get = async () => ({ data: { success: true, data: original } }) usdExchangeRate: 7.2,
})
await waitForLoadedForm()
await renderDrawer(original, { expect(getControlByLabel('Quota (CNY)').value).toBe('200')
quotaDisplayType: 'CNY',
usdExchangeRate: 7.2,
})
await waitForLoadedForm()
assert.equal(getControlByLabel<HTMLInputElement>('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' },
}) })
await renderDrawer(redemption(1)) test('blocks updates and reports an error when loading rejects', async () => {
await act(async () => const updates: unknown[] = []
waitForCondition( Reflect.set(console, 'log', () => undefined)
() => document.body.textContent?.includes('Failed to load') === true, apiClient.get = async () => {
'unsuccessful-load toast was not shown' 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) expect(getSaveButton()).toBeDisabled()
assert.equal(document.body.textContent?.includes('raw server message'), false) submitForm()
}) expect(updates).toEqual([])
})
test('redemption drawer keeps the original quota when another field changes', async () => { test('blocks updates and uses localized feedback for unsuccessful responses', async () => {
const original = redemption(1) apiClient.get = async () => ({
const updates: Array<Record<string, unknown>> = [] data: { success: false, message: 'raw server message' },
apiClient.get = async () => ({ data: { success: true, data: original } }) })
apiClient.put = async (_url, data) => {
assert.ok(data && typeof data === 'object')
updates.push(data as Record<string, unknown>)
return { data: { success: true, data: original } }
}
await renderDrawer(original) await renderDrawer(redemption(1))
await waitForLoadedForm() await waitFor(() =>
assert.equal(getControlByLabel<HTMLInputElement>('Quota (USD)').value, '1') expect(document.body).toHaveTextContent('Failed to load')
await changeInput(getControlByLabel<HTMLInputElement>('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<Record<string, unknown>> = []
apiClient.get = async () => ({ data: { success: true, data: original } })
apiClient.put = async (_url, data) => {
assert.ok(data && typeof data === 'object')
updates.push(data as Record<string, unknown>)
return { data: { success: true, data: original } }
}
await renderDrawer(original)
await waitForLoadedForm()
await changeInput(getControlByLabel<HTMLInputElement>('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<Record<string, unknown>> = []
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<string, unknown>)
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 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<Record<string, unknown>> = []
apiClient.get = async () => ({ data: { success: true, data: original } })
apiClient.put = async (_url, data) => {
expect(data && typeof data === 'object').toBeTruthy()
updates.push(data as Record<string, unknown>)
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<Record<string, unknown>> = []
apiClient.get = async () => ({ data: { success: true, data: original } })
apiClient.put = async (_url, data) => {
expect(data && typeof data === 'object').toBeTruthy()
updates.push(data as Record<string, unknown>)
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<Record<string, unknown>> = []
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<string, unknown>)
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 } }) secondRequest.resolve({ data: { success: true, data: second } })
) await waitForLoadedForm()
await waitForLoadedForm()
await act(async () =>
firstRequest.resolve({ data: { success: true, data: first } }) firstRequest.resolve({ data: { success: true, data: first } })
) expect(getControlByLabel('Name').value).toBe('code-2')
assert.equal(getControlByLabel<HTMLInputElement>('Name').value, 'code-2')
await changeInput(getControlByLabel<HTMLInputElement>('Name'), 'second') changeInput(getControlByLabel('Name'), 'second')
await submitForm() submitForm()
await act(async () => await waitFor(() => expect(updates).toHaveLength(1))
waitForCondition(() => updates.length === 1, 'update was not submitted')
)
assert.equal(updates[0]?.id, 2) expect(updates[0]?.id).toBe(2)
assert.equal(updates[0]?.quota, 1000001) expect(updates[0]?.quota).toBe(1000001)
})
}) })
@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { positiveIntegerSchema } from '../../utils/numeric-field' 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', () => { describe('per-token Auto group limit validation', () => {
test('accepts any positive integer without a product upper bound', () => { 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', () => { test('rejects zero, negative, and fractional limits', () => {
for (const maxTokenAutoGroups of [0, -1, 1.5]) { for (const maxTokenAutoGroups of [0, -1, 1.5]) {
const result = schema.safeParse(maxTokenAutoGroups) const result = schema.safeParse(maxTokenAutoGroups)
assert.equal(result.success, false) expect(result.success).toBe(false)
if (result.success) continue 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')
} }
}) })
}) })
@@ -16,128 +16,49 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { after, describe, test } from 'node:test' import { fireEvent, render, screen } from '@testing-library/react'
import i18next from 'i18next'
import { beforeAll, describe, expect, test } from 'vitest'
import { Window } from 'happy-dom' import { ToolPriceSettings } from '../tool-price-settings'
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
)
}
describe('tool price validation', () => { describe('tool price validation', () => {
after(() => { beforeAll(() => {
domWindow.close() 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 () => { test('blocks an empty price without converting it to an explicit zero', () => {
const container = document.createElement('div')
document.body.append(container)
const root = createRoot(container)
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { mutations: { retry: false } }, defaultOptions: { mutations: { retry: false } },
}) })
await act(async () => { render(
root.render( <QueryClientProvider client={queryClient}>
<QueryClientProvider client={queryClient}> <ToolPriceSettings defaultValue='{"web_search":10}' />
<I18nextProvider i18n={i18n}> </QueryClientProvider>
<ToolPriceSettings defaultValue='{"web_search":10}' />
</I18nextProvider>
</QueryClientProvider>
)
})
const priceInput = container.querySelector<HTMLInputElement>(
'input[aria-label="Price ($/1K calls): web_search"]'
) )
assert.ok(priceInput)
await act(async () => { const priceInput = screen.getByRole('spinbutton', {
changeInputValue(priceInput, '') name: 'Price ($/1K calls): web_search',
}) })
const saveButton = screen.getByRole('button', { name: 'Save tool prices' })
assert.equal(priceInput.getAttribute('aria-invalid'), 'true') fireEvent.change(priceInput, { target: { value: '' } })
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)
await act(async () => { expect(priceInput).toHaveAttribute('aria-invalid', 'true')
changeInputValue(priceInput, '0') expect(screen.getByText('Please enter a valid number')).toBeInTheDocument()
}) expect(saveButton).toBeDisabled()
assert.equal(priceInput.getAttribute('aria-invalid'), 'false') fireEvent.change(priceInput, { target: { value: '0' } })
assert.equal(saveButton.disabled, false)
expect(priceInput).toHaveAttribute('aria-invalid', 'false')
expect(saveButton).toBeEnabled()
await act(async () => root.unmount())
container.remove()
queryClient.clear() queryClient.clear()
}) })
}) })
@@ -16,88 +16,19 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { render, screen } from '@testing-library/react'
import { after, describe, test } from 'node:test' import i18next from 'i18next'
import { Window } from 'happy-dom'
import type React from 'react' import type React from 'react'
import { beforeAll, describe, expect, test } from 'vitest'
const domWindow = new Window() import { formatLogQuota } from '@/lib/format'
const domGlobals = [
'window',
'document',
'navigator',
'HTMLElement',
'SVGElement',
'Node',
'Element',
'Event',
'CustomEvent',
'MutationObserver',
'requestAnimationFrame',
'cancelAnimationFrame',
'getComputedStyle',
] as const
for (const key of domGlobals) { import { LogCostDisplay } from '../log-cost-display'
Object.defineProperty(globalThis, key, {
configurable: true,
value: domWindow[key],
})
}
const { act } = await import('react') function renderCost(
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<typeof createRoot>
}
async function renderCost(
props: React.ComponentProps<typeof LogCostDisplay> props: React.ComponentProps<typeof LogCostDisplay>
): Promise<RenderedCost> { ): ReturnType<typeof render> {
const container = document.createElement('div') return render(<LogCostDisplay {...props} />)
document.body.append(container)
const root = createRoot(container)
await act(async () => {
root.render(
<I18nextProvider i18n={i18n}>
<LogCostDisplay {...props} />
</I18nextProvider>
)
})
return { container, root }
}
async function unmountCost(rendered: RenderedCost) {
await act(async () => rendered.root.unmount())
rendered.container.remove()
} }
function normalizedText(value: string | null): string { function normalizedText(value: string | null): string {
@@ -105,39 +36,36 @@ function normalizedText(value: string | null): string {
} }
describe('log cost display', () => { describe('log cost display', () => {
after(() => { beforeAll(() => {
domWindow.close() 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 () => { test('keeps the regular cost visible and adds an accessible surcharge marker', () => {
const rendered = await renderCost({ const rendered = renderCost({
quota: 12500, quota: 12500,
other: { other: {
tool_surcharges: [{ name: 'lookup_customer', count: 1, price: 5 }], tool_surcharges: [{ name: 'lookup_customer', count: 1, price: 5 }],
}, },
}) })
assert.equal( expect(
normalizedText(rendered.container.textContent).includes( normalizedText(rendered.container.textContent).includes(
normalizedText(formatLogQuota(12500)) normalizedText(formatLogQuota(12500))
), )
true ).toBe(true)
) const marker = screen.getByRole('img', {
const marker = rendered.container.querySelector( name: 'Includes tool-call surcharge',
'[data-tool-surcharge-indicator="true"]' })
) expect(marker).toHaveAttribute('data-tool-surcharge-indicator', 'true')
assert.ok(marker) expect(marker).toHaveAttribute('tabindex', '0')
assert.equal(
marker.getAttribute('aria-label'),
'Includes tool-call surcharge'
)
assert.equal(marker.getAttribute('tabindex'), '0')
await unmountCost(rendered)
}) })
test('preserves the subscription badge and adds the same legacy surcharge marker', async () => { test('preserves the subscription badge and adds the same legacy surcharge marker', () => {
const rendered = await renderCost({ renderCost({
quota: 5000, quota: 5000,
other: { other: {
billing_source: 'subscription', billing_source: 'subscription',
@@ -147,11 +75,9 @@ describe('log cost display', () => {
}, },
}) })
assert.equal(rendered.container.textContent?.includes('Subscription'), true) expect(screen.getByText('Subscription')).toBeInTheDocument()
assert.ok( expect(
rendered.container.querySelector('[data-tool-surcharge-indicator="true"]') screen.getByRole('img', { name: 'Includes tool-call surcharge' })
) ).toHaveAttribute('data-tool-surcharge-indicator', 'true')
await unmountCost(rendered)
}) })
}) })
@@ -16,20 +16,18 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import type { LogOtherData } from '../../types' import type { LogOtherData } from '../../types'
import { hasToolSurcharge } from '../format' import { hasToolSurcharge } from '../format'
describe('tool surcharge detection', () => { describe('tool surcharge detection', () => {
test('shows the marker for a charged structured tool surcharge', () => { test('shows the marker for a charged structured tool surcharge', () => {
assert.equal( expect(
hasToolSurcharge({ hasToolSurcharge({
tool_surcharges: [{ name: 'lookup_customer', count: 2, price: 5 }], tool_surcharges: [{ name: 'lookup_customer', count: 2, price: 5 }],
}), })
true ).toBe(true)
)
}) })
const legacyCases: Array<{ const legacyCases: Array<{
@@ -63,7 +61,7 @@ describe('tool surcharge detection', () => {
for (const scenario of legacyCases) { for (const scenario of legacyCases) {
test(`keeps the marker visible for legacy ${scenario.name} charges`, () => { 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) { for (const other of invalidCases) {
assert.equal(hasToolSurcharge(other), false) expect(hasToolSurcharge(other)).toBe(false)
} }
}) })
}) })
@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { PAYMENT_TYPES } from '../constants' import { PAYMENT_TYPES } from '../constants'
import { requestPaymentAmount } from './use-payment' import { requestPaymentAmount } from './use-payment'
@@ -44,7 +43,7 @@ describe('payment amount routing', () => {
}, },
}) })
assert.equal(amount, 18.75) expect(amount).toBe(18.75)
assert.deepEqual(calls, ['waffo:120']) expect(calls).toEqual(['waffo:120'])
}) })
}) })
+10 -11
View File
@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { PAYMENT_TYPES } from '../constants' import { PAYMENT_TYPES } from '../constants'
import { import {
@@ -29,11 +28,11 @@ import {
describe('payment type classification', () => { describe('payment type classification', () => {
test('keeps Waffo and Waffo Pancake on their dedicated flows', () => { test('keeps Waffo and Waffo Pancake on their dedicated flows', () => {
assert.equal(isWaffoPayment(PAYMENT_TYPES.WAFFO), true) expect(isWaffoPayment(PAYMENT_TYPES.WAFFO)).toBe(true)
assert.equal(isWaffoPayment(PAYMENT_TYPES.WAFFO_PANCAKE), false) expect(isWaffoPayment(PAYMENT_TYPES.WAFFO_PANCAKE)).toBe(false)
assert.equal(isWaffoPancakePayment(PAYMENT_TYPES.WAFFO_PANCAKE), true) expect(isWaffoPancakePayment(PAYMENT_TYPES.WAFFO_PANCAKE)).toBe(true)
assert.equal(isWaffoPancakePayment(PAYMENT_TYPES.WAFFO), false) expect(isWaffoPancakePayment(PAYMENT_TYPES.WAFFO)).toBe(false)
assert.equal(isStripePayment(PAYMENT_TYPES.STRIPE), true) expect(isStripePayment(PAYMENT_TYPES.STRIPE)).toBe(true)
}) })
}) })
@@ -60,8 +59,8 @@ describe('payment dispatch', () => {
} }
) )
assert.equal(success, true) expect(success).toBe(true)
assert.deepEqual(calls, ['waffo:120:3']) expect(calls).toEqual(['waffo:120:3'])
}) })
test('does not create a Waffo order without a selected method index', async () => { test('does not create a Waffo order without a selected method index', async () => {
@@ -80,7 +79,7 @@ describe('payment dispatch', () => {
} }
) )
assert.equal(success, false) expect(success).toBe(false)
assert.equal(called, false) expect(called).toBe(false)
}) })
}) })
+57 -66
View File
@@ -16,10 +16,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com 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 { QueryClient } from '@tanstack/react-query'
import { afterEach, describe, expect, test } from 'vitest'
import { useAuthStore, type AuthBundle } from '../stores/auth-store' import { useAuthStore, type AuthBundle } from '../stores/auth-store'
import { import {
@@ -59,10 +57,10 @@ afterEach(() => {
describe('authentication session coordination', () => { describe('authentication session coordination', () => {
test('bootstrap distinguishes a completed anonymous check from an active session', async () => { test('bootstrap distinguishes a completed anonymous check from an active session', async () => {
useAuthStore.getState().auth.reset('complete') useAuthStore.getState().auth.reset('complete')
assert.deepEqual(await bootstrapAuthentication(), { kind: 'anonymous' }) expect(await bootstrapAuthentication()).toEqual({ kind: 'anonymous' })
useAuthStore.getState().auth.setBundle(bundle) useAuthStore.getState().auth.setBundle(bundle)
assert.deepEqual(await bootstrapAuthentication(), { expect(await bootstrapAuthentication()).toEqual({
kind: 'authenticated', kind: 'authenticated',
bundle, bundle,
}) })
@@ -97,10 +95,10 @@ describe('authentication session coordination', () => {
const outcome = await createRefreshRunner(runtime)() const outcome = await createRefreshRunner(runtime)()
assert.equal(outcome.kind, 'authenticated') expect(outcome.kind).toBe('authenticated')
assert.deepEqual(requestedSIDs, [bundle.session.sid, undefined]) expect(requestedSIDs).toEqual([bundle.session.sid, undefined])
assert.deepEqual(clears, [[false, 'idle']]) expect(clears).toEqual([[false, 'idle']])
assert.deepEqual(accepted, [bundle]) expect(accepted).toEqual([bundle])
}) })
test('a rejected refresh confirms anonymous state and synchronizes sign-out', async () => { test('a rejected refresh confirms anonymous state and synchronizes sign-out', async () => {
@@ -117,10 +115,10 @@ describe('authentication session coordination', () => {
wait: async () => undefined, wait: async () => undefined,
} }
assert.deepEqual(await createRefreshRunner(runtime)(), { expect(await createRefreshRunner(runtime)()).toEqual({
kind: 'anonymous', kind: 'anonymous',
}) })
assert.deepEqual(clears, [[true, undefined]]) expect(clears).toEqual([[true, undefined]])
}) })
test('a temporary refresh failure remains retryable without clearing the session', async () => { 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)() const outcome = await createRefreshRunner(runtime)()
assert.equal(outcome.kind, 'transient_error') expect(outcome.kind).toBe('transient_error')
assert.equal(clearCount, 0) expect(clearCount).toBe(0)
assert.equal(transientCount, 1) expect(transientCount).toBe(1)
}) })
test('a rate limited refresh remains retryable without clearing the session', async () => { 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)() const outcome = await createRefreshRunner(runtime)()
assert.equal(outcome.kind, 'transient_error') expect(outcome.kind).toBe('transient_error')
assert.equal(clearCount, 0) expect(clearCount).toBe(0)
assert.equal(transientCount, 1) expect(transientCount).toBe(1)
}) })
test('an exhausted refresh race clears the unusable local session', async () => { 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', kind: 'out_of_sync',
code: 'AUTH_REFRESH_RACE', code: 'AUTH_REFRESH_RACE',
}) })
assert.deepEqual(requestedDelays, [80, 200, 500]) expect(requestedDelays).toEqual([80, 200, 500])
assert.deepEqual(clears, [[false, undefined]]) expect(clears).toEqual([[false, undefined]])
}) })
test('an unexpected successful response is treated as out of sync', async () => { test('an unexpected successful response is treated as out of sync', async () => {
@@ -213,11 +211,11 @@ describe('authentication session coordination', () => {
wait: async () => undefined, wait: async () => undefined,
} }
assert.deepEqual(await createRefreshRunner(runtime)(), { expect(await createRefreshRunner(runtime)()).toEqual({
kind: 'out_of_sync', kind: 'out_of_sync',
code: 'AUTH_INVALID_REFRESH_RESPONSE', 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 () => { 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)() const outcome = await createRefreshRunner(runtime)()
assert.equal(outcome.kind, 'transient_error') expect(outcome.kind).toBe('transient_error')
assert.equal(accepted, false) expect(accepted).toBe(false)
}) })
test('explicit rotations update only the current session', () => { test('explicit rotations update only the current session', () => {
@@ -254,41 +252,35 @@ describe('authentication session coordination', () => {
session: { ...bundle.session, last_active_at: 200 }, session: { ...bundle.session, last_active_at: 200 },
}) })
assert.equal(useAuthStore.getState().auth.accessToken, 'rotated-token') expect(useAuthStore.getState().auth.accessToken).toBe('rotated-token')
assert.strictEqual(useAuthStore.getState().auth.user, bundle.user) expect(useAuthStore.getState().auth.user).toBe(bundle.user)
assert.throws( expect(() =>
() => applyAuthRotation({
applyAuthRotation({ access_token: 'non-bearer-token',
access_token: 'non-bearer-token', token_type: 'Custom',
token_type: 'Custom', access_expires_at: bundle.access_expires_at + 120,
access_expires_at: bundle.access_expires_at + 120, session: bundle.session,
session: bundle.session, })
}), ).toThrow(/Invalid authentication rotation response/)
/Invalid authentication rotation response/ expect(() =>
) applyAuthRotation({
assert.throws( access_token: 'non-current-token',
() => token_type: 'Bearer',
applyAuthRotation({ access_expires_at: bundle.access_expires_at + 120,
access_token: 'non-current-token', session: { ...bundle.session, current: false },
token_type: 'Bearer', })
access_expires_at: bundle.access_expires_at + 120, ).toThrow(/Invalid authentication rotation response/)
session: { ...bundle.session, current: false },
}),
/Invalid authentication rotation response/
)
assert.throws( expect(() =>
() => applyAuthRotation({
applyAuthRotation({ access_token: 'wrong-session-token',
access_token: 'wrong-session-token', token_type: 'Bearer',
token_type: 'Bearer', access_expires_at: bundle.access_expires_at + 120,
access_expires_at: bundle.access_expires_at + 120, session: { ...bundle.session, sid: 'session-b' },
session: { ...bundle.session, sid: 'session-b' }, })
}), ).toThrow(/session mismatch/)
/session mismatch/ expect(useAuthStore.getState().auth.accessToken).toBe('rotated-token')
)
assert.equal(useAuthStore.getState().auth.accessToken, 'rotated-token')
}) })
test('sign-out clears user-scoped query, mutation, and authentication state', () => { test('sign-out clears user-scoped query, mutation, and authentication state', () => {
@@ -305,13 +297,13 @@ describe('authentication session coordination', () => {
clearAuthenticatedClientState(queryClient, false) clearAuthenticatedClientState(queryClient, false)
assert.equal(queryClient.getQueryCache().getAll().length, 0) expect(queryClient.getQueryCache().getAll().length).toBe(0)
assert.equal(queryClient.getMutationCache().getAll().length, 0) expect(queryClient.getMutationCache().getAll().length).toBe(0)
assert.equal(useAuthStore.getState().auth.user, null) expect(useAuthStore.getState().auth.user).toBe(null)
assert.equal(useAuthStore.getState().auth.accessToken, null) expect(useAuthStore.getState().auth.accessToken).toBe(null)
assert.equal(useAuthStore.getState().auth.session, null) expect(useAuthStore.getState().auth.session).toBe(null)
assert.equal(useAuthStore.getState().auth.pending2FAFlowToken, null) expect(useAuthStore.getState().auth.pending2FAFlowToken).toBe(null)
assert.equal(useAuthStore.getState().auth.bootstrapState, 'complete') expect(useAuthStore.getState().auth.bootstrapState).toBe('complete')
const nextBundle: AuthBundle = { const nextBundle: AuthBundle = {
...bundle, ...bundle,
@@ -320,8 +312,7 @@ describe('authentication session coordination', () => {
session: { ...bundle.session, sid: 'session-b' }, session: { ...bundle.session, sid: 'session-b' },
} }
useAuthStore.getState().auth.setBundle(nextBundle) useAuthStore.getState().auth.setBundle(nextBundle)
assert.equal( expect(queryClient.getQueryData(['account', bundle.user.id])).toBe(
queryClient.getQueryData(['account', bundle.user.id]),
undefined undefined
) )
}) })
+11 -17
View File
@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { resolveLegacyRoute } from './legacy-route' import { resolveLegacyRoute } from './legacy-route'
@@ -43,17 +42,15 @@ describe('legacy frontend route migration', () => {
} }
for (const [source, target] of Object.entries(routes)) { 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', () => { test('preserves search and hash while applying route-specific behavior', () => {
assert.equal( expect(resolveLegacyRoute('/login?redirect=%2Fkeys#continue')).toBe(
resolveLegacyRoute('/login?redirect=%2Fkeys#continue'),
'/sign-in?redirect=%2Fkeys#continue' '/sign-in?redirect=%2Fkeys#continue'
) )
assert.equal( expect(resolveLegacyRoute('/console/topup?source=email#orders')).toBe(
resolveLegacyRoute('/console/topup?source=email#orders'),
'/wallet?source=email#orders' '/wallet?source=email#orders'
) )
}) })
@@ -75,23 +72,20 @@ describe('legacy frontend route migration', () => {
} }
for (const [tab, target] of Object.entries(settingsTabs)) { for (const [tab, target] of Object.entries(settingsTabs)) {
assert.equal( expect(
resolveLegacyRoute(`/console/setting?tab=${tab}&from=bookmark#form`), resolveLegacyRoute(`/console/setting?tab=${tab}&from=bookmark#form`)
`${target}?tab=${tab}&from=bookmark#form` ).toBe(`${target}?tab=${tab}&from=bookmark#form`)
)
} }
assert.equal( expect(resolveLegacyRoute('/console/setting?tab=unknown')).toBe(
resolveLegacyRoute('/console/setting?tab=unknown'),
'/system-settings?tab=unknown' '/system-settings?tab=unknown'
) )
}) })
test('safely redirects unknown console locations without touching new routes', () => { test('safely redirects unknown console locations without touching new routes', () => {
assert.equal( expect(resolveLegacyRoute('/console/removed?page=2#old')).toBe(
resolveLegacyRoute('/console/removed?page=2#old'),
'/dashboard?page=2#old' '/dashboard?page=2#old'
) )
assert.equal(resolveLegacyRoute('/dashboard'), null) expect(resolveLegacyRoute('/dashboard')).toBe(null)
assert.equal(resolveLegacyRoute('/api/status'), null) expect(resolveLegacyRoute('/api/status')).toBe(null)
}) })
}) })
+9 -11
View File
@@ -16,8 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import assert from 'node:assert/strict' import { describe, expect, test } from 'vitest'
import { describe, test } from 'node:test'
import { getServerErrorMessageKey } from './server-error-message' import { getServerErrorMessageKey } from './server-error-message'
@@ -25,8 +24,8 @@ describe('server error message mapping', () => {
test('maps the active-session limit to recovery instructions', () => { test('maps the active-session limit to recovery instructions', () => {
const message = getServerErrorMessageKey({ code: 'AUTH_SESSION_LIMIT' }) const message = getServerErrorMessageKey({ code: 'AUTH_SESSION_LIMIT' })
assert.match(message ?? '', /Sign out other sessions/) expect(message ?? '').toMatch(/Sign out other sessions/)
assert.match(message ?? '', /reset your password/) expect(message ?? '').toMatch(/reset your password/)
}) })
test('maps an Axios-shaped issuance limit to rolling-window guidance', () => { 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' } }, response: { data: { code: 'AUTH_SESSION_ISSUANCE_LIMIT' } },
}) })
assert.match(message ?? '', /rolling window/) expect(message ?? '').toMatch(/rolling window/)
assert.equal(getServerErrorMessageKey({ code: 'UNKNOWN_CODE' }), null) expect(getServerErrorMessageKey({ code: 'UNKNOWN_CODE' })).toBe(null)
}) })
test('maps stable Telegram bind errors without exposing server text', () => { 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)) { for (const [code, message] of Object.entries(expected)) {
assert.equal(getServerErrorMessageKey({ code }), message) expect(getServerErrorMessageKey({ code })).toBe(message)
} }
assert.equal( expect(
getServerErrorMessageKey({ getServerErrorMessageKey({
response: { response: {
data: { code: 'TELEGRAM_BIND_INTERNAL_ERROR', message: 'raw detail' }, data: { code: 'TELEGRAM_BIND_INTERNAL_ERROR', message: 'raw detail' },
}, },
}), })
expected.TELEGRAM_BIND_INTERNAL_ERROR ).toBe(expected.TELEGRAM_BIND_INTERNAL_ERROR)
)
}) })
}) })
+73
View File
@@ -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 <https://www.gnu.org/licenses/>.
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,
})
+1 -1
View File
@@ -20,5 +20,5 @@
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true
}, },
"include": ["rsbuild.config.ts"] "include": ["rsbuild.config.ts", "vitest.config.ts"]
} }
+39
View File
@@ -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 <https://www.gnu.org/licenses/>.
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}'],
},
})