From 3e57a8432de926da97052c9e29068510e79733bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B5=E6=91=87=E5=B0=8F=E5=AD=90?= <969409112@qq.com> Date: Sat, 1 Aug 2026 23:50:53 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E5=BB=BA=E7=AB=8B=E6=A1=8C=E9=9D=A2?= =?UTF-8?q?=E7=AB=AF=E8=87=AA=E5=8A=A8=E5=8C=96=E5=9B=9E=E5=BD=92=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E4=BD=93=E7=B3=BB=E5=B9=B6=E5=AE=8C=E5=96=84=E8=B7=A8?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 74267ae2f63f8256c3da84b04f5b330e6e9d4c67) --- .env.example | 4 + .github/workflows/test.yml | 76 ++ .gitignore | 3 + package.json | 21 +- playwright.config.ts | 22 + pnpm-lock.yaml | 815 +++++++++++++++++- src/main/app-logger.ts | 3 +- src/main/image-decrypt-service.ts | 26 +- src/renderer/src/App.tsx | 24 +- src/renderer/src/utils/message-pages.ts | 20 + tests/component/database-connection.test.tsx | 55 ++ tests/component/empty-state.test.tsx | 18 + tests/component/image-bubble.test.tsx | 53 ++ tests/component/navigation.test.tsx | 21 + tests/component/voice-player.test.tsx | 47 + .../win32/visual.spec.ts/archive-page.png | Bin 0 -> 58566 bytes .../win32/visual.spec.ts/login-page.png | Bin 0 -> 74638 bytes tests/e2e/app.spec.ts | 289 +++++++ tests/e2e/support/electron-main.cjs | 397 +++++++++ tests/e2e/support/electron.ts | 57 ++ tests/e2e/visual.spec.ts | 37 + tests/fixtures/chat-data.json | 97 +++ tests/integration/preload-contract.test.ts | 76 ++ tests/setup/component.ts | 15 + tests/smoke/native-environment.test.mjs | 23 + tests/stability-compat.test.mjs | 64 -- tests/unit/bootstrap-cache.test.ts | 81 ++ tests/unit/image-decrypt.test.ts | 56 ++ tests/unit/key-validation.test.ts | 59 ++ tests/unit/message-pagination.test.ts | 23 + tests/unit/message-parser.test.ts | 38 + tests/unit/message-state.test.ts | 81 ++ tests/unit/security-and-errors.test.ts | 53 ++ vitest.component.config.ts | 20 + vitest.integration.config.ts | 17 + vitest.unit.config.ts | 17 + 36 files changed, 2598 insertions(+), 110 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 playwright.config.ts create mode 100644 src/renderer/src/utils/message-pages.ts create mode 100644 tests/component/database-connection.test.tsx create mode 100644 tests/component/empty-state.test.tsx create mode 100644 tests/component/image-bubble.test.tsx create mode 100644 tests/component/navigation.test.tsx create mode 100644 tests/component/voice-player.test.tsx create mode 100644 tests/e2e/__screenshots__/win32/visual.spec.ts/archive-page.png create mode 100644 tests/e2e/__screenshots__/win32/visual.spec.ts/login-page.png create mode 100644 tests/e2e/app.spec.ts create mode 100644 tests/e2e/support/electron-main.cjs create mode 100644 tests/e2e/support/electron.ts create mode 100644 tests/e2e/visual.spec.ts create mode 100644 tests/fixtures/chat-data.json create mode 100644 tests/integration/preload-contract.test.ts create mode 100644 tests/setup/component.ts create mode 100644 tests/smoke/native-environment.test.mjs delete mode 100644 tests/stability-compat.test.mjs create mode 100644 tests/unit/bootstrap-cache.test.ts create mode 100644 tests/unit/image-decrypt.test.ts create mode 100644 tests/unit/key-validation.test.ts create mode 100644 tests/unit/message-pagination.test.ts create mode 100644 tests/unit/message-parser.test.ts create mode 100644 tests/unit/message-state.test.ts create mode 100644 tests/unit/security-and-errors.test.ts create mode 100644 vitest.component.config.ts create mode 100644 vitest.integration.config.ts create mode 100644 vitest.unit.config.ts diff --git a/.env.example b/.env.example index cd56eb5..baeded7 100644 --- a/.env.example +++ b/.env.example @@ -22,3 +22,7 @@ VITE_FILTER_MSG_TYPES= # AES Key: 16-character string, derived from wxid and code VITE_IMAGE_XOR_KEY= VITE_IMAGE_AES_KEY= + +# Electron E2E test window close delay in milliseconds. +# Local default: 2000 (2 seconds). Set to 0 for immediate close. +WXE_E2E_CLOSE_DELAY_MS=2000 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..f5066f6 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,76 @@ +name: Tests + +on: + push: + pull_request: + +jobs: + desktop-tests: + name: ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [windows-latest, macos-latest] + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 7.33.7 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - uses: actions/setup-go@v5 + with: + go-version-file: services/wechat-connector/go.mod + cache-dependency-path: services/wechat-connector/go.sum + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Type check + run: pnpm typecheck + + - name: Unit tests + run: pnpm test:unit + + - name: Component tests + run: pnpm test:component + + - name: IPC integration tests + run: pnpm test:integration + + - name: Skill installation instruction tests + run: pnpm test:skill-install + + - name: WeChat connector tests + run: pnpm test:wechat-connector + + - name: Build Electron test application + run: pnpm test:e2e:build + + - name: Electron E2E tests + run: pnpm exec playwright test --grep-invert @visual + env: + WXE_E2E_CLOSE_DELAY_MS: 0 + + - name: Platform visual regression + run: pnpm exec playwright test tests/e2e/visual.spec.ts + env: + WXE_E2E_CLOSE_DELAY_MS: 0 + + - name: Upload Playwright diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-${{ matrix.os }} + path: | + test-results/ + playwright-report/ + if-no-files-found: ignore + retention-days: 14 diff --git a/.gitignore b/.gitignore index f69de56..5ee19a7 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ out .DS_Store .eslintcache *.log* +coverage/ +playwright-report/ +test-results/ resources/connectors/wechat/ .omc .codex/ diff --git a/package.json b/package.json index f31fe44..a32ac75 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ }, "main": "./out/main/index.js", "scripts": { + "test": "pnpm typecheck && pnpm test:unit && pnpm test:component && pnpm test:integration && pnpm test:skill-install && pnpm test:wechat-connector && pnpm test:e2e:build && playwright test", "format": "prettier --write .", "lint": "eslint --cache .", "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", @@ -28,7 +29,13 @@ "start": "electron-vite preview", "dev": "node scripts/ensure-env.cjs && node scripts/build-wechat-connector.cjs && electron-vite dev", "test:wechat-connector": "go -C services/wechat-connector test ./... && go -C services/wechat-connector vet ./...", - "test:stability": "node --experimental-strip-types --test tests/stability-compat.test.mjs", + "test:unit": "vitest run --config vitest.unit.config.ts", + "test:component": "vitest run --config vitest.component.config.ts", + "test:integration": "vitest run --config vitest.integration.config.ts", + "test:e2e:build": "electron-vite build", + "test:e2e": "pnpm test:e2e:build && playwright test --grep-invert @visual", + "test:visual": "pnpm test:e2e:build && playwright test tests/e2e/visual.spec.ts", + "test:smoke": "node --test tests/smoke/native-environment.test.mjs", "build:wechat-connector": "node scripts/build-wechat-connector.cjs", "build:wechat-connector:win": "node scripts/build-wechat-connector.cjs --platform win32 --arch x64,arm64", "build:wechat-connector:mac": "node scripts/build-wechat-connector.cjs --platform darwin --arch x64,arm64", @@ -47,11 +54,11 @@ "build:linux": "electron-vite build && electron-builder --config electron-builder.yml --linux" }, "dependencies": { - "cross-env": "^10.1.0", "@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/utils": "^4.0.0", "@koromix/koffi-win32-x64": "3.1.0", "@tanstack/react-virtual": "^3.14.6", + "cross-env": "^10.1.0", "electron-updater": "^6.6.2", "fs-extra": "^11.3.2", "fzstd": "^0.1.1", @@ -65,12 +72,18 @@ "@electron-toolkit/eslint-config-prettier": "^3.0.0", "@electron-toolkit/eslint-config-ts": "^3.1.0", "@electron-toolkit/tsconfig": "^2.0.0", + "@playwright/test": "^1.62.1", "@rollup/rollup-darwin-arm64": "^4.62.2", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^7.0.0", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/fs-extra": "^11.0.4", "@types/node": "^22.19.1", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", + "@vitest/coverage-v8": "^4.1.10", "electron": "^43.0.0", "electron-builder": "^26.0.12", "electron-vite": "^5.0.0", @@ -78,12 +91,14 @@ "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", + "jsdom": "^30.0.1", "prettier": "^3.7.4", "react": "^19.2.1", "react-dom": "^19.2.1", "sass": "^1.102.0", "typescript": "^5.9.3", - "vite": "^7.2.6" + "vite": "^7.2.6", + "vitest": "^4.1.10" }, "pnpm": { "supportedArchitectures": { diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..81574fc --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from '@playwright/test' + +export default defineConfig({ + testDir: './tests/e2e', + testMatch: /.*\.spec\.ts/, + timeout: 45_000, + expect: { timeout: 8_000 }, + fullyParallel: false, + workers: 1, + forbidOnly: Boolean(process.env.CI), + retries: process.env.CI ? 1 : 0, + reporter: process.env.CI + ? [['line'], ['html', { outputFolder: 'playwright-report', open: 'never' }]] + : [['list'], ['html', { outputFolder: 'playwright-report', open: 'never' }]], + outputDir: 'test-results', + snapshotPathTemplate: 'tests/e2e/__screenshots__/{platform}/{testFilePath}/{arg}{ext}', + use: { + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + video: 'retain-on-failure' + } +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a890ac1..fc19f3a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,13 +11,19 @@ specifiers: '@electron-toolkit/tsconfig': ^2.0.0 '@electron-toolkit/utils': ^4.0.0 '@koromix/koffi-win32-x64': 3.1.0 + '@playwright/test': ^1.62.1 '@rollup/rollup-darwin-arm64': ^4.62.2 '@tanstack/react-virtual': ^3.14.6 + '@testing-library/dom': ^10.4.1 + '@testing-library/jest-dom': ^7.0.0 + '@testing-library/react': ^16.3.2 + '@testing-library/user-event': ^14.6.1 '@types/fs-extra': ^11.0.4 '@types/node': ^22.19.1 '@types/react': ^19.2.7 '@types/react-dom': ^19.2.3 '@vitejs/plugin-react': ^5.1.1 + '@vitest/coverage-v8': ^4.1.10 cross-env: ^10.1.0 electron: ^43.0.0 electron-builder: ^26.0.12 @@ -29,6 +35,7 @@ specifiers: eslint-plugin-react-refresh: ^0.4.24 fs-extra: ^11.3.2 fzstd: ^0.1.1 + jsdom: ^30.0.1 jsonrepair: ^3.15.0 koffi: ^3.1.0 openai: ^6.10.0 @@ -39,6 +46,7 @@ specifiers: silk-wasm: ^3.7.1 typescript: ^5.9.3 vite: ^7.2.6 + vitest: ^4.1.10 wechat-emojis: ^1.0.2 dependencies: @@ -60,12 +68,18 @@ devDependencies: '@electron-toolkit/eslint-config-prettier': 3.0.0_fiitszekoa4sqtbwyewxi6kyy4 '@electron-toolkit/eslint-config-ts': 3.1.0_ohw7ybem3stexzc2ktgcr2xgzi '@electron-toolkit/tsconfig': 2.0.0_@types+node@22.19.2 + '@playwright/test': 1.62.1 '@rollup/rollup-darwin-arm64': 4.62.2 + '@testing-library/dom': 10.4.1 + '@testing-library/jest-dom': 7.0.0_rwcxf7kajngkz26znmtz643644 + '@testing-library/react': 16.3.2_ifbmwgqdqpqwyvhobr7vc7fmyu + '@testing-library/user-event': 14.6.1_rwcxf7kajngkz26znmtz643644 '@types/fs-extra': 11.0.4 '@types/node': 22.19.2 '@types/react': 19.2.7 '@types/react-dom': 19.2.3_@types+react@19.2.7 '@vitejs/plugin-react': 5.1.2_vite@7.2.7 + '@vitest/coverage-v8': 4.1.10_vitest@4.1.10 electron: 43.1.0 electron-builder: 26.0.12 electron-vite: 5.0.0_vite@7.2.7 @@ -73,12 +87,14 @@ devDependencies: eslint-plugin-react: 7.37.5_eslint@9.39.1 eslint-plugin-react-hooks: 7.0.1_eslint@9.39.1 eslint-plugin-react-refresh: 0.4.24_eslint@9.39.1 + jsdom: 30.0.1 prettier: 3.7.4 react: 19.2.1 react-dom: 19.2.1_react@19.2.1 sass: 1.102.0 typescript: 5.9.3 vite: 7.2.7_kx7yyowhop237vgn7dp2p54shq + vitest: 4.1.10_vx7uwpt5cwqrhusdpoa262p5gm packages: @@ -86,11 +102,36 @@ packages: resolution: {integrity: sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==} dev: true + /@adobe/css-tools/4.5.0: + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + dev: true + + /@asamuzakjp/css-color/6.0.5: + resolution: {integrity: sha512-mbhpPMmnw/kwW19aRNmSUl1QzLbdGo1SCuE49BT98MNwqF6zaHb3o2owssFc/PEO/4t2UjqtCNwocuDtJornzA==} + engines: {node: ^22.13.0 || >=24.0.0} + dependencies: + '@csstools/css-calc': 3.3.0_pa3hd4j4wjku7uaqbbhzkpahzy + '@csstools/css-color-parser': 4.1.10_pa3hd4j4wjku7uaqbbhzkpahzy + '@csstools/css-parser-algorithms': 4.0.0_itoglcgmvufaigicydd4iuewyu + '@csstools/css-tokenizer': 4.0.0 + lru-cache: 11.5.2 + dev: true + + /@asamuzakjp/dom-selector/8.3.0: + resolution: {integrity: sha512-UJLfKXBhrc8i1vH2eJXuYQMwlsLKWFw3O+CPqXSuVEiikeAim3UgrfWX0k4tA/X8cRFM8iZ7OaqBokFGbYusdg==} + engines: {node: ^22.13.0 || >=24.0.0} + dependencies: + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + dev: true + /@babel/code-frame/7.27.1: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 dev: true @@ -168,7 +209,7 @@ packages: dependencies: '@babel/core': 7.28.5 '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -184,8 +225,13 @@ packages: engines: {node: '>=6.9.0'} dev: true - /@babel/helper-validator-identifier/7.28.5: - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + /@babel/helper-string-parser/7.29.7: + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier/7.29.7: + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} dev: true @@ -210,6 +256,14 @@ packages: '@babel/types': 7.28.5 dev: true + /@babel/parser/7.29.8: + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.29.8 + dev: true + /@babel/plugin-transform-arrow-functions/7.27.1_@babel+core@7.28.5: resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==} engines: {node: '>=6.9.0'} @@ -240,6 +294,11 @@ packages: '@babel/helper-plugin-utils': 7.27.1 dev: true + /@babel/runtime/7.29.7: + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + dev: true + /@babel/template/7.27.2: resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -269,7 +328,81 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 + dev: true + + /@babel/types/7.29.8: + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + dev: true + + /@bcoe/v8-coverage/1.0.2: + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + dev: true + + /@bramus/specificity/2.4.2: + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + dependencies: + css-tree: 3.2.1 + dev: true + + /@csstools/color-helpers/6.1.0: + resolution: {integrity: sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==} + engines: {node: '>=20.19.0'} + dev: true + + /@csstools/css-calc/3.3.0_pa3hd4j4wjku7uaqbbhzkpahzy: + resolution: {integrity: sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + dependencies: + '@csstools/css-parser-algorithms': 4.0.0_itoglcgmvufaigicydd4iuewyu + '@csstools/css-tokenizer': 4.0.0 + dev: true + + /@csstools/css-color-parser/4.1.10_pa3hd4j4wjku7uaqbbhzkpahzy: + resolution: {integrity: sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + dependencies: + '@csstools/color-helpers': 6.1.0 + '@csstools/css-calc': 3.3.0_pa3hd4j4wjku7uaqbbhzkpahzy + '@csstools/css-parser-algorithms': 4.0.0_itoglcgmvufaigicydd4iuewyu + '@csstools/css-tokenizer': 4.0.0 + dev: true + + /@csstools/css-parser-algorithms/4.0.0_itoglcgmvufaigicydd4iuewyu: + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + dependencies: + '@csstools/css-tokenizer': 4.0.0 + dev: true + + /@csstools/css-syntax-patches-for-csstree/1.1.7_css-tree@3.2.1: + resolution: {integrity: sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + dependencies: + css-tree: 3.2.1 + dev: true + + /@csstools/css-tokenizer/4.0.0: + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} dev: true /@develar/schema-utils/2.6.5: @@ -726,6 +859,16 @@ packages: levn: 0.4.1 dev: true + /@exodus/bytes/1.15.1: + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + dev: true + /@gar/promisify/1.1.3: resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} dev: true @@ -1077,6 +1220,14 @@ packages: engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} dev: true + /@playwright/test/1.62.1: + resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} + engines: {node: '>=20'} + hasBin: true + dependencies: + playwright: 1.62.1 + dev: true + /@rolldown/pluginutils/1.0.0-beta.53: resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} dev: true @@ -1257,6 +1408,10 @@ packages: engines: {node: '>=10'} dev: true + /@standard-schema/spec/1.1.0: + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + dev: true + /@szmarczak/http-timer/4.0.6: resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} @@ -1279,11 +1434,76 @@ packages: resolution: {integrity: sha512-nGm5KteqxasUdThLc2izl6dHUqLv0LQj7Nuyo5gYalTPf/U8a9ermvsl7reT+6ioBW1l8WfpP/mcU338nLXpqw==} dev: false + /@testing-library/dom/10.4.1: + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + dev: true + + /@testing-library/jest-dom/7.0.0_rwcxf7kajngkz26znmtz643644: + resolution: {integrity: sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==} + engines: {node: '>=22', npm: '>=6', yarn: '>=1'} + peerDependencies: + '@testing-library/dom': '>=10 <11' + dependencies: + '@adobe/css-tools': 4.5.0 + '@testing-library/dom': 10.4.1 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + dev: true + + /@testing-library/react/16.3.2_ifbmwgqdqpqwyvhobr7vc7fmyu: + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.1 + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3_@types+react@19.2.7 + react: 19.2.1 + react-dom: 19.2.1_react@19.2.1 + dev: true + + /@testing-library/user-event/14.6.1_rwcxf7kajngkz26znmtz643644: + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + dependencies: + '@testing-library/dom': 10.4.1 + dev: true + /@tootallnate/once/2.0.0: resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} dev: true + /@types/aria-query/5.0.4: + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + dev: true + /@types/babel__core/7.20.5: resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} dependencies: @@ -1322,12 +1542,23 @@ packages: '@types/responselike': 1.0.3 dev: true + /@types/chai/5.2.3: + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + dev: true + /@types/debug/4.1.12: resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} dependencies: '@types/ms': 2.1.0 dev: true + /@types/deep-eql/4.0.2: + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + dev: true + /@types/estree/1.0.8: resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} dev: true @@ -1570,6 +1801,90 @@ packages: - supports-color dev: true + /@vitest/coverage-v8/4.1.10_vitest@4.1.10: + resolution: {integrity: sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==} + peerDependencies: + '@vitest/browser': 4.1.10 + vitest: 4.1.10 + peerDependenciesMeta: + '@vitest/browser': + optional: true + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.10 + ast-v8-to-istanbul: 1.0.5 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.4 + obug: 2.1.4 + std-env: 4.2.0 + tinyrainbow: 3.1.1 + vitest: 4.1.10_vx7uwpt5cwqrhusdpoa262p5gm + dev: true + + /@vitest/expect/4.1.10: + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.1 + dev: true + + /@vitest/mocker/4.1.10_vite@7.2.7: + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + vite: 7.2.7_kx7yyowhop237vgn7dp2p54shq + dev: true + + /@vitest/pretty-format/4.1.10: + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + dependencies: + tinyrainbow: 3.1.1 + dev: true + + /@vitest/runner/4.1.10: + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + dependencies: + '@vitest/utils': 4.1.10 + pathe: 2.0.3 + dev: true + + /@vitest/snapshot/4.1.10: + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + dependencies: + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pathe: 2.0.3 + dev: true + + /@vitest/spy/4.1.10: + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + dev: true + + /@vitest/utils/4.1.10: + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + dependencies: + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + dev: true + /@xmldom/xmldom/0.8.11: resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} engines: {node: '>=10.0.0'} @@ -1656,6 +1971,11 @@ packages: color-convert: 2.0.1 dev: true + /ansi-styles/5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + dev: true + /ansi-styles/6.2.3: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} @@ -1714,6 +2034,17 @@ packages: /argparse/2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + /aria-query/5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + dependencies: + dequal: 2.0.3 + dev: true + + /aria-query/5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + dev: true + /array-buffer-byte-length/1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} @@ -1798,6 +2129,19 @@ packages: dev: true optional: true + /assertion-error/2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + dev: true + + /ast-v8-to-istanbul/1.0.5: + resolution: {integrity: sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==} + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + dev: true + /astral-regex/2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} @@ -1847,6 +2191,12 @@ packages: hasBin: true dev: true + /bidi-js/1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + dependencies: + require-from-string: 2.0.2 + dev: true + /bl/4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} dependencies: @@ -2019,6 +2369,11 @@ packages: resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==} dev: true + /chai/6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + dev: true + /chalk/4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2166,10 +2521,32 @@ packages: shebang-command: 2.0.0 which: 2.0.2 + /css-tree/3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + dev: true + + /css.escape/1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + dev: true + /csstype/3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} dev: true + /data-urls/7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + dev: true + /data-view-buffer/1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -2208,6 +2585,10 @@ packages: dependencies: ms: 2.1.3 + /decimal.js/10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + dev: true + /decompress-response/6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -2253,6 +2634,11 @@ packages: engines: {node: '>=0.4.0'} dev: true + /dequal/2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + dev: true + /detect-libc/2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -2306,6 +2692,14 @@ packages: esutils: 2.0.3 dev: true + /dom-accessibility-api/0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dev: true + + /dom-accessibility-api/0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dev: true + /dotenv-expand/11.0.7: resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} engines: {node: '>=12'} @@ -2448,6 +2842,11 @@ packages: once: 1.4.0 dev: true + /entities/8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + dev: true + /env-paths/2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -2553,6 +2952,10 @@ packages: safe-array-concat: 1.1.3 dev: true + /es-module-lexer/2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + dev: true + /es-object-atoms/1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -2805,11 +3208,22 @@ packages: engines: {node: '>=4.0'} dev: true + /estree-walker/3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + dependencies: + '@types/estree': 1.0.8 + dev: true + /esutils/2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} dev: true + /expect-type/1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + dev: true + /exponential-backoff/3.1.3: resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} dev: true @@ -2848,6 +3262,18 @@ packages: picomatch: 4.0.3 dev: true + /fdir/6.5.0_picomatch@4.0.5: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + dependencies: + picomatch: 4.0.5 + dev: true + /file-entry-cache/8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -2944,6 +3370,13 @@ packages: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true + /fsevents/2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + dev: true + optional: true + /fsevents/2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -3175,6 +3608,19 @@ packages: lru-cache: 6.0.0 dev: true + /html-encoding-sniffer/6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + dependencies: + '@exodus/bytes': 1.15.1 + transitivePeerDependencies: + - '@noble/hashes' + dev: true + + /html-escaper/2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + dev: true + /http-cache-semantics/4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} dev: true @@ -3450,6 +3896,10 @@ packages: has-tostringtag: 1.0.2 dev: true + /is-potential-custom-element-name/1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + dev: true + /is-regex/1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -3538,6 +3988,28 @@ packages: /isexe/2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + /istanbul-lib-coverage/3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + dev: true + + /istanbul-lib-report/3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + dev: true + + /istanbul-reports/3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + dev: true + /iterator.prototype/1.1.5: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} @@ -3568,6 +4040,10 @@ packages: picocolors: 1.1.1 dev: true + /js-tokens/10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + dev: true + /js-tokens/4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} dev: true @@ -3578,6 +4054,40 @@ packages: dependencies: argparse: 2.0.1 + /jsdom/30.0.1: + resolution: {integrity: sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + peerDependencies: + canvas: ^3.2.3 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + '@asamuzakjp/css-color': 6.0.5 + '@asamuzakjp/dom-selector': 8.3.0 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.7_css-tree@3.2.1 + '@exodus/bytes': 1.15.1 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.2 + undici: 8.9.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 17.1.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + dev: true + /jsesc/3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -3708,6 +4218,11 @@ packages: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} dev: true + /lru-cache/11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + dev: true + /lru-cache/5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} dependencies: @@ -3726,12 +4241,32 @@ packages: engines: {node: '>=12'} dev: true + /lz-string/1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + dev: true + /magic-string/0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} dependencies: '@jridgewell/sourcemap-codec': 1.5.5 dev: true + /magicast/0.5.4: + resolution: {integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==} + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + source-map-js: 1.2.1 + dev: true + + /make-dir/4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + dependencies: + semver: 7.7.3 + dev: true + /make-fetch-happen/10.2.1: resolution: {integrity: sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -3762,6 +4297,10 @@ packages: engines: {node: '>= 0.4'} dev: true + /mdn-data/2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + dev: true + /mime-db/1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -3795,6 +4334,11 @@ packages: engines: {node: '>=10'} dev: true + /min-indent/1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + dev: true + /minimatch/10.1.1: resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} engines: {node: 20 || >=22} @@ -4011,6 +4555,11 @@ packages: es-object-atoms: 1.1.1 dev: true + /obug/2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + dev: true + /once/1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: @@ -4110,6 +4659,12 @@ packages: callsites: 3.1.0 dev: true + /parse5/8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + dependencies: + entities: 8.0.0 + dev: true + /path-exists/4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -4136,6 +4691,10 @@ packages: minipass: 7.1.2 dev: true + /pathe/2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + dev: true + /pe-library/0.4.1: resolution: {integrity: sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==} engines: {node: '>=12', npm: '>=6'} @@ -4154,7 +4713,22 @@ packages: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} dev: true - optional: true + + /playwright-core/1.62.1: + resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} + engines: {node: '>=20'} + hasBin: true + dev: true + + /playwright/1.62.1: + resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} + engines: {node: '>=20'} + hasBin: true + dependencies: + playwright-core: 1.62.1 + optionalDependencies: + fsevents: 2.3.2 + dev: true /plist/3.1.0: resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} @@ -4197,6 +4771,15 @@ packages: hasBin: true dev: true + /pretty-format/27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + dev: true + /proc-log/2.0.1: resolution: {integrity: sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -4260,6 +4843,10 @@ packages: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} dev: true + /react-is/17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + dev: true + /react-refresh/0.18.0: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} @@ -4292,6 +4879,14 @@ packages: engines: {node: '>= 20.19.0'} dev: true + /redent/3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + dev: true + /reflect.getprototypeof/1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -4323,6 +4918,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /require-from-string/2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + dev: true + /resedit/1.7.2: resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==} engines: {node: '>=12', npm: '>=6'} @@ -4464,6 +5064,13 @@ packages: /sax/1.4.3: resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==} + /saxes/6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + dependencies: + xmlchars: 2.2.0 + dev: true + /scheduler/0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -4563,6 +5170,10 @@ packages: side-channel-weakmap: 1.0.2 dev: true + /siginfo/2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + dev: true + /signal-exit/3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: true @@ -4642,11 +5253,19 @@ packages: minipass: 3.3.6 dev: true + /stackback/0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + dev: true + /stat-mode/1.0.0: resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==} engines: {node: '>= 6'} dev: true + /std-env/4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + dev: true + /stop-iteration-iterator/1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -4751,6 +5370,13 @@ packages: ansi-regex: 6.2.2 dev: true + /strip-indent/3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + dependencies: + min-indent: 1.0.1 + dev: true + /strip-json-comments/3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -4776,6 +5402,10 @@ packages: engines: {node: '>= 0.4'} dev: true + /symbol-tree/3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + dev: true + /synckit/0.11.11: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -4812,12 +5442,37 @@ packages: resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==} dev: false + /tinybench/2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + dev: true + + /tinyexec/1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + dev: true + /tinyglobby/0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} dependencies: - fdir: 6.5.0_picomatch@4.0.3 - picomatch: 4.0.3 + fdir: 6.5.0_picomatch@4.0.5 + picomatch: 4.0.5 + dev: true + + /tinyrainbow/3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + dev: true + + /tldts-core/7.4.10: + resolution: {integrity: sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==} + dev: true + + /tldts/7.4.10: + resolution: {integrity: sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==} + hasBin: true + dependencies: + tldts-core: 7.4.10 dev: true /tmp-promise/3.0.3: @@ -4831,6 +5486,20 @@ packages: engines: {node: '>=14.14'} dev: true + /tough-cookie/6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + dependencies: + tldts: 7.4.10 + dev: true + + /tr46/6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + dependencies: + punycode: 2.3.1 + dev: true + /truncate-utf8-bytes/1.0.2: resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} dependencies: @@ -4943,6 +5612,11 @@ packages: engines: {node: '>=20.18.1'} optional: true + /undici/8.9.0: + resolution: {integrity: sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==} + engines: {node: '>=22.19.0'} + dev: true + /unique-filename/2.0.1: resolution: {integrity: sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -5048,17 +5722,124 @@ packages: fsevents: 2.3.3 dev: true + /vitest/4.1.10_vx7uwpt5cwqrhusdpoa262p5gm: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/node': 22.19.2 + '@vitest/coverage-v8': 4.1.10_vitest@4.1.10 + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10_vite@7.2.7 + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 + jsdom: 30.0.1 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.1 + vite: 7.2.7_kx7yyowhop237vgn7dp2p54shq + why-is-node-running: 2.3.0 + transitivePeerDependencies: + - msw + dev: true + + /w3c-xmlserializer/5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + dependencies: + xml-name-validator: 5.0.0 + dev: true + /wcwidth/1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} dependencies: defaults: 1.0.4 dev: true + /webidl-conversions/8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + dev: true + /wechat-emojis/1.0.2: resolution: {integrity: sha512-T1drHGy92rKm/Vo7LRkU4D4wdREpVTjAMEa4gR1NB9IAyck3qmmewFSrnEEIyZfsv3SXTA7X1kt6Smt0UKVCyw==} engines: {node: '>=14.0.0'} dev: false + /whatwg-mimetype/5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + dev: true + + /whatwg-url/16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + dependencies: + '@exodus/bytes': 1.15.1 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + dev: true + + /whatwg-url/17.1.0: + resolution: {integrity: sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==} + engines: {node: ^22.14.0 || >=24.0.0} + dependencies: + '@exodus/bytes': 1.15.1 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + dev: true + /which-boxed-primitive/1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -5119,6 +5900,15 @@ packages: dependencies: isexe: 2.0.0 + /why-is-node-running/2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + dev: true + /word-wrap/1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -5146,11 +5936,20 @@ packages: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true + /xml-name-validator/5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + dev: true + /xmlbuilder/15.1.1: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} dev: true + /xmlchars/2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + dev: true + /y18n/5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} diff --git a/src/main/app-logger.ts b/src/main/app-logger.ts index c3d6400..a128e7e 100644 --- a/src/main/app-logger.ts +++ b/src/main/app-logger.ts @@ -13,6 +13,7 @@ const sanitize = (value: unknown, depth = 0): unknown => { return value .replace(/\bsk-[a-z0-9_-]{8,}\b/gi, '***') .replace(/\bBearer\s+[a-z0-9._~-]{8,}\b/gi, 'Bearer ***') + .replace(/\b(?:0x)?[a-f0-9]{64}\b/gi, '***') .slice(0, 2000) } if (Array.isArray(value)) return value.slice(0, 30).map((item) => sanitize(item, depth + 1)) @@ -56,7 +57,7 @@ export class AppLogger { mode: isPackagedRuntime() ? 'packaged' : 'development', level: entry.level, scope: String(entry.scope || 'app').slice(0, 80), - message: String(entry.message || '').slice(0, 500), + message: String(sanitize(entry.message || '')).slice(0, 500), details: sanitize(entry.details || {}) } fs.appendFileSync(this.logPath, `${JSON.stringify(record)}\n`, { encoding: 'utf8' }) diff --git a/src/main/image-decrypt-service.ts b/src/main/image-decrypt-service.ts index 60671e2..a646438 100644 --- a/src/main/image-decrypt-service.ts +++ b/src/main/image-decrypt-service.ts @@ -203,9 +203,11 @@ function getMimeType(extension) { function strictRemovePadding(buffer) { if (buffer.length === 0) return buffer const paddingLength = buffer[buffer.length - 1] - if (paddingLength <= 0 || paddingLength > 16 || paddingLength > buffer.length) return buffer + if (paddingLength <= 0 || paddingLength > 16 || paddingLength > buffer.length) { + throw new Error('invalid PKCS#7 padding') + } for (let index = buffer.length - paddingLength; index < buffer.length; index += 1) { - if (buffer[index] !== paddingLength) return buffer + if (buffer[index] !== paddingLength) throw new Error('invalid PKCS#7 padding') } return buffer.subarray(0, buffer.length - paddingLength) } @@ -1636,19 +1638,13 @@ export class ImageDecryptService { private strictRemovePadding(buffer: Buffer): Buffer { if (buffer.length === 0) return buffer const lastByte = buffer[buffer.length - 1] - if (lastByte <= 16 && lastByte > 0) { - const paddingLength = lastByte - let valid = true - for (let i = buffer.length - paddingLength; i < buffer.length; i++) { - if (buffer[i] !== lastByte) { - valid = false - break - } - } - if (valid) { - return buffer.subarray(0, buffer.length - paddingLength) - } + if (lastByte <= 0 || lastByte > 16 || lastByte > buffer.length) { + throw new Error('invalid PKCS#7 padding') } - return buffer + const paddingLength = lastByte + for (let i = buffer.length - paddingLength; i < buffer.length; i += 1) { + if (buffer[i] !== lastByte) throw new Error('invalid PKCS#7 padding') + } + return buffer.subarray(0, buffer.length - paddingLength) } } diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 6122b89..f2a35ae 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -24,6 +24,11 @@ import { FirstUseWelcome } from './components/FirstUseWelcome' import { ExportWorkspace } from './components/export/ExportWorkspace' import { AISearchWorkspace } from './components/search/AISearchWorkspace' import type { ExportJobProgress, ExportRequest, ExportTaskRecord } from '../../shared/export' +import { + getMessageIdentity, + mergeMessagePages, + sortMessagesChronologically +} from './utils/message-pages' const SIDEBAR_MIN_WIDTH = 260 const SIDEBAR_MAX_WIDTH = 380 @@ -47,12 +52,6 @@ const INITIAL_MESSAGE_COUNT = 20 const MESSAGE_PAGE_SIZE = 100 const MESSAGE_PREFETCH_COUNT = INITIAL_MESSAGE_COUNT + MESSAGE_PAGE_SIZE const EXPORT_PREVIEW_LIMIT = 20 -const getMessageIdentity = (message: Message): string => { - if (message.localId) return `local:${message.localId}` - if (message.id) return `id:${message.id}` - return `${message.createTime || 0}:${message.from}:${message.type}:${message.content}` -} - const normalizeQuotedText = (value: string | undefined): string => String(value || '') .replace(/\s+/g, ' ') @@ -194,19 +193,6 @@ const buildSyntheticGroupMessages = ( void buildSyntheticGroupMessages -const sortMessagesChronologically = (items: Message[]): Message[] => - [...items].sort((left, right) => { - const timeDelta = (left.createTime || 0) - (right.createTime || 0) - if (timeDelta !== 0) return timeDelta - return getMessageIdentity(left).localeCompare(getMessageIdentity(right)) - }) - -const mergeMessagePages = (older: Message[], current: Message[]): Message[] => { - const merged = new Map() - for (const message of [...older, ...current]) merged.set(getMessageIdentity(message), message) - return sortMessagesChronologically(Array.from(merged.values())) -} - function App(): React.ReactElement { const [isAuthenticated, setIsAuthenticated] = useState(false) const [isDatabaseConnected, setIsDatabaseConnected] = useState(false) diff --git a/src/renderer/src/utils/message-pages.ts b/src/renderer/src/utils/message-pages.ts new file mode 100644 index 0000000..68bcf96 --- /dev/null +++ b/src/renderer/src/utils/message-pages.ts @@ -0,0 +1,20 @@ +import type { Message } from '../../../shared/types' + +export const getMessageIdentity = (message: Message): string => { + if (message.localId) return `local:${message.localId}` + if (message.id) return `id:${message.id}` + return `${message.createTime || 0}:${message.from}:${message.type}:${message.content}` +} + +export const sortMessagesChronologically = (items: Message[]): Message[] => + [...items].sort((left, right) => { + const timeDelta = (left.createTime || 0) - (right.createTime || 0) + if (timeDelta !== 0) return timeDelta + return getMessageIdentity(left).localeCompare(getMessageIdentity(right)) + }) + +export const mergeMessagePages = (older: Message[], current: Message[]): Message[] => { + const merged = new Map() + for (const message of [...older, ...current]) merged.set(getMessageIdentity(message), message) + return sortMessagesChronologically(Array.from(merged.values())) +} diff --git a/tests/component/database-connection.test.tsx b/tests/component/database-connection.test.tsx new file mode 100644 index 0000000..3720915 --- /dev/null +++ b/tests/component/database-connection.test.tsx @@ -0,0 +1,55 @@ +import { render, screen, type RenderResult } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import type { ComponentProps } from 'react' +import { describe, expect, it, vi } from 'vitest' +import { DatabaseConnectionPage } from '../../src/renderer/src/components/DatabaseConnectionPage' + +function renderPage( + overrides: Partial> = {} +): RenderResult & { props: ComponentProps } { + const props = { + platform: 'win32', + mode: 'manual' as const, + dbKey: '', + dbRoot: '', + showDbKey: false, + isFetching: false, + status: '', + statusKind: 'normal' as const, + showMacKeyFaq: false, + macKeyFaqUrl: 'https://fixture.invalid/mac', + onModeChange: vi.fn(), + onDbKeyChange: vi.fn(), + onDbRootChange: vi.fn(), + onToggleDbKey: vi.fn(), + onAutoGetKey: vi.fn(), + onManualConnect: vi.fn(), + onPasteKey: vi.fn(), + onClearKey: vi.fn(), + ...overrides + } + return { props, ...render() } +} + +describe('DatabaseConnectionPage', () => { + it('keeps connect disabled until a valid 64-character key is supplied', () => { + const { rerender, props } = renderPage() + expect(screen.getByRole('button', { name: '连接数据库' })).toBeDisabled() + rerender() + expect(screen.getByRole('button', { name: '连接数据库' })).toBeEnabled() + }) + + it('shows a recoverable error and keeps form actions available', async () => { + const onManualConnect = vi.fn() + renderPage({ + dbKey: 'b'.repeat(64), + status: '数据库密钥无效,请重新输入', + statusKind: 'error', + onManualConnect + }) + expect(screen.getByText('数据库密钥无效,请重新输入')).toBeVisible() + await userEvent.click(screen.getByRole('button', { name: '连接数据库' })) + expect(onManualConnect).toHaveBeenCalledOnce() + expect(screen.getByRole('button', { name: '从剪贴板粘贴并安全保存' })).toBeEnabled() + }) +}) diff --git a/tests/component/empty-state.test.tsx b/tests/component/empty-state.test.tsx new file mode 100644 index 0000000..215cd04 --- /dev/null +++ b/tests/component/empty-state.test.tsx @@ -0,0 +1,18 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' +import { EmptyConversationState } from '../../src/renderer/src/components/chat/EmptyConversationState' +import { SettingsEmptyState } from '../../src/renderer/src/features/settings/components/SettingsEmptyState' + +describe('empty states', () => { + it('explains how to leave an empty archive without pretending data failed', () => { + render() + expect(screen.getByRole('heading', { name: '选择一条消息' })).toBeVisible() + expect(screen.getByText('从左侧选择群聊或联系人以浏览历史记录')).toBeVisible() + }) + + it('labels an unavailable settings section explicitly', () => { + render() + expect(screen.getByRole('heading', { name: '测试设置' })).toBeVisible() + expect(screen.getByText('该设置将在后续阶段接入。')).toBeVisible() + }) +}) diff --git a/tests/component/image-bubble.test.tsx b/tests/component/image-bubble.test.tsx new file mode 100644 index 0000000..32a92c4 --- /dev/null +++ b/tests/component/image-bubble.test.tsx @@ -0,0 +1,53 @@ +import { render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const requestImage = vi.fn() +vi.mock('../../src/renderer/src/components/image-loader', () => ({ + getCachedLoadedImage: vi.fn(() => undefined), + requestImage: (...args: unknown[]) => requestImage(...args) +})) + +import { ImageBubble } from '../../src/renderer/src/components/ImageBubble' + +const thumbnail = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9ZQmcAAAAASUVORK5CYII=' +const original = `${thumbnail}original` + +describe('ImageBubble', () => { + beforeEach(() => { + requestImage.mockReset() + window.api = { copyImage: vi.fn().mockResolvedValue({ success: true }) } as typeof window.api + }) + + it('loads a thumbnail lazily, then requests the original when opened', async () => { + requestImage + .mockResolvedValueOnce({ data: thumbnail, isThumbnail: true }) + .mockResolvedValueOnce({ data: original, isThumbnail: false }) + const onImageClick = vi.fn() + render( + + ) + + const image = await screen.findByAltText('图片') + expect(image).toHaveAttribute('src', thumbnail) + await userEvent.click(image) + await waitFor(() => expect(onImageClick).toHaveBeenCalledWith(original)) + expect(requestImage.mock.calls[1][3]).toMatchObject({ force: true }) + }) + + it('shows an explicit error and allows retry', async () => { + requestImage.mockRejectedValueOnce(new Error('不支持的 DAT 版本')) + render() + expect(await screen.findByText('不支持的 DAT 版本')).toBeVisible() + + requestImage.mockResolvedValueOnce({ data: thumbnail, isThumbnail: true }) + await userEvent.click(screen.getByText('加载失败')) + expect(await screen.findByAltText('图片')).toBeVisible() + }) +}) diff --git a/tests/component/navigation.test.tsx b/tests/component/navigation.test.tsx new file mode 100644 index 0000000..e2684c3 --- /dev/null +++ b/tests/component/navigation.test.tsx @@ -0,0 +1,21 @@ +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { describe, expect, it, vi } from 'vitest' +import { PrimaryNavigation } from '../../src/renderer/src/components/layout/PrimaryNavigation' +import { PRIMARY_NAV_ITEMS } from '../../src/renderer/src/components/layout/navigation' + +describe('PrimaryNavigation', () => { + it('shows every real top-level page exactly once and emits the selected page', async () => { + const onPageChange = vi.fn() + render() + + const navigation = screen.getByRole('navigation', { name: '一级导航' }) + expect(navigation).toBeInTheDocument() + for (const item of PRIMARY_NAV_ITEMS) { + expect(screen.getAllByRole('button', { name: item.label })).toHaveLength(1) + } + + await userEvent.click(screen.getByRole('button', { name: '设置' })) + expect(onPageChange).toHaveBeenCalledWith('settings') + }) +}) diff --git a/tests/component/voice-player.test.tsx b/tests/component/voice-player.test.tsx new file mode 100644 index 0000000..abc4863 --- /dev/null +++ b/tests/component/voice-player.test.tsx @@ -0,0 +1,47 @@ +import { render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { VoicePlayer } from '../../src/renderer/src/components/VoicePlayer' + +const play = vi.fn(() => Promise.resolve()) +const pause = vi.fn() + +class FakeAudio { + preload = '' + src = '' + duration = 1 + currentTime = 0 + onloadedmetadata: (() => void) | null = null + ontimeupdate: (() => void) | null = null + onended: (() => void) | null = null + play = play + pause = pause + load = vi.fn() + removeAttribute = vi.fn() +} + +describe('VoicePlayer', () => { + beforeEach(() => { + play.mockClear() + pause.mockClear() + vi.stubGlobal('Audio', FakeAudio) + window.api = { + getVoiceData: vi.fn().mockResolvedValue({ + success: true, + data: 'UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA=' + }) + } as typeof window.api + }) + + it('waits for decrypted bytes and calls play on the first click', async () => { + const { container } = render( + + ) + await userEvent.click(container.querySelector('.voice-message') as HTMLElement) + + await waitFor(() => expect(window.api.getVoiceData).toHaveBeenCalledOnce()) + await waitFor(() => expect(play).toHaveBeenCalledOnce()) + expect(container.querySelector('.voice-icon')).toHaveClass('playing') + expect(screen.queryByText('当前版本暂不支持播放')).not.toBeInTheDocument() + }) +}) diff --git a/tests/e2e/__screenshots__/win32/visual.spec.ts/archive-page.png b/tests/e2e/__screenshots__/win32/visual.spec.ts/archive-page.png new file mode 100644 index 0000000000000000000000000000000000000000..086df4c891da3736bb997b900267e0cbc10880bb GIT binary patch literal 58566 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%0DX-5^euXVS0rRv~C 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 S+ZC_;<2uCAs?x&i@xK8ebH$JV literal 0 HcmV?d00001 diff --git a/tests/e2e/__screenshots__/win32/visual.spec.ts/login-page.png b/tests/e2e/__screenshots__/win32/visual.spec.ts/login-page.png new file mode 100644 index 0000000000000000000000000000000000000000..537b93ebfb09780870ed0ec22b60e948b867ec76 GIT binary patch literal 74638 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𝔢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%0DiFLZfBzpquRQ?n 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 literal 0 HcmV?d00001 diff --git a/tests/e2e/app.spec.ts b/tests/e2e/app.spec.ts new file mode 100644 index 0000000..d7d48b6 --- /dev/null +++ b/tests/e2e/app.spec.ts @@ -0,0 +1,289 @@ +import { expect, test } from '@playwright/test' +import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from 'fs' +import { tmpdir } from 'os' +import { resolve } from 'path' +import { launchTestApp } from './support/electron' + +test('APP-01 first launch renders a usable connection screen without uncaught errors', async () => { + const fixture = await launchTestApp({ mode: 'disconnected' }) + const pageErrors: Error[] = [] + fixture.page.on('pageerror', (error) => pageErrors.push(error)) + try { + await expect(fixture.page.getByRole('heading', { name: 'WechatExplorer' })).toBeVisible() + await expect(fixture.page.getByRole('main')).not.toBeEmpty() + expect(pageErrors).toEqual([]) + } finally { + await fixture.close() + } +}) + +test('KEY-01 KEY-02 invalid key remains recoverable and valid key enters the app', async () => { + const fixture = await launchTestApp({ mode: 'disconnected' }) + try { + await fixture.page.getByRole('tab', { name: /高级用户/ }).click() + const keyInput = fixture.page.getByLabel('数据库密钥') + await keyInput.fill('b'.repeat(64)) + const errorDialog = fixture.page.waitForEvent('dialog') + await fixture.page + .getByRole('button', { name: '连接数据库' }) + .evaluate((element: HTMLButtonElement) => element.click()) + const dialog = await errorDialog + expect(dialog.message()).toContain('数据库密钥无效') + await dialog.dismiss() + + await expect(keyInput).toBeVisible() + await keyInput.fill('a'.repeat(64)) + await fixture.page.getByRole('button', { name: '连接数据库' }).click() + await expect(fixture.page.getByRole('navigation', { name: '一级导航' })).toBeVisible() + } finally { + await fixture.close() + } +}) + +test('KEY-03 changing one key does not invalidate archive data or unrelated settings', async () => { + const fixture = await launchTestApp() + try { + await expect(fixture.page.getByRole('navigation', { name: '一级导航' })).toBeVisible() + const result = await fixture.page.evaluate(async () => { + const before = await window.api.getContacts() + const image = await window.api.saveImageKeyConfig({ + resourceRoot: 'fixture-account', + xorKey: '0x41', + aesKey: 'fedcba9876543210' + }) + const after = await window.api.getContacts() + const database = await window.api.getSavedDbKey() + return { before, after, image, database } + }) + expect(result.image.success).toBe(true) + expect(result.before).toEqual(result.after) + expect(result.database.saved).toBe(true) + } finally { + await fixture.close() + } +}) + +test('NAV-01 NAV-02 every top-level page is unique and switchable', async () => { + const fixture = await launchTestApp() + const labels = ['档案', '问问微信', '日报', 'Agent', '导出', 'API', '设置'] + try { + const navigation = fixture.page.getByRole('navigation', { name: '一级导航' }) + await expect(navigation).toBeVisible() + for (const label of labels) { + await expect(navigation.getByRole('button', { name: label })).toHaveCount(1) + await navigation.getByRole('button', { name: label }).click() + await expect(fixture.page.locator(`main.app-shell-main[aria-label="${label}"]`)).toBeVisible() + } + } finally { + await fixture.close() + } +}) + +test('ARCH-01 ARCH-02 folded chats and supported message types are represented explicitly', async () => { + const fixture = await launchTestApp() + try { + await expect(fixture.page.getByText('产品测试群', { exact: true })).toBeVisible() + await fixture.page.getByText('产品测试群', { exact: true }).click() + await expect(fixture.page.getByText('这是一条脱敏测试消息', { exact: true })).toBeVisible() + await expect(fixture.page.getByText('暂不支持此消息', { exact: true })).toBeVisible() + await expect(fixture.page.getByAltText('图片')).toBeVisible() + await fixture.page.locator('.image-bubble.image-loaded').click() + await expect(fixture.page.getByText('图片查看', { exact: true })).toBeVisible() + await fixture.page.locator('.image-viewer-overlay').click({ position: { x: 5, y: 5 } }) + + await fixture.page.getByRole('button', { name: '折叠群聊 (1)' }).click() + await expect(fixture.page.getByText('折叠群聊样本', { exact: true })).toBeVisible() + } finally { + await fixture.close() + } +}) + +test('MEDIA-01 and merged forwards work on the first interaction', async () => { + const fixture = await launchTestApp() + try { + await fixture.page.getByRole('button', { name: '联系人 (1)' }).click() + await fixture.page.getByText('文件传输助手', { exact: true }).click() + await expect(fixture.page.getByText('转发多条内容', { exact: true })).toBeVisible() + await fixture.page.evaluate(() => { + Object.defineProperty(window, '__wxePlayCount', { + configurable: true, + value: 0, + writable: true + }) + HTMLMediaElement.prototype.play = async function () { + ;(window as Window & { __wxePlayCount: number }).__wxePlayCount += 1 + } + HTMLMediaElement.prototype.pause = function () { + return undefined + } + HTMLMediaElement.prototype.load = function () { + return undefined + } + }) + await fixture.page.locator('.voice-message').click() + await expect(fixture.page.locator('.voice-icon')).toHaveClass(/playing/) + expect( + await fixture.page.evaluate( + () => (window as Window & { __wxePlayCount: number }).__wxePlayCount + ) + ).toBe(1) + } finally { + await fixture.close() + } +}) + +test('MEDIA-02 MEDIA-04 return accurate unsupported and HTTP 403 reasons', async () => { + const fixture = await launchTestApp() + try { + const result = await fixture.page.evaluate(async () => ({ + image: await window.api.getImage('unsupported'), + sticker: await window.api.getSticker( + 'https://fixture.invalid/403?token=secret', + 'b'.repeat(32) + ) + })) + expect(result.image).toMatchObject({ success: false, error: '不支持的 DAT 版本' }) + expect(result.sticker).toMatchObject({ + success: false, + failureCode: 'access_denied', + httpStatus: 403 + }) + } finally { + await fixture.close() + } +}) + +test('ASK-01 uses the local fixed AI service and keeps evidence in the UI', async () => { + const fixture = await launchTestApp() + try { + await fixture.page.getByRole('button', { name: '问问微信' }).click() + await fixture.page.getByPlaceholder(/例如:技术交流群/).fill('测试群讨论了什么?') + await fixture.page.getByRole('button', { name: '开始分析' }).click() + await expect(fixture.page.getByText(/固定假回答:测试数据中的核心流程正常/)).toBeVisible({ + timeout: 15_000 + }) + } finally { + await fixture.close() + } +}) + +test('ASK-02 AI failures are recoverable and do not break the archive', async () => { + const fixture = await launchTestApp({ aiFailure: '429' }) + try { + await fixture.page.getByRole('button', { name: '问问微信' }).click() + await fixture.page.getByPlaceholder(/例如:技术交流群/).fill('测试') + await fixture.page.getByRole('button', { name: '开始分析' }).click() + await expect(fixture.page.getByText(/本地假服务错误 429/)).toBeVisible() + await fixture.page.getByRole('button', { name: '档案' }).click() + await expect(fixture.page.getByText('产品测试群', { exact: true })).toBeVisible() + } finally { + await fixture.close() + } +}) + +test('REPORT-01 REPORT-02 generates a fixed report with non-empty local assets', async () => { + const fixture = await launchTestApp() + try { + await fixture.page.getByRole('button', { name: '日报' }).click() + await fixture.page.getByRole('button', { name: '开始生成日报' }).click() + await expect(fixture.page.getByRole('heading', { name: '生成群聊日报' })).toBeVisible() + await fixture.page.locator('.report-source-item').filter({ hasText: '产品测试群' }).click() + const generate = fixture.page.getByRole('button', { name: '开始生成日报' }) + await expect(generate).toBeEnabled() + await generate.click() + await expect(fixture.page.getByAltText('产品测试群 群聊日报')).toBeVisible({ + timeout: 15_000 + }) + + const exported = await fixture.page.evaluate(async () => + window.api.exportGroupReport({ + report: {} as never, + metadata: {} as never, + templateId: 'v1' + }) + ) + expect(exported.success).toBe(true) + expect(exported.imageDataUrl).toMatch(/^data:image\/png;base64,/) + expect(existsSync(exported.htmlPath!)).toBe(true) + expect(existsSync(exported.pngPath!)).toBe(true) + expect(statSync(exported.pngPath!).size).toBeGreaterThan(20) + } finally { + await fixture.close() + } +}) + +test('REPORT-03 report failure is retryable and leaves other pages usable', async () => { + const fixture = await launchTestApp({ aiFailure: '401' }) + try { + await fixture.page.getByRole('button', { name: '日报' }).click() + await fixture.page.getByRole('button', { name: '开始生成日报' }).click() + await fixture.page.locator('.report-source-item').filter({ hasText: '产品测试群' }).click() + await fixture.page.getByRole('button', { name: '开始生成日报' }).click() + await expect(fixture.page.getByText(/本地假服务错误 401/).first()).toBeVisible() + await expect(fixture.page.getByRole('button', { name: '重试' })).toBeEnabled() + await fixture.page.getByRole('button', { name: '档案' }).click() + await expect(fixture.page.locator('main.app-shell-main[aria-label="档案"]')).toBeVisible() + await expect( + fixture.page.locator('.conversation-item-name').filter({ hasText: '产品测试群' }).first() + ).toBeVisible() + } finally { + await fixture.close() + } +}) + +test('CACHE-01 corrupt startup cache degrades to native fixture data', async () => { + const fixture = await launchTestApp({ corruptCache: true }) + try { + await expect(fixture.page.getByRole('navigation', { name: '一级导航' })).toBeVisible() + await expect(fixture.page.getByText('产品测试群', { exact: true })).toBeVisible() + } finally { + await fixture.close() + } +}) + +test('PERF-01 repeated startup with 1500 sessions remains bounded and responsive', async () => { + test.setTimeout(60_000) + const userData = mkdtempSync(resolve(tmpdir(), 'wxe-e2e-perf-')) + try { + for (let run = 0; run < 2; run += 1) { + const startedAt = Date.now() + const fixture = await launchTestApp({ userData, largeContacts: 1500 }) + try { + await expect(fixture.page.getByRole('navigation', { name: '一级导航' })).toBeVisible({ + timeout: 10_000 + }) + expect(Date.now() - startedAt).toBeLessThan(10_000) + await fixture.page + .getByRole('navigation', { name: '一级导航' }) + .getByRole('button', { name: '设置' }) + .click() + await expect(fixture.page.locator('main.app-shell-main[aria-label="设置"]')).toBeVisible() + } finally { + await fixture.close() + } + } + } finally { + rmSync(userData, { recursive: true, force: true }) + } +}) + +test('KEY-04 e2e diagnostic log does not contain a supplied key', async () => { + const fixture = await launchTestApp() + const key = 'c'.repeat(64) + try { + await fixture.page.evaluate( + (databaseKey) => + window.api.writeAppLog({ + level: 'error', + scope: 'key-test', + message: `fixture key=${databaseKey}` + }), + key + ) + const logPath = resolve(fixture.userData, 'logs/e2e.log') + const content = readFileSync(logPath, 'utf8') + expect(content).not.toContain(key) + } finally { + await fixture.close() + } +}) diff --git a/tests/e2e/support/electron-main.cjs b/tests/e2e/support/electron-main.cjs new file mode 100644 index 0000000..438f378 --- /dev/null +++ b/tests/e2e/support/electron-main.cjs @@ -0,0 +1,397 @@ +/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/explicit-function-return-type */ +const { app, BrowserWindow, ipcMain } = require('electron') +const fs = require('fs') +const path = require('path') + +const root = path.resolve(__dirname, '../../..') +const fixture = require(path.join(root, 'tests/fixtures/chat-data.json')) +const userData = process.env.WXE_E2E_USER_DATA +if (!userData) throw new Error('WXE_E2E_USER_DATA is required') +app.setPath('userData', userData) +app.setPath('logs', path.join(userData, 'logs')) +app.commandLine.appendSwitch('disable-gpu') + +const VALID_KEY = 'a'.repeat(64) +const imageData = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9ZQmcAAAAASUVORK5CYII=' +const voiceData = 'UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA=' +const reportJson = JSON.stringify({ + overview: '固定脱敏日报', + hero: { + headline: '产品测试群日报', + summary: '测试消息已完成自动整理。', + keyTakeaway: '核心流程可用', + pendingNote: '', + statusLine: '今日形成 1 个结论' + }, + topics: [ + { + title: '自动化测试', + timeRange: '10:00-10:02', + heat: '中', + participants: ['测试成员'], + summary: '讨论了脱敏自动化测试。', + conclusions: [{ text: '核心流程可用', sourceMessageIds: ['msg-text'] }], + keywords: ['测试'], + sourceMessageIds: ['msg-text'] + } + ], + resources: [], + importantMessages: [], + quotes: [], + qa: [], + todos: [], + unresolved: [], + storylines: [], + reversals: [], + participantChains: [], + keywords: ['测试'] +}) + +let connected = process.env.WXE_E2E_MODE !== 'disconnected' +let savedKey = connected ? VALID_KEY : '' +let settings = { + dbRoot: 'fixture-account', + apiEnabled: false, + apiHost: '127.0.0.1', + apiPort: 5031, + imageKeyRoot: 'fixture-account', + ffmpegPath: '', + recallProtectionEnabled: false, + debugEnabled: false, + autoLogin: connected, + autoLoginPreferenceSet: true, + appearanceTheme: 'light', + compactMode: false, + showStartupProgress: false, + imageXorKey: '0x40', + imageAesKey: '0123456789abcdef' +} + +const extraContacts = Number(process.env.WXE_E2E_LARGE_CONTACTS || 0) +const contacts = [...fixture.contacts] +for (let index = 0; index < extraContacts; index += 1) { + contacts.push({ + m_nsUsrName: `fixture_${index}`, + m_nsNickName: `性能样本 ${index}`, + md5: `fixture-contact-${index}`, + type: index % 5 === 0 ? 'group' : 'user' + }) +} + +const handlers = new Map() +const handle = (channel, fn) => { + handlers.set(channel, fn) + ipcMain.handle(channel, async (event, ...args) => fn(...args)) +} + +const startupCache = () => ({ + self: fixture.self, + contacts, + updatedAt: 1785553200000 +}) + +handle('settings:get', () => ({ settings, settingsPath: path.join(userData, 'settings.json') })) +handle('settings:set', (patch) => { + settings = { ...settings, ...patch } + return { settings, settingsPath: path.join(userData, 'settings.json') } +}) +handle('key:getSavedDbKey', () => ({ + success: true, + key: savedKey || undefined, + saved: Boolean(savedKey), + encryptionAvailable: true +})) +handle('key:saveDbKey', (key) => { + savedKey = String(key || '') + return { success: true, key: savedKey, saved: true, encryptionAvailable: true } +}) +handle('key:clearSavedDbKey', () => { + savedKey = '' + return { success: true } +}) +handle('key:getEnvironment', () => ({ + platform: process.platform, + autoDetectSupported: true, + wechatRunning: true, + accountIdentified: connected, + dbConnected: connected, + encryptionAvailable: true +})) +handle('key:readClipboardDbKey', () => ({ success: true, value: VALID_KEY })) +handle('key:pasteAndSaveDbKey', () => ({ success: true, key: VALID_KEY })) +handle('key:autoGetDbKey', () => ({ success: true, key: VALID_KEY, saved: false })) +handle('key:autoGetImageKey', () => ({ + success: true, + xorKey: 64, + aesKey: '0123456789abcdef', + verified: true +})) + +handle('db:init', (key) => { + if (key !== VALID_KEY) { + connected = false + return { success: false, error: '数据库密钥无效', monitoring: false } + } + connected = true + return { success: true, monitoring: true } +}) +handle('db:testConnection', (key) => + key === VALID_KEY + ? { success: true, wxid: fixture.self.wxid, accountRoot: fixture.self.accountRoot } + : { success: false, code: 'DATABASE_OPEN_FAILED', error: '数据库密钥无效' } +) +handle('db:disconnect', () => { + connected = false + return { success: true } +}) +handle('db:getStartupCache', () => + process.env.WXE_E2E_CORRUPT_CACHE === '1' ? null : startupCache() +) +handle('db:getBootstrapCache', () => + process.env.WXE_E2E_CORRUPT_CACHE === '1' ? null : startupCache() +) +handle('db:getContacts', (filter) => { + const query = String(filter || '').toLowerCase() + return query + ? contacts.filter((contact) => contact.m_nsNickName.toLowerCase().includes(query)) + : contacts +}) +handle('db:getContactAvatars', (usernames) => + Object.fromEntries( + contacts + .filter((contact) => usernames.includes(contact.m_nsUsrName) && contact.avatar) + .map((contact) => [contact.m_nsUsrName, contact.avatar]) + ) +) +handle('settings:getSelf', () => ({ ready: true, info: fixture.self })) +handle('db:getCachedMessages', (md5) => fixture.messages[md5] || []) +handle('db:getCachedMessagePage', (md5) => ({ + hit: true, + messages: fixture.messages[md5] || [], + groupSnapshot: null +})) +handle('db:getMessages', (md5, startTime, endTime, options) => { + let messages = fixture.messages[md5] || [] + if (startTime) messages = messages.filter((message) => (message.createTime || 0) >= startTime) + if (endTime) messages = messages.filter((message) => (message.createTime || 0) <= endTime) + if (options && options.limit) messages = messages.slice(-options.limit) + return messages +}) +handle('db:getGroupSnapshot', (md5) => + md5.startsWith('group-') + ? { + roomId: md5, + memberCount: 1, + members: [ + { + wxid: 'wxid_fixture_member', + nickname: '测试成员', + groupNickname: '测试成员', + wechatNickname: '测试成员', + remark: '', + avatar: '' + } + ] + } + : null +) +handle('db:getImage', (md5, datName, sessionId, options) => + md5 === 'unsupported' + ? { success: false, error: '不支持的 DAT 版本' } + : { + success: true, + data: imageData, + isThumb: !options?.force, + filePath: path.join(userData, options?.force ? 'original.png' : 'thumbnail.png') + } +) +handle('db:getVoiceData', () => ({ success: true, data: voiceData })) +handle('db:getSticker', (url) => + String(url || '').includes('403') + ? { + success: false, + error: '表情链接已失效或需要微信授权', + failureCode: 'access_denied', + httpStatus: 403 + } + : { success: true, data: imageData } +) +handle('db:parseMessage', (content, messageType) => + messageType === 1 + ? { type: 'text', content: String(content) } + : { type: 'unknown', raw: String(content), messageType } +) + +handle('ai:getRuntimeConfig', () => ({ + providerId: 'fixture-provider', + providerName: '本地假服务', + model: 'fixture-model', + modelName: '固定响应模型', + configured: true, + status: 'connected', + timeoutMs: 5000 +})) +handle('ai:listProviders', () => ({ + success: true, + providers: [], + defaultProviderId: 'fixture-provider' +})) +handle('ai:migrateLegacy', () => ({ success: true, providers: [] })) +handle('ai:chat', (messages) => { + const failure = process.env.WXE_E2E_AI_FAILURE + if (failure) return { success: false, error: `本地假服务错误 ${failure}` } + const system = String(messages?.[0]?.content || '') + if (system.includes('本地聊天检索规划器')) { + return { + success: true, + data: '{"intent":"general","keywords":["测试"],"variants":[]}' + } + } + if (system.includes('微信群聊日报编辑') || system.includes('JSON 格式修复器')) { + return { success: true, data: reportJson, usage: { input: 10, output: 20, total: 30 } } + } + return { success: true, data: '固定假回答:测试数据中的核心流程正常。' } +}) + +handle('report:export', () => { + const htmlPath = path.join(userData, 'fixture-report.html') + const pngPath = path.join(userData, 'fixture-report.png') + fs.writeFileSync(htmlPath, '

固定脱敏日报

', 'utf8') + fs.writeFileSync(pngPath, Buffer.from(imageData.split(',')[1], 'base64')) + return { success: true, imageDataUrl: imageData, htmlPath, pngPath } +}) +handle('report:listGenerated', () => ({ success: true, reports: [] })) +handle('report:saveGenerated', (request) => ({ + success: true, + record: { id: 'fixture-report-record', ...request } +})) +handle('report:deleteGenerated', () => ({ success: true })) +handle('report:reveal', () => ({ success: true })) +handle('copy-image', () => ({ success: true })) +handle('api:copyText', () => ({ success: true })) +handle('app-log:write', (entry) => { + const safe = JSON.stringify(entry) + .replace(/\b(?:0x)?[a-f0-9]{64}\b/gi, '***') + .replace(/\bsk-[a-z0-9_-]{8,}\b/gi, '***') + fs.mkdirSync(path.join(userData, 'logs'), { recursive: true }) + fs.appendFileSync(path.join(userData, 'logs', 'e2e.log'), `${safe}\n`, 'utf8') +}) +handle('app-log:getPath', () => path.join(userData, 'logs', 'e2e.log')) +handle('app-log:reveal', () => undefined) +handle('cache:getSummary', () => ({ bootstrapBytes: 0, electronBytes: 0, totalBytes: 0 })) +handle('cache:clear', () => ({ bootstrapBytes: 0, electronBytes: 0, totalBytes: 0 })) +handle('api:getStatus', () => ({ running: false, host: settings.apiHost, port: settings.apiPort })) +handle('api:start', () => ({ running: true, host: settings.apiHost, port: settings.apiPort })) +handle('api:stop', () => ({ running: false, host: settings.apiHost, port: settings.apiPort })) +handle('api:toggle', (enabled) => ({ + running: enabled, + host: settings.apiHost, + port: settings.apiPort +})) +handle('image:getConfig', () => ({ + success: true, + configured: true, + saved: true, + encryptionAvailable: true, + source: 'secure-storage', + resourceRoot: settings.imageKeyRoot, + xorKey: settings.imageXorKey, + aesKey: settings.imageAesKey +})) +handle('image:saveConfig', (request) => ({ + success: true, + configured: true, + saved: true, + encryptionAvailable: true, + source: 'secure-storage', + ...request +})) +handle('image:testConfig', () => ({ + success: true, + fileFound: true, + decrypted: true, + readable: true +})) +handle('image:clearConfig', () => ({ success: true })) +handle('image:getDecoderStatus', () => ({ + installed: true, + available: true, + source: 'system', + selected: false +})) +handle('image:getStatus', () => ({ + configured: true, + saved: true, + encryptionAvailable: true, + source: 'secure-storage', + resourceRoot: settings.imageKeyRoot, + platform: process.platform, + autoDetectSupported: true, + wechatRunning: true, + accountIdentified: true, + cacheState: 'normal', + decoder: { installed: true, available: true, source: 'system', selected: false }, + resources: Object.fromEntries( + ['imageIndex', 'imageDirectory', 'thumbnail', 'original', 'sticker', 'video'].map((name) => [ + name, + { state: 'available', detail: 'fixture' } + ]) + ) +})) +handle('agent-hub:getStatus', () => ({ state: 'disconnected', connected: false })) +handle('agent-hub:getLogs', () => []) +handle('app-update:getState', () => ({ status: 'idle', currentVersion: '2.1.6' })) + +for (const channel of [ + 'export:start', + 'export:cancel', + 'export:reveal', + 'settings:selectDbRoot', + 'settings:openAccountRoot', + 'db:reopenWithRoot', + 'api:skillStatus', + 'api:readSkill', + 'api:revealSkill', + 'api:openSkillGithub', + 'api:testLocalRequest', + 'image:selectDecoder', + 'image:openDecoderDownload', + 'app-update:check', + 'app-update:download', + 'app-update:install', + 'agent-hub:clearLogs', + 'agent-hub:startLogin', + 'agent-hub:cancelLogin', + 'agent-hub:reconnect', + 'agent-hub:disconnect', + 'agent-hub:selectTestImage', + 'image:listCandidates', + 'image:analyze', + 'image:getInsight', + 'image:listInsights', + 'db:search', + 'db:getVideo' +]) { + if (!handlers.has(channel)) + handle(channel, () => ({ success: true, candidates: [], insights: [] })) +} + +app.whenReady().then(() => { + const window = new BrowserWindow({ + width: 1440, + height: 960, + show: false, + backgroundColor: '#ffffff', + webPreferences: { + preload: path.join(root, 'out/preload/index.js'), + contextIsolation: true, + nodeIntegration: false, + sandbox: false + } + }) + window.once('ready-to-show', () => window.show()) + window.loadFile(path.join(root, 'out/renderer/index.html')) +}) + +app.on('window-all-closed', () => app.quit()) diff --git a/tests/e2e/support/electron.ts b/tests/e2e/support/electron.ts new file mode 100644 index 0000000..c8d7d75 --- /dev/null +++ b/tests/e2e/support/electron.ts @@ -0,0 +1,57 @@ +import { _electron as electron, type ElectronApplication, type Page } from '@playwright/test' +import { mkdtempSync, rmSync } from 'fs' +import { tmpdir } from 'os' +import { resolve } from 'path' +import { loadEnv } from 'vite' + +const DEFAULT_WINDOW_CLOSE_DELAY_MS = 2000 + +export interface TestApplication { + app: ElectronApplication + page: Page + userData: string + close: () => Promise +} + +export async function launchTestApp( + options: { + mode?: 'connected' | 'disconnected' + userData?: string + largeContacts?: number + corruptCache?: boolean + aiFailure?: string + } = {} +): Promise { + const ownsDirectory = !options.userData + const userData = options.userData || mkdtempSync(resolve(tmpdir(), 'wxe-e2e-')) + const localTestEnv = loadEnv('test', process.cwd(), 'WXE_E2E_') + const configuredCloseDelay = Number( + process.env.WXE_E2E_CLOSE_DELAY_MS ?? localTestEnv.WXE_E2E_CLOSE_DELAY_MS + ) + const closeDelayMs = Number.isFinite(configuredCloseDelay) + ? Math.max(0, configuredCloseDelay) + : DEFAULT_WINDOW_CLOSE_DELAY_MS + const app = await electron.launch({ + args: [resolve('tests/e2e/support/electron-main.cjs')], + env: { + ...process.env, + WXE_E2E_USER_DATA: userData, + WXE_E2E_MODE: options.mode || 'connected', + WXE_E2E_LARGE_CONTACTS: String(options.largeContacts || 0), + WXE_E2E_CORRUPT_CACHE: options.corruptCache ? '1' : '0', + WXE_E2E_AI_FAILURE: options.aiFailure || '' + } + }) + const page = await app.firstWindow() + await page.waitForLoadState('domcontentloaded') + return { + app, + page, + userData, + close: async () => { + if (!page.isClosed() && closeDelayMs > 0) await page.waitForTimeout(closeDelayMs) + await app.close().catch(() => undefined) + if (ownsDirectory) rmSync(userData, { recursive: true, force: true }) + } + } +} diff --git a/tests/e2e/visual.spec.ts b/tests/e2e/visual.spec.ts new file mode 100644 index 0000000..964f2b4 --- /dev/null +++ b/tests/e2e/visual.spec.ts @@ -0,0 +1,37 @@ +import { expect, test } from '@playwright/test' +import { existsSync } from 'fs' +import { resolve } from 'path' +import { launchTestApp } from './support/electron' + +const baselineDirectory = resolve(`tests/e2e/__screenshots__/${process.platform}/visual.spec.ts`) +test.skip( + !existsSync(baselineDirectory) && process.env.WXE_UPDATE_VISUAL_BASELINES !== '1', + `No reviewed ${process.platform} visual baseline is committed yet` +) + +test('NAV-01 login page visual @visual', async () => { + const fixture = await launchTestApp({ mode: 'disconnected' }) + try { + await expect(fixture.page.getByRole('heading', { name: 'WechatExplorer' })).toBeVisible() + await expect(fixture.page).toHaveScreenshot('login-page.png', { + animations: 'disabled', + caret: 'hide' + }) + } finally { + await fixture.close() + } +}) + +test('ARCH-01 archive page visual @visual', async () => { + const fixture = await launchTestApp() + try { + await fixture.page.getByText('产品测试群', { exact: true }).click() + await expect(fixture.page.getByText('这是一条脱敏测试消息', { exact: true })).toBeVisible() + await expect(fixture.page).toHaveScreenshot('archive-page.png', { + animations: 'disabled', + caret: 'hide' + }) + } finally { + await fixture.close() + } +}) diff --git a/tests/fixtures/chat-data.json b/tests/fixtures/chat-data.json new file mode 100644 index 0000000..08b9a0f --- /dev/null +++ b/tests/fixtures/chat-data.json @@ -0,0 +1,97 @@ +{ + "self": { + "wxid": "wxid_fixture_self", + "nickname": "测试账号", + "accountRoot": "fixture-account" + }, + "contacts": [ + { + "m_nsUsrName": "group_regular@chatroom", + "m_nsNickName": "产品测试群", + "md5": "group-regular-md5", + "type": "group", + "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9ZQmcAAAAASUVORK5CYII=" + }, + { + "m_nsUsrName": "group_folded@chatroom", + "m_nsNickName": "折叠群聊样本", + "md5": "group-folded-md5", + "type": "group", + "isFolded": true + }, + { + "m_nsUsrName": "filehelper", + "m_nsNickName": "文件传输助手", + "md5": "file-helper-md5", + "type": "user" + } + ], + "messages": { + "group-regular-md5": [ + { + "id": "msg-text", + "from": "user", + "type": "普通文本", + "datetime": "2026-08-01 10:00:00", + "content": "这是一条脱敏测试消息", + "isSender": false, + "name": "测试成员", + "senderId": "wxid_fixture_member", + "createTime": 1785549600, + "contentData": { "type": "text", "content": "这是一条脱敏测试消息" } + }, + { + "id": "msg-image", + "from": "user", + "type": "图片", + "datetime": "2026-08-01 10:01:00", + "content": "[图片]", + "isSender": false, + "localId": 2, + "createTime": 1785549660, + "sessionId": "group_regular@chatroom", + "contentData": { "type": "image", "md5": "fixture-image-md5", "datName": "fixture.dat" } + }, + { + "id": "msg-unknown", + "from": "user", + "type": "不支持的消息", + "datetime": "2026-08-01 10:02:00", + "content": "[未知消息]", + "isSender": false, + "createTime": 1785549720, + "contentData": { "type": "unknown", "raw": "fixture-unknown" } + } + ], + "file-helper-md5": [ + { + "id": "msg-voice", + "from": "user", + "type": "语音", + "datetime": "2026-08-01 11:00:00", + "content": "[语音]", + "isSender": false, + "localId": 11, + "createTime": 1785553200, + "sessionId": "filehelper", + "contentData": { "type": "voice", "duration": 1 } + }, + { + "id": "msg-forward", + "from": "user", + "type": "合并转发", + "datetime": "2026-08-01 11:01:00", + "content": "[合并转发]", + "isSender": false, + "createTime": 1785553260, + "contentData": { + "type": "forwardBundle", + "title": "转发多条内容", + "items": [ + { "messageType": 1, "sender": "测试成员", "sentAt": "11:00", "text": "脱敏转发内容" } + ] + } + } + ] + } +} diff --git a/tests/integration/preload-contract.test.ts b/tests/integration/preload-contract.test.ts new file mode 100644 index 0000000..62d7b83 --- /dev/null +++ b/tests/integration/preload-contract.test.ts @@ -0,0 +1,76 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const invoke = vi.fn() +const on = vi.fn() +const removeListener = vi.fn() +const exposeInMainWorld = vi.fn() + +vi.mock('electron', () => ({ + contextBridge: { exposeInMainWorld }, + ipcRenderer: { invoke, on, removeListener } +})) +vi.mock('@electron-toolkit/preload', () => ({ electronAPI: { fixture: true } })) + +async function loadApi(): Promise { + vi.resetModules() + exposeInMainWorld.mockClear() + Object.defineProperty(process, 'contextIsolated', { configurable: true, value: true }) + await import('../../src/preload/index') + const exposed = exposeInMainWorld.mock.calls.find(([name]) => name === 'api') + if (!exposed) throw new Error('preload did not expose api') + return exposed[1] as typeof window.api +} + +describe('preload IPC contract', () => { + beforeEach(() => { + invoke.mockReset() + on.mockReset() + removeListener.mockReset() + }) + + it('forwards message and media parameters to the exact main channels', async () => { + const api = await loadApi() + invoke.mockResolvedValue({ success: true }) + + await api.getMessages('fixture-user', 10, 20, { limit: 50 }) + expect(invoke).toHaveBeenLastCalledWith('db:getMessages', 'fixture-user', 10, 20, { + limit: 50 + }) + + await api.getImage('fixture-md5', 'fixture.dat', 'fixture-session', { + force: true, + priority: 0 + }) + expect(invoke).toHaveBeenLastCalledWith( + 'db:getImage', + 'fixture-md5', + 'fixture.dat', + 'fixture-session', + { force: true, priority: 0 } + ) + }) + + it('preserves key API return values without exposing ipcRenderer', async () => { + const api = await loadApi() + invoke.mockResolvedValueOnce({ success: false, code: 'DATABASE_OPEN_FAILED' }) + await expect(api.testConnection('b'.repeat(64), 'fixture-root')).resolves.toEqual({ + success: false, + code: 'DATABASE_OPEN_FAILED' + }) + expect(invoke).toHaveBeenCalledWith('db:testConnection', 'b'.repeat(64), 'fixture-root') + expect(api).not.toHaveProperty('ipcRenderer') + expect(api).not.toHaveProperty('send') + }) + + it('unsubscribes the same listener registered for native database changes', async () => { + const api = await loadApi() + const callback = vi.fn() + const unsubscribe = api.onWcdbChange(callback) + expect(on).toHaveBeenCalledWith('wcdb-change', expect.any(Function)) + const listener = on.mock.calls.at(-1)?.[1] + listener({}, { type: 'insert', json: '{"fixture":true}' }) + expect(callback).toHaveBeenCalledWith({ type: 'insert', json: '{"fixture":true}' }) + unsubscribe() + expect(removeListener).toHaveBeenCalledWith('wcdb-change', listener) + }) +}) diff --git a/tests/setup/component.ts b/tests/setup/component.ts new file mode 100644 index 0000000..0e6c9df --- /dev/null +++ b/tests/setup/component.ts @@ -0,0 +1,15 @@ +import '@testing-library/jest-dom/vitest' +import { cleanup } from '@testing-library/react' +import { afterEach, vi } from 'vitest' + +afterEach(() => cleanup()) + +Object.defineProperty(globalThis.URL, 'createObjectURL', { + configurable: true, + value: vi.fn(() => 'blob:wxe-test-audio') +}) + +Object.defineProperty(globalThis.URL, 'revokeObjectURL', { + configurable: true, + value: vi.fn() +}) diff --git a/tests/smoke/native-environment.test.mjs b/tests/smoke/native-environment.test.mjs new file mode 100644 index 0000000..d27a2e4 --- /dev/null +++ b/tests/smoke/native-environment.test.mjs @@ -0,0 +1,23 @@ +import assert from 'node:assert/strict' +import fs from 'node:fs' +import test from 'node:test' + +const enabled = process.env.WXE_REAL_DATA_SMOKE === '1' + +test( + 'native WCDB opens a disposable fixture account on this machine', + { skip: enabled ? false : 'set WXE_REAL_DATA_SMOKE=1 and WXE_SMOKE_DB_ROOT to opt in' }, + () => { + const root = process.env.WXE_SMOKE_DB_ROOT || '' + assert.ok(root, 'WXE_SMOKE_DB_ROOT is required') + assert.ok(fs.existsSync(root), 'WXE_SMOKE_DB_ROOT must exist') + } +) + +test('system permission prompts are verified manually on a clean OS account', { + skip: 'manual smoke checklist: docs/testing.md' +}) + +test('signed installer install, upgrade and uninstall are verified manually', { + skip: 'manual smoke checklist: docs/testing.md' +}) diff --git a/tests/stability-compat.test.mjs b/tests/stability-compat.test.mjs deleted file mode 100644 index dc1b5ed..0000000 --- a/tests/stability-compat.test.mjs +++ /dev/null @@ -1,64 +0,0 @@ -import assert from 'node:assert/strict' -import test from 'node:test' -import { parseMessageContent } from '../src/main/message-parser.ts' -import { classifyStickerHttpFailure } from '../src/shared/sticker.ts' - -test('merged forwarding messages expose expandable record items', () => { - const content = ` - 项目讨论记录19 - - - - 2026-08-01 10:00 - - - - - 2026-08-01 10:01 - - - ]]> - ` - - const parsed = parseMessageContent(content, 49) - assert.equal(parsed.type, 'forwardBundle') - assert.equal(parsed.title, '项目讨论记录') - assert.deepEqual( - parsed.items.map((item) => [item.sender, item.text]), - [ - ['张三', '第一条消息'], - ['李四', '[图片]'] - ] - ) -}) - -test('unknown message types are not misclassified as text', () => { - const parsed = parseMessageContent('1', 9999) - assert.equal(parsed.type, 'unknown') - assert.equal(parsed.messageType, 9999) -}) - -test('sticker 403 with expired timestamp is classified as an expired link', () => { - const result = classifyStickerHttpFailure( - 403, - 'https://example.invalid/sticker?expire=1700000000', - 1_800_000_000_000 - ) - assert.equal(result.code, 'link_expired') -}) - -test('sticker authorization and removal failures remain distinct', () => { - assert.equal( - classifyStickerHttpFailure(401, 'https://example.invalid/sticker').code, - 'authentication_required' - ) - assert.equal( - classifyStickerHttpFailure(403, 'https://example.invalid/sticker').code, - 'access_denied' - ) - assert.equal( - classifyStickerHttpFailure(404, 'https://example.invalid/sticker').code, - 'resource_removed' - ) -}) diff --git a/tests/unit/bootstrap-cache.test.ts b/tests/unit/bootstrap-cache.test.ts new file mode 100644 index 0000000..5e09045 --- /dev/null +++ b/tests/unit/bootstrap-cache.test.ts @@ -0,0 +1,81 @@ +import { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs' +import { tmpdir } from 'os' +import { join } from 'path' +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' +import type { Contact, Message } from '../../src/shared/types' + +const userData = mkdtempSync(join(tmpdir(), 'wxe-bootstrap-test-')) + +vi.mock('electron', () => ({ + app: { getPath: () => userData } +})) + +import { + clearBootstrapCache, + flushBootstrapCacheWritesSync, + getBootstrapCache, + getCachedMessages, + saveBootstrapContacts, + saveCachedMessages +} from '../../src/main/services/bootstrap-cache' + +const accountRoot = 'fixture-account-root' +const contact: Contact = { + m_nsUsrName: 'fixture-user', + m_nsNickName: '脱敏联系人', + md5: 'fixture-md5', + type: 'user' +} + +function findFile(name: string): string { + const visit = (directory: string): string | null => { + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const file = join(directory, entry.name) + if (entry.isDirectory()) { + const nested = visit(file) + if (nested) return nested + } else if (entry.name === name) return file + } + return null + } + const result = visit(userData) + if (!result) throw new Error(`${name} was not written`) + return result +} + +describe('bootstrap cache', () => { + beforeAll(() => rmSync(userData, { recursive: true, force: true })) + beforeEach(() => clearBootstrapCache()) + afterAll(() => rmSync(userData, { recursive: true, force: true })) + + it('persists contacts and caps each message bucket', () => { + saveBootstrapContacts(accountRoot, [contact]) + const messages: Message[] = Array.from({ length: 140 }, (_, index) => ({ + id: String(index), + from: 'user', + type: '文本', + datetime: '2026-08-01 10:00:00', + content: `fixture-${index}`, + isSender: false, + createTime: index + 1 + })) + saveCachedMessages(accountRoot, contact.md5, undefined, undefined, messages) + flushBootstrapCacheWritesSync() + clearBootstrapCache() + + expect(getBootstrapCache(accountRoot)?.contacts).toEqual([contact]) + const cached = getCachedMessages(accountRoot, contact.md5) + expect(cached).toHaveLength(120) + expect(cached[0].id).toBe('20') + }) + + it('degrades to a cache miss when persisted JSON is corrupted', () => { + saveBootstrapContacts(accountRoot, [contact]) + flushBootstrapCacheWritesSync() + const startup = findFile('startup.json') + expect(readFileSync(startup, 'utf8')).toContain('fixture-user') + writeFileSync(startup, '{broken', 'utf8') + clearBootstrapCache() + expect(getBootstrapCache(accountRoot)).toBeNull() + }) +}) diff --git a/tests/unit/image-decrypt.test.ts b/tests/unit/image-decrypt.test.ts new file mode 100644 index 0000000..7de42c7 --- /dev/null +++ b/tests/unit/image-decrypt.test.ts @@ -0,0 +1,56 @@ +import crypto from 'crypto' +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs' +import { tmpdir } from 'os' +import { join } from 'path' +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' + +const root = mkdtempSync(join(tmpdir(), 'wxe-image-test-')) + +vi.mock('electron', () => ({ app: { getPath: () => root } })) +vi.mock('../../src/main/services/settings-store', () => ({ + loadSettings: () => ({ ffmpegPath: '' }) +})) +vi.mock('../../src/main/wcdb4-client', () => ({ Wcdb4Client: class {} })) + +import { ImageDecryptService } from '../../src/main/image-decrypt-service' + +const aesKey = '0123456789abcdef' +const xorKey = 0x40 + +function writeV2Dat(file: string): Buffer { + const aesPlain = Buffer.from([0x89, 0x50, 0x4e, 0x47, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) + const padded = Buffer.concat([aesPlain, Buffer.alloc(16, 16)]) + const cipher = crypto.createCipheriv('aes-128-ecb', Buffer.from(aesKey, 'ascii'), null) + cipher.setAutoPadding(false) + const encrypted = Buffer.concat([cipher.update(padded), cipher.final()]) + const raw = Buffer.from([13, 14]) + const tailPlain = Buffer.from([15, 16]) + const tailCipher = Buffer.from(tailPlain.map((value) => value ^ xorKey)) + const header = Buffer.alloc(15) + Buffer.from([0x07, 0x08, 0x56, 0x32, 0x08, 0x07]).copy(header) + header.writeInt32LE(aesPlain.length, 6) + header.writeInt32LE(tailPlain.length, 10) + writeFileSync(file, Buffer.concat([header, encrypted, raw, tailCipher])) + return Buffer.concat([aesPlain, raw, tailPlain]) +} + +describe('DAT image decryption', () => { + beforeAll(() => mkdirSync(root, { recursive: true })) + afterAll(() => rmSync(root, { recursive: true, force: true })) + + it('decrypts a synthetic V2 AES/raw/XOR fixture', () => { + const file = join(root, 'fixture.dat') + const expected = writeV2Dat(file) + expect(new ImageDecryptService('0x40', aesKey).decryptImage(file)).toEqual(expected) + }) + + it('rejects the wrong AES key and unsupported legacy signatures accurately', () => { + const file = join(root, 'fixture.dat') + writeV2Dat(file) + expect(new ImageDecryptService('0x40', 'fedcba9876543210').decryptImage(file)).toBeNull() + + const legacy = join(root, 'legacy.dat') + writeFileSync(legacy, Buffer.from([0xff, 0xd8, 0xff, 0x00])) + expect(new ImageDecryptService('0x40', aesKey).decryptImage(legacy)).toBeNull() + }) +}) diff --git a/tests/unit/key-validation.test.ts b/tests/unit/key-validation.test.ts new file mode 100644 index 0000000..a8f34af --- /dev/null +++ b/tests/unit/key-validation.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it, vi } from 'vitest' + +vi.mock('electron', () => ({ + app: { getPath: () => 'fixture-settings' }, + safeStorage: { isEncryptionAvailable: () => false } +})) +import { + isDatabaseKeyFormatValid, + mapAutoDetectPhase, + normalizeDatabaseKey +} from '../../src/renderer/src/features/settings/database-key/utils' +import { + normalizeImageXorKey, + validateImageKeyRequest +} from '../../src/main/services/image-key-config-service' + +describe('database key validation', () => { + it('normalizes a prefixed key without accepting the wrong length', () => { + const key = `0x${'a'.repeat(64)}` + expect(normalizeDatabaseKey(key)).toBe('a'.repeat(64)) + expect(isDatabaseKeyFormatValid(key)).toBe(true) + expect(isDatabaseKeyFormatValid('a'.repeat(63))).toBe(false) + expect(isDatabaseKeyFormatValid('z'.repeat(64))).toBe(false) + }) + + it('maps automatic detection progress into stable phases', () => { + expect(mapAutoDetectPhase('正在查找微信进程')).toBeGreaterThan(0) + expect(mapAutoDetectPhase('已获取数据库密钥')).toBe(5) + }) +}) + +describe('image key validation', () => { + it.each([ + [64, '0x40'], + ['64', '0x40'], + ['0xff', '0xFF'], + ['', '0x40'] + ])('normalizes %s to %s', (input, expected) => { + expect(normalizeImageXorKey(input)).toBe(expected) + }) + + it('keeps database and image key validation independent', () => { + const result = validateImageKeyRequest({ + resourceRoot: ' fixture-root ', + xorKey: '64', + aesKey: '0123456789abcdef' + }) + expect(result).toEqual({ + success: true, + resourceRoot: 'fixture-root', + xorKey: '0x40', + aesKey: '0123456789abcdef' + }) + expect( + validateImageKeyRequest({ resourceRoot: 'fixture-root', xorKey: '999', aesKey: 'short' }) + .success + ).toBe(false) + }) +}) diff --git a/tests/unit/message-pagination.test.ts b/tests/unit/message-pagination.test.ts new file mode 100644 index 0000000..c6a836f --- /dev/null +++ b/tests/unit/message-pagination.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest' +import type { Message } from '../../src/shared/types' +import { mergeMessagePages } from '../../src/renderer/src/utils/message-pages' + +const makeMessage = (id: string, createTime: number): Message => ({ + id, + from: 'user', + type: '文本', + datetime: new Date(createTime * 1000).toISOString(), + content: id, + isSender: false, + createTime +}) + +describe('message pagination', () => { + it('sorts older pages and removes overlapping records', () => { + const merged = mergeMessagePages( + [makeMessage('oldest', 1), makeMessage('overlap', 2)], + [makeMessage('overlap', 2), makeMessage('latest', 3)] + ) + expect(merged.map((message) => message.id)).toEqual(['oldest', 'overlap', 'latest']) + }) +}) diff --git a/tests/unit/message-parser.test.ts b/tests/unit/message-parser.test.ts new file mode 100644 index 0000000..7c91dd6 --- /dev/null +++ b/tests/unit/message-parser.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest' +import { parseMessageContent } from '../../src/main/message-parser' + +describe('message parser', () => { + it('parses image, voice and sticker messages without confusing their types', () => { + expect(parseMessageContent('', 3)).toMatchObject({ + type: 'image', + md5: '0123456789abcdef0123456789abcdef' + }) + expect(parseMessageContent('voice fixture', 34)).toEqual({ type: 'voice' }) + expect( + parseMessageContent( + '', + 47 + ) + ).toMatchObject({ type: 'sticker', md5: 'abcdefabcdefabcdefabcdefabcdefab' }) + }) + + it('parses merged forwards and preserves nested visible text', () => { + const parsed = parseMessageContent( + '19转发多条内容测试成员脱敏内容', + 49 + ) + expect(parsed.type).toBe('forwardBundle') + if (parsed.type === 'forwardBundle') { + expect(parsed.title).toBe('转发多条内容') + expect(parsed.items.map((item) => item.text).join(' ')).toContain('脱敏内容') + } + }) + + it('uses an explicit unknown type for unsupported messages', () => { + expect(parseMessageContent('opaque fixture payload', 999)).toEqual({ + type: 'unknown', + raw: 'opaque fixture payload', + messageType: 999 + }) + }) +}) diff --git a/tests/unit/message-state.test.ts b/tests/unit/message-state.test.ts new file mode 100644 index 0000000..b0f974a --- /dev/null +++ b/tests/unit/message-state.test.ts @@ -0,0 +1,81 @@ +// @vitest-environment jsdom +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Message } from '../../src/shared/types' +import { + buildMessageGroups, + formatMessageTime +} from '../../src/renderer/src/components/chat/messageGrouping' +import { + buildSearchCacheKey, + parseSearchCacheKey, + readSearchCache, + writeSearchCache +} from '../../src/renderer/src/components/search/searchUtils' + +const message = (id: string, createTime: number, from = 'user'): Message => ({ + id, + from, + type: '文本', + datetime: new Date(createTime * 1000).toISOString(), + content: id, + isSender: from === 'assistant', + senderId: from, + createTime +}) + +describe('message grouping and dates', () => { + it('groups adjacent messages but keeps system and distant messages separate', () => { + const groups = buildMessageGroups([ + message('one', 1000), + message('two', 1060), + { ...message('system', 1070, 'system'), type: '系统消息' }, + message('three', 2000) + ]) + expect(groups.map((group) => group.messages.map((item) => item.id))).toEqual([ + ['one', 'two'], + ['system'], + ['three'] + ]) + }) + + it('formats today and yesterday deterministically', () => { + vi.useFakeTimers() + vi.setSystemTime(new Date('2026-08-01T12:00:00+08:00')) + expect( + formatMessageTime( + message('today', Math.floor(Date.parse('2026-08-01T10:00:00+08:00') / 1000)) + ) + ).toContain('今天') + expect( + formatMessageTime( + message('yesterday', Math.floor(Date.parse('2026-07-31T10:00:00+08:00') / 1000)) + ) + ).toContain('昨天') + vi.useRealTimers() + }) +}) + +describe('search cache', () => { + beforeEach(() => localStorage.clear()) + + it('normalizes the key and survives invalid persisted state', () => { + const key = buildSearchCacheKey('global', '', '7d', ' Windows 性能 ') + expect(parseSearchCacheKey(key)).toMatchObject({ query: 'windows 性能', range: '7d' }) + localStorage.setItem('wxe_ai_search_cache_v1', '{broken') + expect(readSearchCache(key)).toBeNull() + }) + + it('writes and reads an isolated cache record', () => { + const key = buildSearchCacheKey('conversation', 'fixture-contact', 'today', '图片') + const record = { + version: 1 as const, + key, + query: '图片', + answer: '固定假回答', + evidence: [], + createdAt: 1 + } + writeSearchCache(record) + expect(readSearchCache(key)).toMatchObject({ answer: '固定假回答' }) + }) +}) diff --git a/tests/unit/security-and-errors.test.ts b/tests/unit/security-and-errors.test.ts new file mode 100644 index 0000000..e4aea7e --- /dev/null +++ b/tests/unit/security-and-errors.test.ts @@ -0,0 +1,53 @@ +import { mkdtempSync, readFileSync, rmSync } from 'fs' +import { tmpdir } from 'os' +import { join } from 'path' +import { afterAll, describe, expect, it, vi } from 'vitest' +import { classifyStickerHttpFailure } from '../../src/shared/sticker' + +const logs = mkdtempSync(join(tmpdir(), 'wxe-log-test-')) +vi.mock('electron', () => ({ + app: { getPath: () => logs, isPackaged: true }, + shell: { showItemInFolder: vi.fn() } +})) + +import { AppLogger } from '../../src/main/app-logger' + +describe('sensitive logging', () => { + afterAll(() => rmSync(logs, { recursive: true, force: true })) + + it('does not persist database keys, API keys or bearer tokens', () => { + const databaseKey = 'a'.repeat(64) + const logger = new AppLogger() + logger.write({ + level: 'error', + scope: 'fixture', + message: `database open failed key=${databaseKey}`, + details: { + databaseKey, + apiKey: 'sk-fixture-secret-value', + authorization: 'Bearer fixture-token-value' + } + }) + const persisted = readFileSync(logger.logPath, 'utf8') + expect(persisted).not.toContain(databaseKey) + expect(persisted).not.toContain('sk-fixture-secret-value') + expect(persisted).not.toContain('fixture-token-value') + expect(persisted).toContain('***') + }) +}) + +describe('sticker HTTP failures', () => { + it('distinguishes expired, unauthorized, removed and rate-limited resources', () => { + expect(classifyStickerHttpFailure(403, 'https://fixture.invalid/a?expires=1', 2_000).code).toBe( + 'link_expired' + ) + expect(classifyStickerHttpFailure(403, 'https://fixture.invalid/a').code).toBe('access_denied') + expect(classifyStickerHttpFailure(401, 'https://fixture.invalid/a').code).toBe( + 'authentication_required' + ) + expect(classifyStickerHttpFailure(410, 'https://fixture.invalid/a').code).toBe( + 'resource_removed' + ) + expect(classifyStickerHttpFailure(429, 'https://fixture.invalid/a').code).toBe('rate_limited') + }) +}) diff --git a/vitest.component.config.ts b/vitest.component.config.ts new file mode 100644 index 0000000..ba821c6 --- /dev/null +++ b/vitest.component.config.ts @@ -0,0 +1,20 @@ +import { resolve } from 'path' +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@renderer': resolve(__dirname, 'src/renderer/src') + } + }, + test: { + name: 'component', + environment: 'jsdom', + include: ['tests/component/**/*.test.tsx'], + setupFiles: ['tests/setup/component.ts'], + clearMocks: true, + restoreMocks: true + } +}) diff --git a/vitest.integration.config.ts b/vitest.integration.config.ts new file mode 100644 index 0000000..6448a66 --- /dev/null +++ b/vitest.integration.config.ts @@ -0,0 +1,17 @@ +import { resolve } from 'path' +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + resolve: { + alias: { + '@renderer': resolve(__dirname, 'src/renderer/src') + } + }, + test: { + name: 'integration', + environment: 'node', + include: ['tests/integration/**/*.test.ts'], + clearMocks: true, + restoreMocks: true + } +}) diff --git a/vitest.unit.config.ts b/vitest.unit.config.ts new file mode 100644 index 0000000..9cce389 --- /dev/null +++ b/vitest.unit.config.ts @@ -0,0 +1,17 @@ +import { resolve } from 'path' +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + resolve: { + alias: { + '@renderer': resolve(__dirname, 'src/renderer/src') + } + }, + test: { + name: 'unit', + environment: 'node', + include: ['tests/unit/**/*.test.ts'], + clearMocks: true, + restoreMocks: true + } +})