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