Merge pull request #255 from cita-777/codex/ci-typecheck-pr

fix: tighten CI typecheck coverage
This commit is contained in:
Cita
2026-03-23 16:40:52 +08:00
committed by GitHub
70 changed files with 336 additions and 223 deletions
+22
View File
@@ -138,6 +138,28 @@ jobs:
- name: Build desktop - name: Build desktop
run: npm run build:desktop run: npm run build:desktop
typecheck:
name: Typecheck
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
- name: Install dependencies
run: npm ci --prefer-offline --no-audit --no-fund
- name: Run typecheck
run: npm run typecheck
schema-sqlite: schema-sqlite:
name: Schema Check (SQLite) name: Schema Check (SQLite)
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
+42 -19
View File
@@ -43,6 +43,7 @@
"@types/pg": "^8.15.6", "@types/pg": "^8.15.6",
"@types/react": "^18.3.12", "@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",
"@types/react-test-renderer": "^19.1.0",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"concurrently": "^9.1.0", "concurrently": "^9.1.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
@@ -66,7 +67,7 @@
"wait-on": "^9.0.4" "wait-on": "^9.0.4"
}, },
"engines": { "engines": {
"node": "^20.19.0 || >=22.12.0" "node": ">=22.15.0"
} }
}, },
"node_modules/@algolia/abtesting": { "node_modules/@algolia/abtesting": {
@@ -230,6 +231,7 @@
"integrity": "sha512-Nt9hri7nbOo0RipAsGjIssHkpLMHHN/P7QqENywAq5TLsoYDzUyJGny8FEiD/9KJUxtGH8blGpMedilI6kK3rA==", "integrity": "sha512-Nt9hri7nbOo0RipAsGjIssHkpLMHHN/P7QqENywAq5TLsoYDzUyJGny8FEiD/9KJUxtGH8blGpMedilI6kK3rA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@algolia/client-common": "5.49.1", "@algolia/client-common": "5.49.1",
"@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1",
@@ -443,6 +445,7 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.0", "@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0", "@babel/generator": "^7.29.0",
@@ -846,6 +849,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=20.19.0" "node": ">=20.19.0"
}, },
@@ -894,6 +898,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=20.19.0" "node": ">=20.19.0"
} }
@@ -922,6 +927,7 @@
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0", "fast-json-stable-stringify": "^2.0.0",
@@ -967,6 +973,7 @@
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
@@ -1456,7 +1463,6 @@
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"cross-dirname": "^0.1.0", "cross-dirname": "^0.1.0",
"debug": "^4.3.4", "debug": "^4.3.4",
@@ -1478,7 +1484,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"graceful-fs": "^4.2.0", "graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1", "jsonfile": "^6.0.1",
@@ -1495,7 +1500,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"universalify": "^2.0.0" "universalify": "^2.0.0"
}, },
@@ -1510,7 +1514,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"engines": { "engines": {
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
@@ -4679,6 +4682,7 @@
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
} }
@@ -5088,6 +5092,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz",
"integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
@@ -5115,6 +5120,7 @@
"integrity": "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==", "integrity": "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/node": "*", "@types/node": "*",
"pg-protocol": "*", "pg-protocol": "*",
@@ -5146,6 +5152,7 @@
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"csstype": "^3.2.2" "csstype": "^3.2.2"
@@ -5161,6 +5168,16 @@
"@types/react": "^18.0.0" "@types/react": "^18.0.0"
} }
}, },
"node_modules/@types/react-test-renderer": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-19.1.0.tgz",
"integrity": "sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/responselike": { "node_modules/@types/responselike": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
@@ -5883,6 +5900,7 @@
"integrity": "sha512-X3Pp2aRQhg4xUC6PQtkubn5NpRKuUPQ9FPDQlx36SmpFwwH2N0/tw4c+NXV3nw3PsgeUs+BuWGP0gjz3TvENLQ==", "integrity": "sha512-X3Pp2aRQhg4xUC6PQtkubn5NpRKuUPQ9FPDQlx36SmpFwwH2N0/tw4c+NXV3nw3PsgeUs+BuWGP0gjz3TvENLQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@algolia/abtesting": "1.15.1", "@algolia/abtesting": "1.15.1",
"@algolia/client-abtesting": "5.49.1", "@algolia/client-abtesting": "5.49.1",
@@ -6317,6 +6335,7 @@
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.11", "follow-redirects": "^1.15.11",
"form-data": "^4.0.5", "form-data": "^4.0.5",
@@ -6371,6 +6390,7 @@
"integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"bindings": "^1.5.0", "bindings": "^1.5.0",
"prebuild-install": "^7.1.1" "prebuild-install": "^7.1.1"
@@ -6460,6 +6480,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -6863,6 +6884,7 @@
"integrity": "sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==", "integrity": "sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@chevrotain/cst-dts-gen": "11.1.2", "@chevrotain/cst-dts-gen": "11.1.2",
"@chevrotain/gast": "11.1.2", "@chevrotain/gast": "11.1.2",
@@ -7218,8 +7240,7 @@
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true
"peer": true
}, },
"node_modules/cross-env": { "node_modules/cross-env": {
"version": "7.0.3", "version": "7.0.3",
@@ -7282,6 +7303,7 @@
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10" "node": ">=0.10"
} }
@@ -7715,6 +7737,7 @@
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
} }
@@ -8170,6 +8193,7 @@
"integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"app-builder-lib": "26.8.1", "app-builder-lib": "26.8.1",
"builder-util": "26.8.1", "builder-util": "26.8.1",
@@ -8765,7 +8789,6 @@
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@electron/asar": "^3.2.1", "@electron/asar": "^3.2.1",
"debug": "^4.1.1", "debug": "^4.1.1",
@@ -8786,7 +8809,6 @@
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"graceful-fs": "^4.1.2", "graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0", "jsonfile": "^4.0.0",
@@ -9407,6 +9429,7 @@
"integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tabbable": "^6.4.0" "tabbable": "^6.4.0"
} }
@@ -11500,7 +11523,6 @@
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"minimist": "^1.2.6" "minimist": "^1.2.6"
}, },
@@ -11545,6 +11567,7 @@
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.20.0.tgz", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.20.0.tgz",
"integrity": "sha512-eCLUs7BNbgA6nf/MZXsaBO1SfGs0LtLVrJD3WeWq+jPLDWkSufTD+aGMwykfUVPdZnblaUK1a8G/P63cl9FkKg==", "integrity": "sha512-eCLUs7BNbgA6nf/MZXsaBO1SfGs0LtLVrJD3WeWq+jPLDWkSufTD+aGMwykfUVPdZnblaUK1a8G/P63cl9FkKg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"aws-ssl-profiles": "^1.1.2", "aws-ssl-profiles": "^1.1.2",
"denque": "^2.1.0", "denque": "^2.1.0",
@@ -12113,6 +12136,7 @@
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
"integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"pg-connection-string": "^2.12.0", "pg-connection-string": "^2.12.0",
"pg-pool": "^3.13.0", "pg-pool": "^3.13.0",
@@ -12210,6 +12234,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -12390,7 +12415,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"commander": "^9.4.0" "commander": "^9.4.0"
}, },
@@ -12408,7 +12432,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"engines": { "engines": {
"node": "^12.20.0 || >=14" "node": "^12.20.0 || >=14"
} }
@@ -12596,6 +12619,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0" "loose-envify": "^1.1.0"
}, },
@@ -12608,6 +12632,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0", "loose-envify": "^1.1.0",
"scheduler": "^0.23.2" "scheduler": "^0.23.2"
@@ -12896,7 +12921,6 @@
"deprecated": "Rimraf versions prior to v4 are no longer supported", "deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"peer": true,
"dependencies": { "dependencies": {
"glob": "^7.1.3" "glob": "^7.1.3"
}, },
@@ -12909,8 +12933,7 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/rimraf/node_modules/brace-expansion": { "node_modules/rimraf/node_modules/brace-expansion": {
"version": "1.1.12", "version": "1.1.12",
@@ -12918,7 +12941,6 @@
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@@ -12931,7 +12953,6 @@
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"peer": true,
"dependencies": { "dependencies": {
"fs.realpath": "^1.0.0", "fs.realpath": "^1.0.0",
"inflight": "^1.0.4", "inflight": "^1.0.4",
@@ -12953,7 +12974,6 @@
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"peer": true,
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
}, },
@@ -13953,7 +13973,6 @@
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"rimraf": "~2.6.2" "rimraf": "~2.6.2"
@@ -14765,6 +14784,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -15018,6 +15038,7 @@
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.4", "fdir": "^6.4.4",
@@ -16675,6 +16696,7 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.21.3", "esbuild": "^0.21.3",
"postcss": "^8.4.43", "postcss": "^8.4.43",
@@ -16790,6 +16812,7 @@
"integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==", "integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.29", "@vue/compiler-dom": "3.5.29",
"@vue/compiler-sfc": "3.5.29", "@vue/compiler-sfc": "3.5.29",
+6
View File
@@ -34,6 +34,11 @@
"build:server": "tsc -p tsconfig.server.json && tsx scripts/dev/copy-runtime-db-generated.ts", "build:server": "tsc -p tsconfig.server.json && tsx scripts/dev/copy-runtime-db-generated.ts",
"build:desktop": "tsc -p tsconfig.desktop.json", "build:desktop": "tsc -p tsconfig.desktop.json",
"build": "npm run build:web && npm run build:server && npm run build:desktop", "build": "npm run build:web && npm run build:server && npm run build:desktop",
"typecheck:web": "tsc -p tsconfig.web.json",
"typecheck:web:test": "tsc -p tsconfig.web.test.json",
"typecheck:server": "tsc --noEmit -p tsconfig.server.json",
"typecheck:desktop": "tsc --noEmit -p tsconfig.desktop.json",
"typecheck": "npm run typecheck:web && npm run typecheck:web:test && npm run typecheck:server && npm run typecheck:desktop",
"dist:desktop": "npm run build && electron-builder --config electron-builder.yml --publish never", "dist:desktop": "npm run build && electron-builder --config electron-builder.yml --publish never",
"dist:desktop:mac:intel": "npm run build && electron-builder --config electron-builder.yml --publish never --mac --x64", "dist:desktop:mac:intel": "npm run build && electron-builder --config electron-builder.yml --publish never --mac --x64",
"package:desktop": "npm run desktop:icons && electron-builder --config electron-builder.yml --publish never", "package:desktop": "npm run desktop:icons && electron-builder --config electron-builder.yml --publish never",
@@ -93,6 +98,7 @@
"@types/pg": "^8.15.6", "@types/pg": "^8.15.6",
"@types/react": "^18.3.12", "@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",
"@types/react-test-renderer": "^19.1.0",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"concurrently": "^9.1.0", "concurrently": "^9.1.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
+1 -1
View File
@@ -72,7 +72,7 @@ describe('Login surface', () => {
expect(githubLink.props.target).toBe('_blank'); expect(githubLink.props.target).toBe('_blank');
expect(tokenInput.props.type).toBe('password'); expect(tokenInput.props.type).toBe('password');
} finally { } finally {
root.unmount(); root?.unmount();
} }
}); });
}); });
+1 -1
View File
@@ -155,7 +155,7 @@ describe('App mobile layout', () => {
'uses the shared breakpoint at width $width', 'uses the shared breakpoint at width $width',
async ({ width, expectedLayout, hasHamburger }) => { async ({ width, expectedLayout, hasHamburger }) => {
setupRuntime(width); setupRuntime(width);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
+1 -1
View File
@@ -151,7 +151,7 @@ describe('App mobile sidebar', () => {
it('opens the mobile drawer from the hamburger trigger and exposes the close affordance', async () => { it('opens the mobile drawer from the hamburger trigger and exposes the close affordance', async () => {
setupRuntime(768); setupRuntime(768);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -42,7 +42,7 @@ describe('ModelAnalysisPanel token summaries', () => {
}); });
it('renders total token summaries with compact units', () => { it('renders total token summaries with compact units', () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
act(() => { act(() => {
root = create( root = create(
@@ -70,10 +70,10 @@ describe('ModelAnalysisPanel token summaries', () => {
globalThis.document = { globalThis.document = {
documentElement: {}, documentElement: {},
} as unknown as Document; } as unknown as Document;
delete (globalThis as typeof globalThis & { getComputedStyle?: typeof getComputedStyle }).getComputedStyle; Reflect.deleteProperty(globalThis as typeof globalThis & Record<string, unknown>, 'getComputedStyle');
delete (globalThis as typeof globalThis & { MutationObserver?: typeof MutationObserver }).MutationObserver; Reflect.deleteProperty(globalThis as typeof globalThis & Record<string, unknown>, 'MutationObserver');
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
await expect(act(async () => { await expect(act(async () => {
root = create( root = create(
+1 -1
View File
@@ -7,7 +7,7 @@ import CenteredModal from './CenteredModal.js';
describe('CenteredModal component', () => { describe('CenteredModal component', () => {
it('does not close on backdrop click by default and exposes an explicit close button', async () => { it('does not close on backdrop click by default and exposes an explicit close button', async () => {
const onClose = vi.fn(); const onClose = vi.fn();
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -17,8 +17,8 @@ describe('SiteDistributionChart', () => {
getAttribute: vi.fn(), getAttribute: vi.fn(),
}, },
} as unknown as Document; } as unknown as Document;
delete (globalThis as typeof globalThis & { getComputedStyle?: typeof getComputedStyle }).getComputedStyle; Reflect.deleteProperty(globalThis as typeof globalThis & Record<string, unknown>, 'getComputedStyle');
delete (globalThis as typeof globalThis & { MutationObserver?: typeof MutationObserver }).MutationObserver; Reflect.deleteProperty(globalThis as typeof globalThis & Record<string, unknown>, 'MutationObserver');
}); });
afterEach(() => { afterEach(() => {
@@ -28,7 +28,7 @@ describe('SiteDistributionChart', () => {
}); });
it('renders with fallback label color when browser theme APIs are unavailable', async () => { it('renders with fallback label color when browser theme APIs are unavailable', async () => {
let renderer: ReturnType<typeof create> | null = null; let renderer!: WebTestRenderer;
await expect(act(async () => { await expect(act(async () => {
renderer = create( renderer = create(
@@ -46,6 +46,6 @@ describe('SiteDistributionChart', () => {
); );
})).resolves.toBeUndefined(); })).resolves.toBeUndefined();
renderer?.unmount(); renderer.unmount();
}); });
}); });
@@ -17,6 +17,10 @@ interface SiteDistributionChartProps {
type ViewMode = 'balance' | 'spend'; type ViewMode = 'balance' | 'spend';
function coerceDatumRecord(datum: unknown): Record<string, unknown> {
return datum && typeof datum === 'object' ? datum as Record<string, unknown> : {};
}
function safeNumber(value: unknown): number { function safeNumber(value: unknown): number {
if (typeof value !== 'number' || Number.isNaN(value) || !Number.isFinite(value)) return 0; if (typeof value !== 'number' || Number.isNaN(value) || !Number.isFinite(value)) return 0;
return value; return value;
@@ -120,24 +124,32 @@ export default function SiteDistributionChart({ data, loading }: SiteDistributio
mark: { mark: {
content: [ content: [
{ {
key: (datum: Record<string, unknown>) => datum.siteName as string, key: (datum: unknown) => {
value: (datum: Record<string, unknown>) => { const item = coerceDatumRecord(datum);
const val = safeNumber(datum.value); return String(item.siteName || '-');
},
value: (datum: unknown) => {
const item = coerceDatumRecord(datum);
const val = safeNumber(item.value);
return `$${val.toFixed(2)}`; return `$${val.toFixed(2)}`;
}, },
}, },
{ {
key: '占比', key: '占比',
value: (datum: Record<string, unknown>) => { value: (datum: unknown) => {
const pct = datum._percent_ as number; const item = coerceDatumRecord(datum);
return `${safeNumber(pct).toFixed(1)}%`; const pct = safeNumber(item._percent_);
return `${pct.toFixed(1)}%`;
}, },
}, },
{ {
key: '账户数', key: '账户数',
value: (datum: Record<string, unknown>) => `${datum.accountCount}`, value: (datum: unknown) => {
const item = coerceDatumRecord(datum);
return String(item.accountCount || 0);
}, },
], },
] as any,
}, },
}, },
color: PIE_COLORS, color: PIE_COLORS,
+1 -1
View File
@@ -11,7 +11,7 @@ vi.mock('react-dom', () => ({
describe('MobileDrawer', () => { describe('MobileDrawer', () => {
it('renders content, locks body scroll, and exposes explicit close affordances', async () => { it('renders content, locks body scroll, and exposes explicit close affordances', async () => {
const onClose = vi.fn(); const onClose = vi.fn();
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
vi.stubGlobal('document', { vi.stubGlobal('document', {
body: { body: {
style: { style: {
@@ -92,7 +92,7 @@ describe('SearchModal results', () => {
}); });
const onClose = vi.fn(); const onClose = vi.fn();
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
+7 -7
View File
@@ -22,7 +22,7 @@ describe('useIsMobile', () => {
removeEventListener: (event: string, handler: () => void) => { removeEventListener: (event: string, handler: () => void) => {
listeners.get(event)?.delete(handler); listeners.get(event)?.delete(handler);
}, },
dispatchEvent: (event: { type: string }) => { dispatchEvent: (event: Event) => {
listeners.get(event.type)?.forEach((handler) => handler()); listeners.get(event.type)?.forEach((handler) => handler());
return true; return true;
}, },
@@ -39,7 +39,7 @@ describe('useIsMobile', () => {
innerWidth: 767, innerWidth: 767,
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -66,7 +66,7 @@ describe('useIsMobile', () => {
}), }),
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -84,7 +84,7 @@ describe('useIsMobile', () => {
}); });
it('treats 767 and 768 as mobile, but 769 as desktop', async () => { it('treats 767 and 768 as mobile, but 769 as desktop', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -97,7 +97,7 @@ describe('useIsMobile', () => {
widthRef.current = 767; widthRef.current = 767;
window.innerWidth = 767; window.innerWidth = 767;
await act(async () => { await act(async () => {
window.dispatchEvent({ type: 'resize' }); window.dispatchEvent(new Event('resize'));
}); });
expect(root.root.findByType('div').props['data-mobile']).toBe('true'); expect(root.root.findByType('div').props['data-mobile']).toBe('true');
@@ -105,7 +105,7 @@ describe('useIsMobile', () => {
widthRef.current = 768; widthRef.current = 768;
window.innerWidth = 768; window.innerWidth = 768;
await act(async () => { await act(async () => {
window.dispatchEvent({ type: 'resize' }); window.dispatchEvent(new Event('resize'));
}); });
expect(root.root.findByType('div').props['data-mobile']).toBe('true'); expect(root.root.findByType('div').props['data-mobile']).toBe('true');
@@ -113,7 +113,7 @@ describe('useIsMobile', () => {
widthRef.current = 769; widthRef.current = 769;
window.innerWidth = 769; window.innerWidth = 769;
await act(async () => { await act(async () => {
window.dispatchEvent({ type: 'resize' }); window.dispatchEvent(new Event('resize'));
}); });
expect(root.root.findByType('div').props['data-mobile']).toBe('false'); expect(root.root.findByType('div').props['data-mobile']).toBe('false');
+1 -1
View File
@@ -6,7 +6,7 @@ import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode> <React.StrictMode>
<BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}> <BrowserRouter>
<App /> <App />
</BrowserRouter> </BrowserRouter>
</React.StrictMode> </React.StrictMode>
+1 -1
View File
@@ -190,7 +190,7 @@ describe('DownstreamKeys mobile layout', () => {
}); });
it('renders mobile cards and supports select-all-visible batch actions', async () => { it('renders mobile cards and supports select-all-visible batch actions', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
+10 -10
View File
@@ -165,7 +165,7 @@ afterEach(() => {
describe('DownstreamKeys page', () => { describe('DownstreamKeys page', () => {
it('loads management data and renders merged row content', async () => { it('loads management data and renders merged row content', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -213,7 +213,7 @@ describe('DownstreamKeys page', () => {
], ],
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -236,7 +236,7 @@ describe('DownstreamKeys page', () => {
const select = root!.root.findAllByType('select').find((node) => collectText(node).includes('仅禁用')); const select = root!.root.findAllByType('select').find((node) => collectText(node).includes('仅禁用'));
await act(async () => { await act(async () => {
select.props.onChange({ target: { value: 'disabled' } }); select!.props.onChange({ target: { value: 'disabled' } });
}); });
await flushMicrotasks(); await flushMicrotasks();
expect(collectText(root!.root)).toContain('batch-key'); expect(collectText(root!.root)).toContain('batch-key');
@@ -247,7 +247,7 @@ describe('DownstreamKeys page', () => {
}); });
it('supports create flow and drawer trend loading', async () => { it('supports create flow and drawer trend loading', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -327,7 +327,7 @@ describe('DownstreamKeys page', () => {
], ],
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -415,7 +415,7 @@ describe('DownstreamKeys page', () => {
}) })
.mockImplementationOnce(() => new Promise(() => {})); .mockImplementationOnce(() => new Promise(() => {}));
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -452,7 +452,7 @@ describe('DownstreamKeys page', () => {
}); });
it('separates exact models from group routes in advanced config and uses single-column layout', async () => { it('separates exact models from group routes in advanced config and uses single-column layout', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -503,7 +503,7 @@ describe('DownstreamKeys page', () => {
{ id: 14, modelPattern: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', enabled: true }, { id: 14, modelPattern: 'claude-opus-4-6', displayName: 'Claude Opus 4.6', enabled: true },
]); ]);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -570,7 +570,7 @@ describe('DownstreamKeys page', () => {
}); });
it('uses backend batch api for selected rows', async () => { it('uses backend batch api for selected rows', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -606,7 +606,7 @@ describe('DownstreamKeys page', () => {
}); });
it('supports group and tag editing plus batch metadata update', async () => { it('supports group and tag editing plus batch metadata update', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
+8 -8
View File
@@ -283,7 +283,7 @@ describe('ImportExport', () => {
}); });
it('shows ALL-API-Hub V2 preview counts and ignored sections', async () => { it('shows ALL-API-Hub V2 preview counts and ignored sections', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -309,7 +309,7 @@ describe('ImportExport', () => {
}); });
it('does not label native metapi backups as ALL-API-Hub V2', async () => { it('does not label native metapi backups as ALL-API-Hub V2', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -335,7 +335,7 @@ describe('ImportExport', () => {
it('uses backend import summary in the completion toast', async () => { it('uses backend import summary in the completion toast', async () => {
const confirmSpy = vi.mocked((window as { confirm: (...args: unknown[]) => boolean }).confirm); const confirmSpy = vi.mocked((window as { confirm: (...args: unknown[]) => boolean }).confirm);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -385,7 +385,7 @@ describe('ImportExport', () => {
}); });
it('loads webdav config and saves updates from the import/export page', async () => { it('loads webdav config and saves updates from the import/export page', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -422,7 +422,7 @@ describe('ImportExport', () => {
}); });
it('shows v2.1 config-backup wording and local-state notice', async () => { it('shows v2.1 config-backup wording and local-state notice', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -445,7 +445,7 @@ describe('ImportExport', () => {
}); });
it('renders webdav export type with ModernSelect instead of a native select', async () => { it('renders webdav export type with ModernSelect instead of a native select', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -493,7 +493,7 @@ describe('ImportExport', () => {
}); });
it('disables webdav actions while config has unsaved changes', async () => { it('disables webdav actions while config has unsaved changes', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -552,7 +552,7 @@ describe('ImportExport', () => {
}, },
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
+2
View File
@@ -43,7 +43,9 @@ import {
type ModelTesterModeState, type ModelTesterModeState,
type ParameterEnabled, type ParameterEnabled,
type PlaygroundMode, type PlaygroundMode,
type PlaygroundProtocol,
type PlaygroundMultipartFile, type PlaygroundMultipartFile,
type ProxyTestEnvelope,
type TestTargetFormat, type TestTargetFormat,
type TestChatPayload, type TestChatPayload,
} from './helpers/modelTesterSession.js'; } from './helpers/modelTesterSession.js';
@@ -82,7 +82,7 @@ describe('Models marketplace text', () => {
}); });
it('renders readable Chinese labels and fallback descriptions for marketplace models', async () => { it('renders readable Chinese labels and fallback descriptions for marketplace models', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -172,7 +172,7 @@ describe('Models marketplace text', () => {
], ],
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -213,7 +213,7 @@ describe('Models marketplace text', () => {
globalThis.window = nextWindow; globalThis.window = nextWindow;
globalThis.matchMedia = nextWindow.matchMedia; globalThis.matchMedia = nextWindow.matchMedia;
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -241,7 +241,7 @@ describe('Models marketplace text', () => {
} as unknown as Window & typeof globalThis; } as unknown as Window & typeof globalThis;
apiMock.getModelsMarketplace.mockImplementation(() => new Promise(() => {})); apiMock.getModelsMarketplace.mockImplementation(() => new Promise(() => {}));
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -331,7 +331,7 @@ describe('Models marketplace text', () => {
], ],
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -470,7 +470,7 @@ describe('Models marketplace text', () => {
], ],
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -547,7 +547,7 @@ describe('Models marketplace text', () => {
], ],
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
+2 -2
View File
@@ -68,7 +68,7 @@ describe('NotificationSettings', () => {
}); });
it('loads and saves telegram api base url and topic id', async () => { it('loads and saves telegram api base url and topic id', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -139,7 +139,7 @@ describe('NotificationSettings', () => {
notifyCooldownSec: 300, notifyCooldownSec: 300,
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
+9 -9
View File
@@ -108,7 +108,7 @@ describe('OAuthManagement page', () => {
offset: 0, offset: 0,
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -206,7 +206,7 @@ describe('OAuthManagement page', () => {
accountId: 7, accountId: 7,
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -304,7 +304,7 @@ describe('OAuthManagement page', () => {
}); });
apiMock.submitOAuthManualCallback.mockResolvedValue({ success: true }); apiMock.submitOAuthManualCallback.mockResolvedValue({ success: true });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -398,7 +398,7 @@ describe('OAuthManagement page', () => {
offset: 0, offset: 0,
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -464,7 +464,7 @@ describe('OAuthManagement page', () => {
offset: 0, offset: 0,
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -526,7 +526,7 @@ describe('OAuthManagement page', () => {
status: 'pending', status: 'pending',
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -618,7 +618,7 @@ describe('OAuthManagement page', () => {
status: 'pending', status: 'pending',
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -717,7 +717,7 @@ describe('OAuthManagement page', () => {
.mockResolvedValueOnce({ items: [], total: 0, limit: 100, offset: 0 }); .mockResolvedValueOnce({ items: [], total: 0, limit: 100, offset: 0 });
apiMock.deleteOAuthConnection.mockResolvedValue({ success: true }); apiMock.deleteOAuthConnection.mockResolvedValue({ success: true });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -860,7 +860,7 @@ describe('OAuthManagement page', () => {
}, },
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
+2 -2
View File
@@ -54,7 +54,7 @@ describe('ProgramLogs status label', () => {
}, },
]); ]);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
await act(async () => { await act(async () => {
root = create( root = create(
<MemoryRouter initialEntries={['/events']}> <MemoryRouter initialEntries={['/events']}>
@@ -90,7 +90,7 @@ describe('ProgramLogs status label', () => {
}, },
]); ]);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
await act(async () => { await act(async () => {
root = create( root = create(
<MemoryRouter initialEntries={['/events']}> <MemoryRouter initialEntries={['/events']}>
@@ -158,7 +158,7 @@ describe('ProxyLogs server-driven page', () => {
}); });
it('requests paginated data from the server and renders server summary counts', async () => { it('requests paginated data from the server and renders server summary counts', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -194,7 +194,7 @@ describe('ProxyLogs server-driven page', () => {
}); });
it('keeps the model badge sized to the model name in desktop rows', async () => { it('keeps the model badge sized to the model name in desktop rows', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -250,7 +250,7 @@ describe('ProxyLogs server-driven page', () => {
], ],
})); }));
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -277,7 +277,7 @@ describe('ProxyLogs server-driven page', () => {
}); });
it('re-queries the server for status, client, and search changes instead of filtering locally', async () => { it('re-queries the server for status, client, and search changes instead of filtering locally', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -342,7 +342,7 @@ describe('ProxyLogs server-driven page', () => {
}); });
it('loads detail on first expand and reuses the cached detail on re-expand', async () => { it('loads detail on first expand and reuses the cached detail on re-expand', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -385,7 +385,7 @@ describe('ProxyLogs server-driven page', () => {
}); });
it('hydrates site and time filters from the route query', async () => { it('hydrates site and time filters from the route query', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
+14 -5
View File
@@ -34,6 +34,12 @@ type ProxyLogDetailState = {
error?: string; error?: string;
}; };
type ProxyLogSiteFilterOption = {
id: number;
name: string;
status: string | null;
};
const PAGE_SIZES = [20, 50, 100]; const PAGE_SIZES = [20, 50, 100];
const DEFAULT_PAGE_SIZE = 50; const DEFAULT_PAGE_SIZE = 50;
const PROXY_LOG_CLIENT_FAMILY_LABELS: Record<string, string> = { const PROXY_LOG_CLIENT_FAMILY_LABELS: Record<string, string> = {
@@ -353,14 +359,17 @@ export default function ProxyLogs() {
try { try {
const result = await api.getSites(); const result = await api.getSites();
const rows = Array.isArray(result) ? result : (result?.sites || []); const rows = Array.isArray(result) ? result : (result?.sites || []);
const normalized = rows const normalized: ProxyLogSiteFilterOption[] = rows
.map((site: any) => ({ .map((site: any) => ({
id: Number(site?.id || 0), id: Number(site?.id || 0),
name: String(site?.name || '').trim() || `站点 #${site?.id ?? ''}`, name: String(site?.name || '').trim() || `站点 #${site?.id ?? ''}`,
status: typeof site?.status === 'string' ? site.status : null, status: typeof site?.status === 'string' ? site.status : null,
})) }))
.filter((site) => site.id > 0) .filter((site: ProxyLogSiteFilterOption) => site.id > 0)
.sort((left, right) => left.name.localeCompare(right.name, 'zh-CN')); .sort(
(left: ProxyLogSiteFilterOption, right: ProxyLogSiteFilterOption) =>
left.name.localeCompare(right.name, 'zh-CN'),
);
if (!cancelled) setSites(normalized); if (!cancelled) setSites(normalized);
} catch (error) { } catch (error) {
console.error('Failed to load sites for proxy log filters:', error); console.error('Failed to load sites for proxy log filters:', error);
@@ -701,7 +710,7 @@ export default function ProxyLogs() {
const detailState = detailById[log.id]; const detailState = detailById[log.id];
const detail = detailState?.data; const detail = detailState?.data;
const detailLog: ProxyLogRenderItem = detail ? { ...log, ...detail } : log; const detailLog: ProxyLogRenderItem = detail ? { ...log, ...detail } : log;
const pathMeta = parseProxyLogPathMeta(detailLog.errorMessage); const pathMeta = parseProxyLogPathMeta(detailLog.errorMessage ?? undefined);
const billingDetailSummary = detail ? formatBillingDetailSummary(detailLog) : null; const billingDetailSummary = detail ? formatBillingDetailSummary(detailLog) : null;
const billingProcessLines = detail ? buildBillingProcessLines(detailLog) : []; const billingProcessLines = detail ? buildBillingProcessLines(detailLog) : [];
const downstreamKeySummary = renderDownstreamKeySummary(detailLog); const downstreamKeySummary = renderDownstreamKeySummary(detailLog);
@@ -808,7 +817,7 @@ export default function ProxyLogs() {
const detailState = detailById[log.id]; const detailState = detailById[log.id];
const detail = detailState?.data; const detail = detailState?.data;
const detailLog: ProxyLogRenderItem = detail ? { ...log, ...detail } : log; const detailLog: ProxyLogRenderItem = detail ? { ...log, ...detail } : log;
const pathMeta = parseProxyLogPathMeta(detailLog.errorMessage); const pathMeta = parseProxyLogPathMeta(detailLog.errorMessage ?? undefined);
const billingDetailSummary = detail ? formatBillingDetailSummary(detailLog) : null; const billingDetailSummary = detail ? formatBillingDetailSummary(detailLog) : null;
const billingProcessLines = detail ? buildBillingProcessLines(detailLog) : []; const billingProcessLines = detail ? buildBillingProcessLines(detailLog) : [];
const downstreamKeySummary = renderDownstreamKeySummary(detailLog); const downstreamKeySummary = renderDownstreamKeySummary(detailLog);
+1 -1
View File
@@ -43,7 +43,7 @@ export default function SiteAnnouncements() {
const [serverTimeZone, setServerTimeZone] = useState<string | undefined>(undefined); const [serverTimeZone, setServerTimeZone] = useState<string | undefined>(undefined);
const [highlightAnnouncementId, setHighlightAnnouncementId] = useState<number | null>(null); const [highlightAnnouncementId, setHighlightAnnouncementId] = useState<number | null>(null);
const rowRefs = useRef<Map<number, HTMLDivElement>>(new Map()); const rowRefs = useRef<Map<number, HTMLDivElement>>(new Map());
const highlightTimerRef = useRef<number | null>(null); const highlightTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const viewerTimeZone = useMemo(() => readClientTimeZone(), []); const viewerTimeZone = useMemo(() => readClientTimeZone(), []);
const displayTimeZone = resolveSiteAnnouncementTimeZone(viewerTimeZone, serverTimeZone); const displayTimeZone = resolveSiteAnnouncementTimeZone(viewerTimeZone, serverTimeZone);
+2 -1
View File
@@ -238,7 +238,8 @@ export default function TokenRoutes() {
if (candidatesPromiseRef.current && !force) return; if (candidatesPromiseRef.current && !force) return;
const seq = ++candidatesSeqRef.current; const seq = ++candidatesSeqRef.current;
candidatesLoadedRef.current = true; candidatesLoadedRef.current = true;
const promise = (async () => { let promise!: Promise<void>;
promise = (async () => {
try { try {
const candidateRows = await api.getModelTokenCandidates(); const candidateRows = await api.getModelTokenCandidates();
if (candidatesSeqRef.current !== seq) return; // stale if (candidatesSeqRef.current !== seq) return; // stale
+17 -3
View File
@@ -44,6 +44,20 @@ type AccountTokenSyncResult = {
}; };
}; };
type SyncableAccount = {
id: number;
username?: string | null;
accessToken?: string | null;
status?: string | null;
credentialMode?: string | null;
capabilities?: {
proxyOnly?: boolean;
} | null;
site?: {
status?: string | null;
name?: string | null;
} | null;
};
const isAccountSyncable = (account: any) => const isAccountSyncable = (account: any) =>
resolveAccountCredentialMode(account) === 'session' resolveAccountCredentialMode(account) === 'session'
&& account?.status === 'active' && account?.status === 'active'
@@ -162,11 +176,11 @@ export function TokensPanel({ embedded = false, onEmbeddedActionsChange }: Token
const nextTokens = tokenRows || []; const nextTokens = tokenRows || [];
setTokens(nextTokens); setTokens(nextTokens);
setSelectedTokenIds((current) => current.filter((id) => nextTokens.some((token: any) => token.id === id))); setSelectedTokenIds((current) => current.filter((id) => nextTokens.some((token: any) => token.id === id)));
const latestAccounts = accountRows || []; const latestAccounts: SyncableAccount[] = Array.isArray(accountRows) ? accountRows : [];
setAccounts(latestAccounts); setAccounts(latestAccounts);
const syncableAccounts = latestAccounts.filter(isAccountSyncable); const syncableAccounts = latestAccounts.filter(isAccountSyncable);
const hasCurrentSelected = syncableAccounts.some((account) => account.id === syncingAccountId); const hasCurrentSelected = syncableAccounts.some((account: SyncableAccount) => account.id === syncingAccountId);
if (!hasCurrentSelected) { if (!hasCurrentSelected) {
setSyncingAccountId(syncableAccounts[0]?.id || 0); setSyncingAccountId(syncableAccounts[0]?.id || 0);
} }
@@ -209,7 +223,7 @@ export function TokensPanel({ embedded = false, onEmbeddedActionsChange }: Token
api.getAccountTokenGroups(form.accountId) api.getAccountTokenGroups(form.accountId)
.then((res: any) => { .then((res: any) => {
if (cancelled) return; if (cancelled) return;
const groups = Array.isArray(res?.groups) const groups: string[] = Array.isArray(res?.groups)
? res.groups.map((item: any) => String(item || '').trim()).filter(Boolean) ? res.groups.map((item: any) => String(item || '').trim()).filter(Boolean)
: []; : [];
const normalized = Array.from(new Set(groups)); const normalized = Array.from(new Set(groups));
@@ -61,7 +61,7 @@ describe('Accounts batch actions', () => {
}); });
it('refreshes balance for selected accounts through the batch toolbar', async () => { it('refreshes balance for selected accounts through the batch toolbar', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -97,7 +97,7 @@ describe('Accounts batch actions', () => {
}); });
it('selects an account when clicking the row instead of only the checkbox', async () => { it('selects an account when clicking the row instead of only the checkbox', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -147,7 +147,7 @@ describe('Accounts batch actions', () => {
}, },
]); ]);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -31,7 +31,7 @@ async function renderAccounts(initialEntry: string) {
]); ]);
apiMock.getAccountTokens.mockResolvedValue([]); apiMock.getAccountTokens.mockResolvedValue([]);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
await act(async () => { await act(async () => {
root = create( root = create(
<MemoryRouter initialEntries={[initialEntry]}> <MemoryRouter initialEntries={[initialEntry]}>
@@ -64,7 +64,7 @@ describe('Accounts create intent handling', () => {
const selects = root.root.findAllByType(ModernSelect); const selects = root.root.findAllByType(ModernSelect);
expect(selects[1]?.props.value).toBe('10'); expect(selects[1]?.props.value).toBe('10');
} finally { } finally {
root.unmount(); root?.unmount();
} }
}); });
@@ -77,7 +77,7 @@ describe('Accounts create intent handling', () => {
const selects = root.root.findAllByType(ModernSelect); const selects = root.root.findAllByType(ModernSelect);
expect(selects[1]?.props.value).toBe('10'); expect(selects[1]?.props.value).toBe('10');
} finally { } finally {
root.unmount(); root?.unmount();
} }
}); });
@@ -88,7 +88,7 @@ describe('Accounts create intent handling', () => {
expect(rendered).not.toContain('添加 Session 连接'); expect(rendered).not.toContain('添加 Session 连接');
expect(rendered).not.toContain('添加 API Key 连接'); expect(rendered).not.toContain('添加 API Key 连接');
} finally { } finally {
root.unmount(); root?.unmount();
} }
}); });
}); });
+4 -4
View File
@@ -94,7 +94,7 @@ describe('Accounts edit panel', () => {
}); });
it('opens edit panel from account row action', async () => { it('opens edit panel from account row action', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -140,7 +140,7 @@ describe('Accounts edit panel', () => {
disabledCount: 0, disabledCount: 0,
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -205,7 +205,7 @@ describe('Accounts edit panel', () => {
return Promise.resolve({ siteId: accountId, siteName: 'Unknown', models: [] }); return Promise.resolve({ siteId: accountId, siteName: 'Unknown', models: [] });
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -269,7 +269,7 @@ describe('Accounts edit panel', () => {
}); });
apiMock.rebuildRoutes.mockRejectedValue(new Error('rebuild failed')); apiMock.rebuildRoutes.mockRejectedValue(new Error('rebuild failed'));
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -80,7 +80,7 @@ describe('Accounts mobile actions', () => {
}); });
it('supports select-all-visible from the mobile toolbar', async () => { it('supports select-all-visible from the mobile toolbar', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -115,7 +115,7 @@ describe('Accounts mobile actions', () => {
}); });
it('clears only the visible segment selection when toggling mobile select-all off', async () => { it('clears only the visible segment selection when toggling mobile select-all off', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
apiMock.getAccounts.mockResolvedValue([ apiMock.getAccounts.mockResolvedValue([
{ {
@@ -62,7 +62,7 @@ describe('Accounts proxy-only expired state', () => {
]); ]);
apiMock.getSites.mockResolvedValue([{ id: 10, name: '小呆api', platform: 'new-api', status: 'active' }]); apiMock.getSites.mockResolvedValue([{ id: 10, name: '小呆api', platform: 'new-api', status: 'active' }]);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -58,7 +58,7 @@ describe('Accounts rebind modal', () => {
]); ]);
apiMock.getSites.mockResolvedValue([{ id: 10, name: 'Demo Site', platform: 'new-api', status: 'active' }]); apiMock.getSites.mockResolvedValue([{ id: 10, name: 'Demo Site', platform: 'new-api', status: 'active' }]);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -75,7 +75,7 @@ describe('Accounts refresh action', () => {
apiMock.getSites.mockResolvedValue([{ id: 10, name: 'Demo Site', platform: 'new-api', status: 'active' }]); apiMock.getSites.mockResolvedValue([{ id: 10, name: 'Demo Site', platform: 'new-api', status: 'active' }]);
apiMock.refreshBalance.mockRejectedValueOnce(new Error('无权进行此操作,access token 无效')); apiMock.refreshBalance.mockRejectedValueOnce(new Error('无权进行此操作,access token 无效'));
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -67,7 +67,7 @@ describe('Accounts segmented connections view', () => {
]); ]);
apiMock.getAccountTokens.mockResolvedValue([]); apiMock.getAccountTokens.mockResolvedValue([]);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -116,7 +116,7 @@ describe('Accounts segmented connections view', () => {
]); ]);
apiMock.getAccountTokens.mockResolvedValue([]); apiMock.getAccountTokens.mockResolvedValue([]);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -59,7 +59,7 @@ describe('Accounts tokens embedded header', () => {
]); ]);
apiMock.getAccountTokens.mockResolvedValue([]); apiMock.getAccountTokens.mockResolvedValue([]);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -53,7 +53,7 @@ describe('Accounts verify feedback', () => {
]); ]);
apiMock.verifyToken.mockRejectedValueOnce(new Error('Failed to fetch')); apiMock.verifyToken.mockRejectedValueOnce(new Error('Failed to fetch'));
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -74,7 +74,7 @@ describe('Dashboard performance stat card', () => {
modelAnalysis: null, modelAnalysis: null,
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -107,14 +107,14 @@ describe('Dashboard performance stat card', () => {
}); });
it('shows five skeleton stat cards while dashboard data is still loading', async () => { it('shows five skeleton stat cards while dashboard data is still loading', async () => {
let resolveDashboard: ((value: Record<string, unknown>) => void) | null = null; let resolveDashboard: ((value: Record<string, unknown>) => void) | undefined;
apiMock.getDashboard.mockImplementation(() => ( apiMock.getDashboard.mockImplementation(() => (
new Promise((resolve) => { new Promise((resolve) => {
resolveDashboard = resolve as (value: Record<string, unknown>) => void; resolveDashboard = resolve as (value: Record<string, unknown>) => void;
}) })
)); ));
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -133,7 +133,8 @@ describe('Dashboard performance stat card', () => {
expect(statCards).toHaveLength(5); expect(statCards).toHaveLength(5);
} finally { } finally {
resolveDashboard?.({ if (resolveDashboard) {
resolveDashboard({
totalBalance: 0, totalBalance: 0,
totalUsed: 0, totalUsed: 0,
todaySpend: 0, todaySpend: 0,
@@ -145,6 +146,7 @@ describe('Dashboard performance stat card', () => {
performance: { windowSeconds: 60, requestsPerMinute: 0, tokensPerMinute: 0 }, performance: { windowSeconds: 60, requestsPerMinute: 0, tokensPerMinute: 0 },
modelAnalysis: null, modelAnalysis: null,
}); });
}
root?.unmount(); root?.unmount();
} }
}); });
@@ -85,7 +85,7 @@ describe('Dashboard site observability panel', () => {
}); });
it('renders site availability strips and summary metrics', async () => { it('renders site availability strips and summary metrics', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -69,7 +69,7 @@ describe('Dashboard site speed buttons', () => {
}); });
it('updates site speed status without imperative document lookups', async () => { it('updates site speed status without imperative document lookups', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -99,7 +99,7 @@ describe('Dashboard site speed buttons', () => {
}); });
it('updates bulk site speed status without imperative document lookups', async () => { it('updates bulk site speed status without imperative document lookups', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
+1 -1
View File
@@ -40,7 +40,7 @@ describe('Dashboard hook order', () => {
}); });
it('keeps hook order stable when switching from loading to loaded render', async () => { it('keeps hook order stable when switching from loading to loaded render', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
let renderedTree = ''; let renderedTree = '';
const dashboardData = { const dashboardData = {
totalBalance: 0, totalBalance: 0,
@@ -180,7 +180,7 @@ describe('DownstreamKeys mobile layout', () => {
}); });
it('renders mobile cards and supports select-all-visible batch actions', async () => { it('renders mobile cards and supports select-all-visible batch actions', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
+8 -6
View File
@@ -72,7 +72,7 @@ describe('buildSiteSaveAction', () => {
it('throws when edit mode has no site id', () => { it('throws when edit mode has no site id', () => {
expect(() => expect(() =>
buildSiteSaveAction( buildSiteSaveAction(
{ mode: 'edit' }, { mode: 'edit' } as unknown as Parameters<typeof buildSiteSaveAction>[0],
{ {
name: 'site-c', name: 'site-c',
url: 'https://c.example.com', url: 'https://c.example.com',
@@ -88,10 +88,7 @@ describe('buildSiteSaveAction', () => {
}); });
it('does not expose deprecated apiKey in site editor state', () => { it('does not expose deprecated apiKey in site editor state', () => {
expect(emptySiteForm()).not.toHaveProperty('apiKey'); const legacySite = {
expect(emptySiteForm().customHeaders).toEqual([emptySiteCustomHeader()]);
expect(emptySiteForm().proxyUrl).toBe('');
expect(siteFormFromSite({
name: 'site-d', name: 'site-d',
url: 'https://d.example.com', url: 'https://d.example.com',
externalCheckinUrl: null, externalCheckinUrl: null,
@@ -100,7 +97,12 @@ describe('buildSiteSaveAction', () => {
customHeaders: '{"x-site-token":"alpha"}', customHeaders: '{"x-site-token":"alpha"}',
globalWeight: 1, globalWeight: 1,
apiKey: 'sk-legacy-site-key', apiKey: 'sk-legacy-site-key',
})).not.toHaveProperty('apiKey'); } as unknown as Parameters<typeof siteFormFromSite>[0];
expect(emptySiteForm()).not.toHaveProperty('apiKey');
expect(emptySiteForm().customHeaders).toEqual([emptySiteCustomHeader()]);
expect(emptySiteForm().proxyUrl).toBe('');
expect(siteFormFromSite(legacySite)).not.toHaveProperty('apiKey');
expect(siteFormFromSite({ expect(siteFormFromSite({
proxyUrl: 'http://127.0.0.1:8080', proxyUrl: 'http://127.0.0.1:8080',
}).proxyUrl).toBe('http://127.0.0.1:8080'); }).proxyUrl).toBe('http://127.0.0.1:8080');
+2 -2
View File
@@ -105,7 +105,7 @@ describe('Models mobile layout', () => {
}); });
it('keeps a mobile filter entry and hides the table-view toggle on small screens', async () => { it('keeps a mobile filter entry and hides the table-view toggle on small screens', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -130,7 +130,7 @@ describe('Models mobile layout', () => {
}); });
it('renders stacked account detail cards instead of tables when a mobile card expands', async () => { it('renders stacked account detail cards instead of tables when a mobile card expands', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
+1 -1
View File
@@ -68,7 +68,7 @@ describe('ProgramLogs mobile layout', () => {
}); });
it('renders mobile cards instead of only the desktop table on small screens', async () => { it('renders mobile cards instead of only the desktop table on small screens', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -116,7 +116,7 @@ describe('Settings factory reset', () => {
}); });
it('shows a 3 second danger confirmation and clears local state after reset', async () => { it('shows a 3 second danger confirmation and clears local state after reset', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -77,7 +77,7 @@ describe('Settings log cleanup schedule', () => {
}); });
it('saves schedule mode and interval fields together with other schedule settings', async () => { it('saves schedule mode and interval fields together with other schedule settings', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -119,7 +119,7 @@ describe('Settings log cleanup schedule', () => {
it('triggers a one-off checkin from the schedule card', async () => { it('triggers a one-off checkin from the schedule card', async () => {
apiMock.triggerCheckinAll.mockResolvedValue({ success: true }); apiMock.triggerCheckinAll.mockResolvedValue({ success: true });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -150,7 +150,7 @@ describe('Settings log cleanup schedule', () => {
}); });
it('renders schedule mode controls with modern selects and ghost action styling', async () => { it('renders schedule mode controls with modern selects and ghost action styling', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
+2 -2
View File
@@ -87,7 +87,7 @@ describe('Settings system proxy', () => {
}); });
it('saves system proxy url from settings', async () => { it('saves system proxy url from settings', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -127,7 +127,7 @@ describe('Settings system proxy', () => {
}); });
it('tests system proxy from settings', async () => { it('tests system proxy from settings', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
+1 -1
View File
@@ -85,7 +85,7 @@ describe('SiteAnnouncements page', () => {
}); });
it('loads announcements, highlights the focused row, and clears local rows', async () => { it('loads announcements, highlights the focused row, and clears local rows', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
+1 -1
View File
@@ -42,7 +42,7 @@ async function createSiteAndCollectLocation(createdSite: { id: number; platform?
apiMock.getSites.mockResolvedValue([]); apiMock.getSites.mockResolvedValue([]);
apiMock.addSite.mockResolvedValue(createdSite); apiMock.addSite.mockResolvedValue(createdSite);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -70,8 +70,8 @@ describe('Sites disabled models save', () => {
vi.clearAllMocks(); vi.clearAllMocks();
}); });
async function renderSitesEditor() { async function renderSitesEditor(): Promise<WebTestRenderer> {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
await act(async () => { await act(async () => {
root = create( root = create(
<MemoryRouter initialEntries={['/sites']}> <MemoryRouter initialEntries={['/sites']}>
@@ -83,7 +83,7 @@ describe('Sites disabled models save', () => {
}); });
await flushMicrotasks(); await flushMicrotasks();
const editButton = root.root.find((node) => ( const editButton = root.root.find((node: ReactTestInstance) => (
node.type === 'button' node.type === 'button'
&& typeof node.props.onClick === 'function' && typeof node.props.onClick === 'function'
&& collectText(node).trim() === '编辑' && collectText(node).trim() === '编辑'
@@ -100,7 +100,7 @@ describe('Sites disabled models save', () => {
it('reports route rebuild failure after saving disabled models from sites editor', async () => { it('reports route rebuild failure after saving disabled models from sites editor', async () => {
apiMock.rebuildRoutes.mockRejectedValue(new Error('rebuild failed')); apiMock.rebuildRoutes.mockRejectedValue(new Error('rebuild failed'));
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
root = await renderSitesEditor(); root = await renderSitesEditor();
@@ -129,11 +129,11 @@ describe('Sites disabled models save', () => {
apiMock.getSiteAvailableModels.mockRejectedValue(new Error('available models failed')); apiMock.getSiteAvailableModels.mockRejectedValue(new Error('available models failed'));
apiMock.rebuildRoutes.mockResolvedValue({ success: true }); apiMock.rebuildRoutes.mockResolvedValue({ success: true });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
root = await renderSitesEditor(); root = await renderSitesEditor();
const saveButton = root.root.find((node) => ( const saveButton = root.root.find((node: ReactTestInstance) => (
node.type === 'button' node.type === 'button'
&& typeof node.props.onClick === 'function' && typeof node.props.onClick === 'function'
&& collectText(node).includes('保存禁用列表') && collectText(node).includes('保存禁用列表')
@@ -153,11 +153,11 @@ describe('Sites disabled models save', () => {
it('allows adding a disabled model manually when no available models are discovered', async () => { it('allows adding a disabled model manually when no available models are discovered', async () => {
apiMock.rebuildRoutes.mockResolvedValue({ success: true }); apiMock.rebuildRoutes.mockResolvedValue({ success: true });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
root = await renderSitesEditor(); root = await renderSitesEditor();
const manualInput = root.root.find((node) => ( const manualInput = root.root.find((node: ReactTestInstance) => (
node.type === 'input' node.type === 'input'
&& node.props.placeholder === '输入模型名称,如 gpt-4o' && node.props.placeholder === '输入模型名称,如 gpt-4o'
)); ));
@@ -166,7 +166,7 @@ describe('Sites disabled models save', () => {
manualInput.props.onChange({ target: { value: 'gpt-4o' } }); manualInput.props.onChange({ target: { value: 'gpt-4o' } });
}); });
const addButton = root.root.find((node) => ( const addButton = root.root.find((node: ReactTestInstance) => (
node.type === 'button' node.type === 'button'
&& typeof node.props.onClick === 'function' && typeof node.props.onClick === 'function'
&& collectText(node).trim() === '添加模型' && collectText(node).trim() === '添加模型'
@@ -177,7 +177,7 @@ describe('Sites disabled models save', () => {
}); });
await flushMicrotasks(); await flushMicrotasks();
const saveButton = root.root.find((node) => ( const saveButton = root.root.find((node: ReactTestInstance) => (
node.type === 'button' node.type === 'button'
&& typeof node.props.onClick === 'function' && typeof node.props.onClick === 'function'
&& collectText(node).includes('保存禁用列表') && collectText(node).includes('保存禁用列表')
+2 -2
View File
@@ -58,7 +58,7 @@ describe('Sites edit behavior', () => {
value: scrollToMock, value: scrollToMock,
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -91,7 +91,7 @@ describe('Sites edit behavior', () => {
it('restores header add button label after closing add modal', async () => { it('restores header add button label after closing add modal', async () => {
apiMock.getSites.mockResolvedValue([]); apiMock.getSites.mockResolvedValue([]);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
+1 -1
View File
@@ -59,7 +59,7 @@ describe('Sites mobile actions', () => {
}); });
it('supports select-all-visible and preserves the primary site url in mobile cards', async () => { it('supports select-all-visible and preserves the primary site url in mobile cards', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -55,7 +55,7 @@ describe('Sites system proxy bulk actions', () => {
}); });
it('sends selected site ids to enable system proxy', async () => { it('sends selected site ids to enable system proxy', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -92,7 +92,7 @@ describe('Sites system proxy bulk actions', () => {
}); });
it('selects a site when clicking the row instead of only the checkbox', async () => { it('selects a site when clicking the row instead of only the checkbox', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
+1
View File
@@ -1,6 +1,7 @@
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import type { BrandInfo } from '../../components/BrandIcon.js'; import type { BrandInfo } from '../../components/BrandIcon.js';
import type { RouteDecision, RouteDecisionCandidate, RouteMode } from '../../../shared/tokenRouteContract.js'; import type { RouteDecision, RouteDecisionCandidate, RouteMode } from '../../../shared/tokenRouteContract.js';
export type { RouteDecision, RouteDecisionCandidate, RouteMode } from '../../../shared/tokenRouteContract.js';
export type RouteSortBy = 'modelPattern' | 'channelCount'; export type RouteSortBy = 'modelPattern' | 'channelCount';
export type RouteSortDir = 'asc' | 'desc'; export type RouteSortDir = 'asc' | 'desc';
@@ -25,8 +25,11 @@ export function useRouteChannels() {
} }
}, []); }, []);
const invalidateChannels = useCallback((routeId: number) => { const invalidateChannels = useCallback((routeId?: number) => {
setChannelsByRouteId((prev) => { setChannelsByRouteId((prev) => {
if (routeId === undefined) {
return {};
}
const next = { ...prev }; const next = { ...prev };
delete next[routeId]; delete next[routeId];
return next; return next;
@@ -122,7 +122,7 @@ describe('TokenRoutes grouped source models', () => {
}, },
]); ]);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -181,7 +181,7 @@ describe('TokenRoutes grouped source models', () => {
]); ]);
apiMock.getRouteChannels.mockResolvedValue(channels); apiMock.getRouteChannels.mockResolvedValue(channels);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -241,7 +241,7 @@ describe('TokenRoutes grouped source models', () => {
]); ]);
apiMock.getRouteChannels.mockResolvedValue(channels); apiMock.getRouteChannels.mockResolvedValue(channels);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -296,7 +296,7 @@ describe('TokenRoutes grouped source models', () => {
}); });
apiMock.getRouteChannels.mockResolvedValue([]); apiMock.getRouteChannels.mockResolvedValue([]);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -336,7 +336,7 @@ describe('TokenRoutes grouped source models', () => {
}, },
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -382,7 +382,7 @@ describe('TokenRoutes grouped source models', () => {
}, },
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -449,7 +449,7 @@ describe('TokenRoutes grouped source models', () => {
}, },
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -497,7 +497,7 @@ describe('TokenRoutes grouped source models', () => {
}); });
apiMock.getRouteChannels.mockResolvedValue([]); apiMock.getRouteChannels.mockResolvedValue([]);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -543,7 +543,7 @@ describe('TokenRoutes grouped source models', () => {
}, },
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -594,7 +594,7 @@ describe('TokenRoutes grouped source models', () => {
}, },
]); ]);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -640,7 +640,7 @@ describe('TokenRoutes grouped source models', () => {
endpointTypesByModel: {}, endpointTypesByModel: {},
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -683,7 +683,7 @@ describe('TokenRoutes grouped source models', () => {
endpointTypesByModel: {}, endpointTypesByModel: {},
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -735,7 +735,7 @@ describe('TokenRoutes grouped source models', () => {
}, },
]); ]);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -774,7 +774,7 @@ describe('TokenRoutes grouped source models', () => {
}, },
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -824,7 +824,7 @@ describe('TokenRoutes grouped source models', () => {
}, },
]); ]);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -855,7 +855,7 @@ describe('TokenRoutes grouped source models', () => {
}, },
]); ]);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -928,7 +928,7 @@ describe('TokenRoutes grouped source models', () => {
}, },
}); });
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -1035,7 +1035,7 @@ describe('TokenRoutes grouped source models', () => {
}, },
]); ]);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -1089,7 +1089,7 @@ describe('TokenRoutes grouped source models', () => {
}, },
]); ]);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -1159,7 +1159,7 @@ describe('TokenRoutes grouped source models', () => {
}, },
]); ]);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -1231,7 +1231,7 @@ describe('TokenRoutes grouped source models', () => {
]); ]);
apiMock.getRouteChannels.mockResolvedValue([]); apiMock.getRouteChannels.mockResolvedValue([]);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -1317,7 +1317,7 @@ describe('TokenRoutes grouped source models', () => {
]); ]);
apiMock.getRouteChannels.mockResolvedValue([]); apiMock.getRouteChannels.mockResolvedValue([]);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -1412,7 +1412,7 @@ describe('TokenRoutes grouped source models', () => {
}, },
]); ]);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -137,7 +137,7 @@ describe('TokenRoutes mobile layout', () => {
}); });
it('shows direct mobile actions and reveals the management panel after expansion', async () => { it('shows direct mobile actions and reveals the management panel after expansion', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -178,7 +178,7 @@ describe('TokenRoutes mobile layout', () => {
}); });
it('lets mobile users toggle route enabled state from the summary card', async () => { it('lets mobile users toggle route enabled state from the summary card', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
+1 -1
View File
@@ -125,7 +125,7 @@ describe('TokenRoutes mobile actions', () => {
}); });
it('shows mobile detail expansion and direct management actions', async () => { it('shows mobile detail expansion and direct management actions', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
@@ -81,7 +81,7 @@ describe('TokenRoutes refresh decision action', () => {
}); });
it('passes refreshPricingCatalog and persistSnapshots when user clicks refresh selection probability', async () => { it('passes refreshPricingCatalog and persistSnapshots when user clicks refresh selection probability', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -79,7 +79,7 @@ describe('TokenRoutes routing strategy updates', () => {
}); });
it('keeps the optimistic routing strategy when refresh fails after a successful save', async () => { it('keeps the optimistic routing strategy when refresh fails after a successful save', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -129,7 +129,7 @@ describe('TokenRoutes routing strategy updates', () => {
}); });
it('supports switching to stable_first and keeps the optimistic label when refresh fails', async () => { it('supports switching to stable_first and keeps the optimistic label when refresh fails', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -113,7 +113,7 @@ describe('TokenRoutes cached snapshot load', () => {
}); });
it('shows cached probabilities immediately from getRoutes snapshot data', async () => { it('shows cached probabilities immediately from getRoutes snapshot data', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
+2 -2
View File
@@ -71,7 +71,7 @@ describe('Tokens batch actions', () => {
}); });
it('deletes selected tokens through the batch toolbar', async () => { it('deletes selected tokens through the batch toolbar', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -117,7 +117,7 @@ describe('Tokens batch actions', () => {
}); });
it('selects a token when clicking the row instead of only the checkbox', async () => { it('selects a token when clicking the row instead of only the checkbox', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
@@ -143,7 +143,7 @@ describe('Tokens edit modal and row selection', () => {
}); });
it('opens the centered edit modal when editing a token', async () => { it('opens the centered edit modal when editing a token', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = buildRoot(); root = buildRoot();
@@ -191,7 +191,7 @@ describe('Tokens edit modal and row selection', () => {
}); });
it('selects a token when clicking the row body, but not when clicking an action button', async () => { it('selects a token when clicking the row body, but not when clicking an action button', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = buildRoot(); root = buildRoot();
@@ -232,7 +232,7 @@ describe('Tokens edit modal and row selection', () => {
it('does not repeatedly refetch groups when edit-group loading fails once', async () => { it('does not repeatedly refetch groups when edit-group loading fails once', async () => {
apiMock.getAccountTokenGroups.mockRejectedValueOnce(new Error('账号会话可能已过期,请重新登录后再拉取分组')); apiMock.getAccountTokenGroups.mockRejectedValueOnce(new Error('账号会话可能已过期,请重新登录后再拉取分组'));
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = buildRoot(); root = buildRoot();
@@ -274,7 +274,7 @@ describe('Tokens edit modal and row selection', () => {
]); ]);
apiMock.getAccountTokenValue.mockRejectedValueOnce(new Error('当前仅保存了脱敏令牌,无法展开/复制。请在站点重新生成并同步,或手动更新为完整令牌。')); apiMock.getAccountTokenValue.mockRejectedValueOnce(new Error('当前仅保存了脱敏令牌,无法展开/复制。请在站点重新生成并同步,或手动更新为完整令牌。'));
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = buildTokensRoot(); root = buildTokensRoot();
@@ -356,7 +356,7 @@ describe('Tokens edit modal and row selection', () => {
}); });
apiMock.getAccountTokenValue.mockRejectedValueOnce(new Error('当前仅保存了脱敏令牌,无法展开/复制。请在站点重新生成并同步,或手动更新为完整令牌。')); apiMock.getAccountTokenValue.mockRejectedValueOnce(new Error('当前仅保存了脱敏令牌,无法展开/复制。请在站点重新生成并同步,或手动更新为完整令牌。'));
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = buildTokensRoot(); root = buildTokensRoot();
+1 -1
View File
@@ -71,7 +71,7 @@ describe('Tokens focus navigation', () => {
}, },
]); ]);
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
+1 -1
View File
@@ -77,7 +77,7 @@ describe('Tokens mobile actions', () => {
}); });
it('supports select-all-visible and expandable mobile token details', async () => { it('supports select-all-visible and expandable mobile token details', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
try { try {
await act(async () => { await act(async () => {
root = create( root = create(
+1 -1
View File
@@ -23,7 +23,7 @@ function LocationProbe() {
describe('Tokens legacy route redirect', () => { describe('Tokens legacy route redirect', () => {
it('redirects /tokens to the merged accounts tokens segment', async () => { it('redirects /tokens to the merged accounts tokens segment', async () => {
let root: ReturnType<typeof create> | null = null; let root!: WebTestRenderer;
await act(async () => { await act(async () => {
root = create( root = create(
<ToastProvider> <ToastProvider>
+1
View File
@@ -0,0 +1 @@
type WebTestRenderer = import('react-test-renderer').ReactTestRenderer;
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true
},
"include": ["src/web/**/*.ts", "src/web/**/*.tsx"],
"exclude": ["src/web/**/*.test.ts", "src/web/**/*.test.tsx"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true
},
"include": ["src/web/**/*.test.ts", "src/web/**/*.test.tsx", "src/web/**/*.d.ts"]
}