From cc63456f726d0cb0d2fd517038a1e458dec84a9b Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Sat, 18 Oct 2025 13:14:13 -0500 Subject: [PATCH] test suite added --- .gitignore | 4 + README.md | 17 + TEST_SETUP_SUMMARY.md | 172 ++++++ package.json | 11 +- pnpm-lock.yaml | 1000 ++++++++++++++++++++++++++++++++- src/tests/README.md | 48 ++ src/tests/edgeCases.test.ts | 309 ++++++++++ src/tests/integration.test.ts | 153 +++++ src/tests/mockData.ts | 142 +++++ src/tests/setup.ts | 62 ++ src/tests/types.test.ts | 157 ++++++ src/tests/utils.test.ts | 295 ++++++++++ tsconfig.json | 5 +- vite.config.ts | 18 + 14 files changed, 2389 insertions(+), 4 deletions(-) create mode 100644 TEST_SETUP_SUMMARY.md create mode 100644 src/tests/README.md create mode 100644 src/tests/edgeCases.test.ts create mode 100644 src/tests/integration.test.ts create mode 100644 src/tests/mockData.ts create mode 100644 src/tests/setup.ts create mode 100644 src/tests/types.test.ts create mode 100644 src/tests/utils.test.ts diff --git a/.gitignore b/.gitignore index 9368343..57806bd 100644 --- a/.gitignore +++ b/.gitignore @@ -42,5 +42,9 @@ web-ext-artifacts/ *.tmp .temp/ +# Test coverage +coverage/ +.nyc_output/ + # Claude .claude/ diff --git a/README.md b/README.md index e2ff995..10911d6 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,23 @@ This extension provides several advantages over the official Claude.ai data expo pnpm type-check # Run TypeScript type checker ``` +6. **Testing** + + ```bash + pnpm test # Run tests in watch mode + pnpm test:run # Run tests once + pnpm test:ui # Run tests with UI + pnpm test:coverage # Run tests with coverage report + ``` + + The test suite includes: + - **Unit tests** for utility functions (utils.ts) + - **Type validation tests** for TypeScript types + - **Integration tests** for complete workflows + - **98%+ coverage** on core utility functions + + See `src/tests/README.md` for more details about the test structure. + ## Installation ### Firefox diff --git a/TEST_SETUP_SUMMARY.md b/TEST_SETUP_SUMMARY.md new file mode 100644 index 0000000..4418fab --- /dev/null +++ b/TEST_SETUP_SUMMARY.md @@ -0,0 +1,172 @@ +# Test Setup Summary + +## What Was Done + +Successfully set up Vitest testing framework for the Claude Conversation Exporter project with comprehensive test coverage. + +## Components Added + +### 1. Dependencies Installed +- `vitest` - Fast unit test framework +- `@vitest/ui` - Interactive UI for tests +- `@vitest/coverage-v8` - Code coverage reporting +- `happy-dom` - Lightweight DOM implementation for tests +- `jsdom` - Full DOM implementation (alternative) + +### 2. Configuration Files + +**vite.config.ts** - Added test configuration: +```typescript +test: { + globals: true, + environment: 'happy-dom', + setupFiles: ['./src/tests/setup.ts'], + include: ['src/tests/**/*.test.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + exclude: [ + 'node_modules/', + 'src/tests/', + '**/*.d.ts', + '**/*.config.*', + '**/mockData', + ], + }, +} +``` + +**tsconfig.json** - Added Vitest types and included test files + +**package.json** - Added test scripts: +- `pnpm test` - Watch mode +- `pnpm test:run` - Run once +- `pnpm test:ui` - Interactive UI +- `pnpm test:coverage` - Coverage report + +**.gitignore** - Added coverage directory exclusion + +### 3. Test Files Created + +#### `src/tests/setup.ts` +- Global test setup +- Browser API mocks (browser.*, chrome.*) +- DOM method mocks + +#### `src/tests/mockData.ts` +- Reusable test fixtures +- Mock conversations with various scenarios +- Mock messages in different formats + +#### `src/tests/utils.test.ts` (31 tests) +- `inferModel()` - 6 tests +- `getCurrentBranch()` - 5 tests +- `convertToMarkdown()` - 5 tests +- `convertToText()` - 5 tests +- `generateFilename()` - 7 tests +- `downloadFile()` - 3 tests + +#### `src/tests/types.test.ts` (12 tests) +- Type validation for all TypeScript interfaces +- Message format validation +- Extension message types + +#### `src/tests/integration.test.ts` (8 tests) +- Full export workflow tests +- Format conversion consistency +- Filename generation scenarios +- Model inference edge cases + +#### `src/tests/edgeCases.test.ts` (17 tests) +- Empty data handling +- Complex branching scenarios +- Filename edge cases +- Model inference boundaries +- Content conversion edge cases + +#### `src/tests/README.md` +- Documentation for test structure +- Instructions for running tests +- Coverage information + +## Test Coverage Results + +### Overall Coverage (utils.ts) +- **Statements**: 100% +- **Branches**: 96% +- **Functions**: 100% +- **Lines**: 100% + +### Total Test Count +- **68 tests total** +- **67 passing** +- **1 skipped** (circular reference test - would cause infinite loop) + +## Running Tests + +```bash +# Run all tests in watch mode +pnpm test + +# Run tests once (for CI/CD) +pnpm test:run + +# Run tests with interactive UI +pnpm test:ui + +# Generate coverage report +pnpm test:coverage +``` + +## What's Tested + +✅ Model inference logic with date-based detection +✅ Branch traversal in conversation trees +✅ Markdown conversion with/without metadata +✅ Text conversion with sender abbreviation +✅ Filename sanitization and generation +✅ File download functionality +✅ Type definitions and interfaces +✅ Integration workflows +✅ Edge cases (empty data, long content, special characters) +✅ Date boundary conditions +✅ Mixed format message handling + +## What's Not Tested + +The following files are not tested as they require complex browser environment mocking: +- `background.ts` - Background service worker (browser-specific) +- `content.ts` - Content script (DOM manipulation) +- `popup.ts` - Popup UI (DOM events) +- `options.ts` - Options page (DOM events) +- `browse.ts` - Browse page (DOM events, large UI) + +These files contain primarily UI logic and would require: +- Full DOM event simulation +- Browser extension API integration testing +- UI component testing framework (e.g., Testing Library) + +The core business logic in `utils.ts` has excellent coverage, which is the most critical for correctness. + +## CI/CD Integration + +The test suite can be integrated into CI/CD pipelines: + +```yaml +# Example GitHub Actions +- name: Run Tests + run: pnpm test:run + +- name: Generate Coverage + run: pnpm test:coverage +``` + +## Future Improvements + +Potential areas for expansion: +1. Add E2E tests with Playwright or Puppeteer +2. Test browser-specific functionality with webextension-polyfill +3. Add visual regression testing for UI components +4. Mock fetch API for content script testing +5. Add performance benchmarks +6. Test the actual ZIP file generation functionality diff --git a/package.json b/package.json index 9daf13c..6c9a985 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,11 @@ "lint:fix": "eslint src/**/*.ts --fix", "format": "prettier --check \"src/**/*.{ts,html,css,json}\"", "format:fix": "prettier --write \"src/**/*.{ts,html,css,json}\"", - "check": "pnpm type-check && pnpm lint && pnpm format", + "test": "vitest", + "test:ui": "vitest --ui", + "test:run": "vitest run", + "test:coverage": "vitest run --coverage", + "check": "pnpm type-check && pnpm lint && pnpm format && pnpm test:run", "firefox:run": "web-ext run --source-dir=dist/firefox --start-url https://claude.ai", "chrome:run": "echo 'Load dist/chrome in chrome://extensions with Developer Mode enabled'", "clean": "rm -rf dist" @@ -39,13 +43,18 @@ "@types/jszip": "^3.4.0", "@typescript-eslint/eslint-plugin": "^8.46.1", "@typescript-eslint/parser": "^8.46.1", + "@vitest/coverage-v8": "^3.2.4", + "@vitest/ui": "^3.2.4", "eslint": "^9.38.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.4", + "happy-dom": "^20.0.5", + "jsdom": "^27.0.1", "prettier": "^3.6.2", "typescript": "^5.9.3", "typescript-eslint": "^8.46.1", "vite": "^7.1.10", + "vitest": "^3.2.4", "web-ext": "^9.0.0" }, "dependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2edc0af..3244ad4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: specifier: ^143.0.0 version: 143.0.0 '@types/jszip': - specifier: ^3.4.1 + specifier: ^3.4.0 version: 3.4.1 '@typescript-eslint/eslint-plugin': specifier: ^8.46.1 @@ -30,6 +30,12 @@ importers: '@typescript-eslint/parser': specifier: ^8.46.1 version: 8.46.1(eslint@9.38.0)(typescript@5.9.3) + '@vitest/coverage-v8': + specifier: ^3.2.4 + version: 3.2.4(vitest@3.2.4) + '@vitest/ui': + specifier: ^3.2.4 + version: 3.2.4(vitest@3.2.4) eslint: specifier: ^9.38.0 version: 9.38.0 @@ -39,6 +45,12 @@ importers: eslint-plugin-prettier: specifier: ^5.5.4 version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.38.0))(eslint@9.38.0)(prettier@3.6.2) + happy-dom: + specifier: ^20.0.5 + version: 20.0.5 + jsdom: + specifier: ^27.0.1 + version: 27.0.1(postcss@8.5.6) prettier: specifier: ^3.6.2 version: 3.6.2 @@ -51,24 +63,91 @@ importers: vite: specifier: ^7.1.10 version: 7.1.10(@types/node@24.8.1) + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@24.8.1)(@vitest/ui@3.2.4)(happy-dom@20.0.5)(jsdom@27.0.1(postcss@8.5.6)) web-ext: specifier: ^9.0.0 version: 9.0.0 packages: + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@asamuzakjp/css-color@4.0.5': + resolution: {integrity: sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==} + + '@asamuzakjp/dom-selector@6.7.2': + resolution: {integrity: sha512-ccKogJI+0aiDhOahdjANIc9SDixSud1gbwdVrhn7kMopAtLXqsz9MKmQQtIl6Y5aC2IYq+j4dz/oedL2AVMmVQ==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.27.1': resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/runtime@7.28.4': resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.14': + resolution: {integrity: sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + '@devicefarmer/adbkit-logcat@2.1.3': resolution: {integrity: sha512-yeaGFjNBc/6+svbDeul1tNHtNChw6h8pSHAt5D+JsedUrMTN7tla7B15WLDyekxsuS2XlZHRxpuC6m92wiwCNw==} engines: {node: '>= 4'} @@ -317,9 +396,27 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@mdn/browser-compat-data@7.1.7': resolution: {integrity: sha512-bpWZ7hidvjrwNWcMngZ8nTMTxn8WhnQntsGqEYgPr1vjy66kfwfDVizwXg6PvsgoANZ7nhuRBmvzjpCMk4ITDw==} @@ -335,6 +432,10 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@pkgr/core@0.2.9': resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -351,6 +452,9 @@ packages: resolution: {integrity: sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==} engines: {node: '>=12'} + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@rollup/rollup-android-arm-eabi@4.52.5': resolution: {integrity: sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==} cpu: [arm] @@ -467,9 +571,15 @@ packages: peerDependencies: vite: ^4.0.3 || ^5.0.0 + '@types/chai@5.2.2': + resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} + '@types/chrome@0.0.233': resolution: {integrity: sha512-T3HUW8LDhHfYPbUkOZilhnqX04sZqEFIQO0OIG8byZ7PBIk2jYys1AkNg56UbtKbsh3dABmOniK+0Q3oyo7Sxg==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -498,9 +608,15 @@ packages: '@types/minimatch@3.0.5': resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} + '@types/node@20.19.22': + resolution: {integrity: sha512-hRnu+5qggKDSyWHlnmThnUqg62l29Aj/6vcYgUaSFL9oc7DVjeWEQN3PRgdSc6F8d9QRMWkf36CLMch1Do/+RQ==} + '@types/node@24.8.1': resolution: {integrity: sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==} + '@types/whatwg-mimetype@3.0.2': + resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} + '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -566,6 +682,49 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@vitest/coverage-v8@3.2.4': + resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==} + peerDependencies: + '@vitest/browser': 3.2.4 + vitest: 3.2.4 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/ui@3.2.4': + resolution: {integrity: sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==} + peerDependencies: + vitest: 3.2.4 + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -645,6 +804,13 @@ packages: resolution: {integrity: sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==} engines: {node: '>=12'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-v8-to-istanbul@0.3.7: + resolution: {integrity: sha512-kr1Hy6YRZBkGQSb6puP+D6FQ59Cx4m0siYhAxygMCAgadiWQ6oxAxQXHOMvJx67SJ63jRoVIIg5eXzUbbct1ww==} + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -658,6 +824,9 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + bluebird@3.7.2: resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} @@ -688,6 +857,10 @@ packages: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -696,6 +869,10 @@ packages: resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} engines: {node: '>=16'} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -704,6 +881,10 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + cheerio-select@2.1.0: resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} @@ -779,10 +960,22 @@ packages: css-select@5.2.2: resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-what@6.2.2: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} + cssstyle@5.3.1: + resolution: {integrity: sha512-g5PC9Aiph9eiczFpcgUhd9S4UUO3F+LHGRIi5NUMZ+4xtoIYbHNZwZnWA2JsFGe8OU8nl4WyaEFiZuGuxlutJQ==} + engines: {node: '>=20'} + + data-urls@6.0.0: + resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==} + engines: {node: '>=20'} + debounce@1.2.1: resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} @@ -808,6 +1001,13 @@ packages: resolution: {integrity: sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -855,12 +1055,18 @@ packages: resolution: {integrity: sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==} engines: {node: '>=18'} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encoding-sniffer@0.2.1: resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} @@ -872,6 +1078,9 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es6-error@4.1.1: resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} @@ -974,6 +1183,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -982,6 +1194,10 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + engines: {node: '>=12.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1023,6 +1239,9 @@ packages: picomatch: optional: true + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -1059,6 +1278,10 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + fs-extra@11.3.2: resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} engines: {node: '>=14.14'} @@ -1094,6 +1317,10 @@ packages: glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported @@ -1125,13 +1352,28 @@ packages: growly@1.3.0: resolution: {integrity: sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==} + happy-dom@20.0.5: + resolution: {integrity: sha512-AiqA0rfS7WR1kihXt9W9aA5LFLaOKzwiL+QoI7BkOQ0r21C7VHTOf4k8QNlnWYaHLhpI2tZzJPLV1lY1obDTmw==} + engines: {node: '>=20.0.0'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + htmlparser2@10.0.0: resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -1242,6 +1484,9 @@ packages: resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-relative@0.1.3: resolution: {integrity: sha512-wBOr+rNM4gkAZqoLRJI4myw5WzzIdQosFAAbnvfXP5z1LyzgAI3ivOKehC5KfqlQJZoihVhirgtCBj378Eg8GA==} engines: {node: '>=0.10.0'} @@ -1266,16 +1511,47 @@ 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'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jose@5.9.6: resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==} js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsdom@27.0.1: + resolution: {integrity: sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA==} + engines: {node: '>=20'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -1325,15 +1601,35 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.2.2: + resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} + engines: {node: 20 || >=22} + magic-string@0.30.19: resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} marky@1.3.0: resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1352,6 +1648,14 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1404,6 +1708,9 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-json@10.0.1: resolution: {integrity: sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==} engines: {node: '>=18'} @@ -1428,6 +1735,9 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parse5@8.0.0: + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1440,6 +1750,17 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -1553,6 +1874,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + run-applescript@7.1.0: resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} @@ -1573,6 +1897,10 @@ packages: sax@1.4.1: resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + semver@7.7.2: resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} @@ -1600,6 +1928,17 @@ packages: shellwords@0.1.1: resolution: {integrity: sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + sonic-boom@4.2.0: resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} @@ -1624,10 +1963,20 @@ packages: split@1.0.1: resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + string-width@7.2.0: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} @@ -1667,6 +2016,9 @@ packages: resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} engines: {node: '>=14.16'} + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + stubborn-fs@1.2.5: resolution: {integrity: sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==} @@ -1674,10 +2026,17 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + synckit@0.11.11: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} + test-exclude@7.0.1: + resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} + engines: {node: '>=18'} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -1687,10 +2046,35 @@ packages: through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + tldts-core@7.0.17: + resolution: {integrity: sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==} + + tldts@7.0.17: + resolution: {integrity: sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==} + hasBin: true + tmp@0.2.5: resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} engines: {node: '>=14.14'} @@ -1699,6 +2083,18 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -1732,6 +2128,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.14.0: resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==} @@ -1761,6 +2160,11 @@ packages: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + vite@7.1.10: resolution: {integrity: sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1801,6 +2205,38 @@ packages: yaml: optional: true + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + watchpack@2.4.4: resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} engines: {node: '>=10.13.0'} @@ -1813,14 +2249,26 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + webidl-conversions@8.0.0: + resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==} + engines: {node: '>=20'} + whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + whatwg-mimetype@4.0.0: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} + whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + when-exit@2.1.4: resolution: {integrity: sha512-4rnvd3A1t16PWzrBUcSDZqcAmsUIy4minDXT/CZ8F2mVDgd65i4Aalimgz1aQkRGU0iH5eT5+6Rx2TK8o443Pg==} @@ -1836,6 +2284,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + widest-line@5.0.0: resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} engines: {node: '>=18'} @@ -1851,6 +2304,10 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} @@ -1858,6 +2315,18 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + wsl-utils@0.1.0: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} @@ -1866,6 +2335,10 @@ packages: resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} engines: {node: '>=12'} + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + xml2js@0.6.2: resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} engines: {node: '>=4.0.0'} @@ -1874,6 +2347,9 @@ packages: resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} engines: {node: '>=4.0'} + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -1898,16 +2374,76 @@ packages: snapshots: + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@asamuzakjp/css-color@4.0.5': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 11.2.2 + + '@asamuzakjp/dom-selector@6.7.2': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.1.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.2.2 + + '@asamuzakjp/nwsapi@2.3.9': {} + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-validator-identifier@7.27.1': {} + '@babel/parser@7.28.4': + dependencies: + '@babel/types': 7.28.4 + '@babel/runtime@7.28.4': {} + '@babel/types@7.28.4': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@bcoe/v8-coverage@1.0.2': {} + + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.14(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + + '@csstools/css-tokenizer@3.0.4': {} + '@devicefarmer/adbkit-logcat@2.1.3': {} '@devicefarmer/adbkit-monkey@1.2.1': {} @@ -2094,8 +2630,31 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@mdn/browser-compat-data@7.1.7': {} '@nodelib/fs.scandir@2.1.5': @@ -2110,6 +2669,9 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 + '@pkgjs/parseargs@0.11.0': + optional: true + '@pkgr/core@0.2.9': {} '@pnpm/config.env-replace@1.1.0': {} @@ -2124,6 +2686,8 @@ snapshots: '@pnpm/network.ca-file': 1.0.2 config-chain: 1.1.13 + '@polka/url@1.0.0-next.29': {} + '@rollup/rollup-android-arm-eabi@4.52.5': optional: true @@ -2200,11 +2764,17 @@ snapshots: magic-string: 0.30.19 vite: 7.1.10(@types/node@24.8.1) + '@types/chai@5.2.2': + dependencies: + '@types/deep-eql': 4.0.2 + '@types/chrome@0.0.233': dependencies: '@types/filesystem': 0.0.36 '@types/har-format': 1.2.16 + '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.8': {} '@types/etag@1.8.4': @@ -2229,10 +2799,16 @@ snapshots: '@types/minimatch@3.0.5': {} + '@types/node@20.19.22': + dependencies: + undici-types: 6.21.0 + '@types/node@24.8.1': dependencies: undici-types: 7.14.0 + '@types/whatwg-mimetype@3.0.2': {} + '@types/yauzl@2.10.3': dependencies: '@types/node': 24.8.1 @@ -2332,6 +2908,78 @@ snapshots: '@ungap/structured-clone@1.3.0': {} + '@vitest/coverage-v8@3.2.4(vitest@3.2.4)': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 1.0.2 + ast-v8-to-istanbul: 0.3.7 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.19 + magicast: 0.3.5 + std-env: 3.10.0 + test-exclude: 7.0.1 + tinyrainbow: 2.0.0 + vitest: 3.2.4(@types/node@24.8.1)(@vitest/ui@3.2.4)(happy-dom@20.0.5)(jsdom@27.0.1(postcss@8.5.6)) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.2 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@7.1.10(@types/node@24.8.1))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.19 + optionalDependencies: + vite: 7.1.10(@types/node@24.8.1) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.19 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/ui@3.2.4(vitest@3.2.4)': + dependencies: + '@vitest/utils': 3.2.4 + fflate: 0.8.2 + flatted: 3.3.3 + pathe: 2.0.3 + sirv: 3.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 2.0.0 + vitest: 3.2.4(@types/node@24.8.1)(@vitest/ui@3.2.4)(happy-dom@20.0.5)(jsdom@27.0.1(postcss@8.5.6)) + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -2421,6 +3069,14 @@ snapshots: array-union@3.0.1: {} + assertion-error@2.0.1: {} + + ast-v8-to-istanbul@0.3.7: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 9.0.1 + async@3.2.6: {} atomic-sleep@1.0.0: {} @@ -2432,6 +3088,10 @@ snapshots: balanced-match@1.0.2: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + bluebird@3.7.2: {} boolbase@1.0.0: {} @@ -2468,10 +3128,20 @@ snapshots: dependencies: run-applescript: 7.1.0 + cac@6.7.14: {} + callsites@3.1.0: {} camelcase@8.0.0: {} + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -2479,6 +3149,8 @@ snapshots: chalk@5.6.2: {} + check-error@2.1.1: {} + cheerio-select@2.1.0: dependencies: boolbase: 1.0.0 @@ -2579,8 +3251,26 @@ snapshots: domutils: 3.2.2 nth-check: 2.1.1 + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + css-what@6.2.2: {} + cssstyle@5.3.1(postcss@8.5.6): + dependencies: + '@asamuzakjp/css-color': 4.0.5 + '@csstools/css-syntax-patches-for-csstree': 1.0.14(postcss@8.5.6) + css-tree: 3.1.0 + transitivePeerDependencies: + - postcss + + data-urls@6.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + debounce@1.2.1: {} debug@4.3.7: @@ -2593,6 +3283,10 @@ snapshots: decamelize@6.0.1: {} + decimal.js@10.6.0: {} + + deep-eql@5.0.2: {} + deep-extend@0.6.0: {} deep-is@0.1.4: {} @@ -2638,10 +3332,14 @@ snapshots: dependencies: type-fest: 4.41.0 + eastasianwidth@0.2.0: {} + emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} + encoding-sniffer@0.2.1: dependencies: iconv-lite: 0.6.3 @@ -2651,6 +3349,8 @@ snapshots: entities@6.0.1: {} + es-module-lexer@1.7.0: {} + es6-error@4.1.1: {} esbuild@0.25.11: @@ -2825,10 +3525,16 @@ snapshots: estraverse@5.3.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} etag@1.8.1: {} + expect-type@1.2.2: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -2863,6 +3569,8 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fflate@0.8.2: {} + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 @@ -2903,6 +3611,11 @@ snapshots: flatted@3.3.3: {} + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + fs-extra@11.3.2: dependencies: graceful-fs: 4.2.11 @@ -2937,6 +3650,15 @@ snapshots: glob-to-regexp@0.4.1: {} + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -2966,8 +3688,20 @@ snapshots: growly@1.3.0: {} + happy-dom@20.0.5: + dependencies: + '@types/node': 20.19.22 + '@types/whatwg-mimetype': 3.0.2 + whatwg-mimetype: 3.0.0 + has-flag@4.0.0: {} + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + html-escaper@2.0.2: {} + htmlparser2@10.0.0: dependencies: domelementtype: 2.3.0 @@ -2975,6 +3709,13 @@ snapshots: domutils: 3.2.2 entities: 6.0.1 + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 @@ -3051,6 +3792,8 @@ snapshots: is-path-inside@4.0.0: {} + is-potential-custom-element-name@1.0.1: {} + is-relative@0.1.3: {} is-utf8@0.2.1: {} @@ -3069,14 +3812,71 @@ snapshots: isexe@2.0.0: {} + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + jose@5.9.6: {} js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + js-yaml@4.1.0: dependencies: argparse: 2.0.1 + jsdom@27.0.1(postcss@8.5.6): + dependencies: + '@asamuzakjp/dom-selector': 6.7.2 + cssstyle: 5.3.1(postcss@8.5.6) + data-urls: 6.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.18.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - postcss + - supports-color + - utf-8-validate + json-buffer@3.0.1: {} json-merge-patch@1.0.2: @@ -3134,14 +3934,32 @@ snapshots: lodash.merge@4.6.2: {} + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + lru-cache@11.2.2: {} + magic-string@0.30.19: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.3.5: + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.3 + make-error@1.3.6: {} marky@1.3.0: {} + mdn-data@2.12.2: {} + merge2@1.4.1: {} micromatch@4.0.8: @@ -3159,6 +3977,10 @@ snapshots: minimist@1.2.8: {} + minipass@7.1.2: {} + + mrmime@2.0.1: {} + ms@2.1.3: {} multimatch@6.0.0: @@ -3219,6 +4041,8 @@ snapshots: dependencies: p-limit: 3.1.0 + package-json-from-dist@1.0.1: {} + package-json@10.0.1: dependencies: ky: 1.12.0 @@ -3251,12 +4075,25 @@ snapshots: dependencies: entities: 6.0.1 + parse5@8.0.0: + dependencies: + entities: 6.0.1 + path-exists@4.0.0: {} path-is-absolute@1.0.1: {} path-key@3.1.1: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + pathe@2.0.3: {} + + pathval@2.0.1: {} + pend@1.2.0: {} picocolors@1.1.1: {} @@ -3386,6 +4223,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.52.5 fsevents: 2.3.3 + rrweb-cssom@0.8.0: {} + run-applescript@7.1.0: {} run-parallel@1.2.0: @@ -3400,6 +4239,10 @@ snapshots: sax@1.4.1: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + semver@7.7.2: {} semver@7.7.3: {} @@ -3416,6 +4259,16 @@ snapshots: shellwords@0.1.1: {} + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + sonic-boom@4.2.0: dependencies: atomic-sleep: 1.0.0 @@ -3440,12 +4293,22 @@ snapshots: dependencies: through: 2.3.8 + stackback@0.0.2: {} + + std-env@3.10.0: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + string-width@7.2.0: dependencies: emoji-regex: 10.6.0 @@ -3481,16 +4344,28 @@ snapshots: strip-json-comments@5.0.3: {} + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + stubborn-fs@1.2.5: {} supports-color@7.2.0: dependencies: has-flag: 4.0.0 + symbol-tree@3.2.4: {} + synckit@0.11.11: dependencies: '@pkgr/core': 0.2.9 + test-exclude@7.0.1: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 10.4.5 + minimatch: 9.0.5 + text-table@0.2.0: {} thread-stream@3.1.0: @@ -3499,17 +4374,43 @@ snapshots: through@2.3.8: {} + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + tldts-core@7.0.17: {} + + tldts@7.0.17: + dependencies: + tldts-core: 7.0.17 + tmp@0.2.5: {} to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + totalist@3.0.1: {} + + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.17 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + ts-api-utils@2.1.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -3537,6 +4438,8 @@ snapshots: typescript@5.9.3: {} + undici-types@6.21.0: {} + undici-types@7.14.0: {} undici@7.16.0: {} @@ -3566,6 +4469,27 @@ snapshots: uuid@8.3.2: {} + vite-node@3.2.4(@types/node@24.8.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.1.10(@types/node@24.8.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite@7.1.10(@types/node@24.8.1): dependencies: esbuild: 0.25.11 @@ -3578,6 +4502,54 @@ snapshots: '@types/node': 24.8.1 fsevents: 2.3.3 + vitest@3.2.4(@types/node@24.8.1)(@vitest/ui@3.2.4)(happy-dom@20.0.5)(jsdom@27.0.1(postcss@8.5.6)): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.1.10(@types/node@24.8.1)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.2.2 + magic-string: 0.30.19 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.1.10(@types/node@24.8.1) + vite-node: 3.2.4(@types/node@24.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.8.1 + '@vitest/ui': 3.2.4(vitest@3.2.4) + happy-dom: 20.0.5 + jsdom: 27.0.1(postcss@8.5.6) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + watchpack@2.4.4: dependencies: glob-to-regexp: 0.4.1 @@ -3623,12 +4595,21 @@ snapshots: - safe-compare - supports-color + webidl-conversions@8.0.0: {} + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 + whatwg-mimetype@3.0.0: {} + whatwg-mimetype@4.0.0: {} + whatwg-url@15.1.0: + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.0 + when-exit@2.1.4: {} when@3.7.7: {} @@ -3642,6 +4623,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + widest-line@5.0.0: dependencies: string-width: 7.2.0 @@ -3656,6 +4642,12 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 @@ -3664,12 +4656,16 @@ snapshots: wrappy@1.0.2: {} + ws@8.18.3: {} + wsl-utils@0.1.0: dependencies: is-wsl: 3.1.0 xdg-basedir@5.1.0: {} + xml-name-validator@5.0.0: {} + xml2js@0.6.2: dependencies: sax: 1.4.1 @@ -3677,6 +4673,8 @@ snapshots: xmlbuilder@11.0.1: {} + xmlchars@2.2.0: {} + y18n@5.0.8: {} yargs-parser@21.1.1: {} diff --git a/src/tests/README.md b/src/tests/README.md new file mode 100644 index 0000000..1b99992 --- /dev/null +++ b/src/tests/README.md @@ -0,0 +1,48 @@ +# Tests + +This directory contains unit and integration tests for the Claude Conversation Exporter. + +## Test Structure + +- **`utils.test.ts`** - Unit tests for utility functions (model inference, conversion, filename generation, etc.) +- **`types.test.ts`** - Type definition validation tests +- **`integration.test.ts`** - Integration tests for complete workflows +- **`mockData.ts`** - Shared mock data and fixtures for tests +- **`setup.ts`** - Vitest setup and browser API mocks + +## Running Tests + +```bash +# Run tests in watch mode +pnpm test + +# Run tests once +pnpm test:run + +# Run tests with UI +pnpm test:ui + +# Run tests with coverage +pnpm test:coverage +``` + +## Coverage + +Test coverage reports are generated in the `coverage/` directory when running `pnpm test:coverage`. + +## Writing Tests + +When adding new features: + +1. Add mock data to `mockData.ts` if needed +2. Create unit tests for individual functions +3. Add integration tests for complete workflows +4. Ensure tests cover edge cases and error scenarios +5. Run `pnpm test:coverage` to check coverage + +## Test Environment + +Tests run using: +- **Vitest** as the test runner +- **happy-dom** for DOM simulation +- **Browser API mocks** for extension APIs (defined in `setup.ts`) diff --git a/src/tests/edgeCases.test.ts b/src/tests/edgeCases.test.ts new file mode 100644 index 0000000..ec9664c --- /dev/null +++ b/src/tests/edgeCases.test.ts @@ -0,0 +1,309 @@ +/** + * Edge case tests for the Claude Conversation Exporter + * Tests unusual scenarios and boundary conditions + */ + +import { describe, it, expect } from 'vitest'; +import { + inferModel, + getCurrentBranch, + convertToMarkdown, + convertToText, + generateFilename, +} from '../utils'; +import type { Conversation } from '../types'; + +describe('Edge Cases', () => { + describe('Empty and minimal data', () => { + it('should handle conversation with empty chat_messages array', () => { + const conversation: Conversation = { + uuid: 'empty-conv', + name: 'Empty Conversation', + created_at: '2024-10-01T10:00:00Z', + updated_at: '2024-10-01T10:00:00Z', + model: 'claude-3-5-sonnet-20241022', + chat_messages: [], + current_leaf_message_uuid: '', + }; + + const branch = getCurrentBranch(conversation); + expect(branch).toEqual([]); + + const markdown = convertToMarkdown(conversation, false); + expect(markdown).toContain('# Empty Conversation'); + + const text = convertToText(conversation, false); + expect(text).toBe(''); + }); + + it('should handle message with empty content array', () => { + const conversation: Conversation = { + uuid: 'empty-content', + name: 'Empty Content', + created_at: '2024-10-01T10:00:00Z', + updated_at: '2024-10-01T10:00:00Z', + model: 'claude-3-5-sonnet-20241022', + chat_messages: [ + { + uuid: 'msg-1', + sender: 'human', + content: [], + parent_message_uuid: null, + created_at: '2024-10-01T10:00:00Z', + }, + ], + current_leaf_message_uuid: 'msg-1', + }; + + const markdown = convertToMarkdown(conversation, false); + expect(markdown).toContain('**You**:'); + }); + + it('should handle message with content but no text', () => { + const conversation: Conversation = { + uuid: 'no-text', + name: 'No Text', + created_at: '2024-10-01T10:00:00Z', + updated_at: '2024-10-01T10:00:00Z', + model: 'claude-3-5-sonnet-20241022', + chat_messages: [ + { + uuid: 'msg-1', + sender: 'human', + content: [{ type: 'image' }], + parent_message_uuid: null, + created_at: '2024-10-01T10:00:00Z', + }, + ], + current_leaf_message_uuid: 'msg-1', + }; + + const markdown = convertToMarkdown(conversation, false); + expect(markdown).toBeDefined(); + }); + }); + + describe('Complex branching scenarios', () => { + it('should handle orphaned messages', () => { + const conversation: Conversation = { + uuid: 'orphaned', + name: 'Orphaned Messages', + created_at: '2024-10-01T10:00:00Z', + updated_at: '2024-10-01T10:00:00Z', + model: 'claude-3-5-sonnet-20241022', + chat_messages: [ + { + uuid: 'root', + sender: 'human', + content: [{ text: 'Root' }], + parent_message_uuid: null, + created_at: '2024-10-01T10:00:00Z', + }, + { + uuid: 'orphan', + sender: 'assistant', + content: [{ text: 'Orphaned message' }], + parent_message_uuid: 'nonexistent', + created_at: '2024-10-01T10:01:00Z', + }, + ], + current_leaf_message_uuid: 'orphan', + }; + + const branch = getCurrentBranch(conversation); + // Should only contain the orphaned message since its parent doesn't exist + expect(branch.length).toBe(1); + expect(branch[0]?.uuid).toBe('orphan'); + }); + }); + + describe('Filename edge cases', () => { + it('should handle very long conversation names', () => { + const longName = 'a'.repeat(1000); + const filename = generateFilename(longName, 'json', 'id-123'); + + // Should be truncated + expect(filename.length).toBeLessThan(100); + expect(filename).toMatch(/\.json$/); + }); + + it('should handle conversation name with only special characters', () => { + const specialName = '!@#$%^&*()[]{}'; + const filename = generateFilename(specialName, 'json', 'id-123'); + + expect(filename).toMatch(/^[a-z0-9_]+_id-123\.json$/); + }); + + it('should handle empty conversation name', () => { + const filename = generateFilename('', 'json', 'id-123'); + + expect(filename).toBe('_id-123.json'); + }); + + it('should handle conversation name with unicode', () => { + const unicodeName = '你好世界 🌍 مرحبا'; + const filename = generateFilename(unicodeName, 'json', 'id-123'); + + // Should only contain allowed characters + expect(filename).toMatch(/^[a-z0-9_]+_id-123\.json$/); + }); + + it('should handle undefined conversation ID', () => { + const filename = generateFilename('Test', 'json', undefined); + + expect(filename).toBe('test.json'); + }); + }); + + describe('Model inference edge cases', () => { + it('should handle very old dates', () => { + const conversation: Conversation = { + uuid: 'old', + name: 'Old', + created_at: '2020-01-01T10:00:00Z', + updated_at: '2020-01-01T10:00:00Z', + model: null, + chat_messages: [], + current_leaf_message_uuid: '', + }; + + const model = inferModel(conversation); + expect(model).toBe('claude-3-sonnet-20240229'); + }); + + it('should handle future dates far in the future', () => { + const conversation: Conversation = { + uuid: 'future', + name: 'Future', + created_at: '2100-01-01T10:00:00Z', + updated_at: '2100-01-01T10:00:00Z', + model: null, + chat_messages: [], + current_leaf_message_uuid: '', + }; + + const model = inferModel(conversation); + // Should return the latest model + expect(model).toBe('claude-sonnet-4-5-20250929'); + }); + + it('should handle invalid date strings gracefully', () => { + const conversation: Conversation = { + uuid: 'invalid', + name: 'Invalid', + created_at: 'not-a-date', + updated_at: 'not-a-date', + model: null, + chat_messages: [], + current_leaf_message_uuid: '', + }; + + // Should not crash + const model = inferModel(conversation); + expect(model).toBeDefined(); + }); + + it('should handle dates exactly on model transition boundaries', () => { + const testCases = [ + { date: '2024-06-20T00:00:00.000Z', expected: 'claude-3-5-sonnet-20240620' }, + { date: '2024-10-22T00:00:00.000Z', expected: 'claude-3-5-sonnet-20241022' }, + { date: '2025-02-29T00:00:00.000Z', expected: 'claude-3-7-sonnet-20250219' }, + ]; + + testCases.forEach(({ date, expected }) => { + const conversation: Conversation = { + uuid: 'boundary', + name: 'Boundary', + created_at: date, + updated_at: date, + model: null, + chat_messages: [], + current_leaf_message_uuid: '', + }; + + const model = inferModel(conversation); + expect(model).toBe(expected); + }); + }); + }); + + describe('Content conversion edge cases', () => { + it('should handle messages with mixed old and new format', () => { + const conversation: Conversation = { + uuid: 'mixed', + name: 'Mixed Format', + created_at: '2024-10-01T10:00:00Z', + updated_at: '2024-10-01T10:00:00Z', + model: 'claude-3-5-sonnet-20241022', + chat_messages: [ + { + uuid: 'msg-1', + sender: 'human', + text: 'Old format', + parent_message_uuid: null, + created_at: '2024-10-01T10:00:00Z', + }, + { + uuid: 'msg-2', + sender: 'assistant', + content: [{ text: 'New format' }], + parent_message_uuid: 'msg-1', + created_at: '2024-10-01T10:01:00Z', + }, + ], + current_leaf_message_uuid: 'msg-2', + }; + + const markdown = convertToMarkdown(conversation, false); + expect(markdown).toContain('Old format'); + expect(markdown).toContain('New format'); + }); + + it('should handle messages without created_at timestamp', () => { + const conversation: Conversation = { + uuid: 'no-timestamp', + name: 'No Timestamp', + created_at: '2024-10-01T10:00:00Z', + updated_at: '2024-10-01T10:00:00Z', + model: 'claude-3-5-sonnet-20241022', + chat_messages: [ + { + uuid: 'msg-1', + sender: 'human', + content: [{ text: 'Message' }], + parent_message_uuid: null, + created_at: '', + }, + ], + current_leaf_message_uuid: 'msg-1', + }; + + const markdown = convertToMarkdown(conversation, true); + expect(markdown).toBeDefined(); + }); + + it('should handle very long message content', () => { + const longText = 'a'.repeat(10000); // Reduced from 100000 for faster tests + const conversation: Conversation = { + uuid: 'long', + name: 'Long', + created_at: '2024-10-01T10:00:00Z', + updated_at: '2024-10-01T10:00:00Z', + model: 'claude-3-5-sonnet-20241022', + chat_messages: [ + { + uuid: 'msg-1', + sender: 'human', + content: [{ text: longText }], + parent_message_uuid: null, + created_at: '2024-10-01T10:00:00Z', + }, + ], + current_leaf_message_uuid: 'msg-1', + }; + + const markdown = convertToMarkdown(conversation, false); + expect(markdown.length).toBeGreaterThan(longText.length); + }); + }); +}); diff --git a/src/tests/integration.test.ts b/src/tests/integration.test.ts new file mode 100644 index 0000000..356046c --- /dev/null +++ b/src/tests/integration.test.ts @@ -0,0 +1,153 @@ +/** + * Integration tests for content script functionality + */ + +import { describe, it, expect } from 'vitest'; +import { convertToMarkdown, convertToText, generateFilename, inferModel } from '../utils'; +import { + mockConversation, + mockConversationWithNullModel, + mockConversationWithBranches, +} from './mockData'; + +describe('Content Script Integration', () => { + describe('Full export workflow', () => { + it('should process a conversation through full export pipeline', () => { + // Infer model + const modelName = inferModel(mockConversation); + expect(modelName).toBe('claude-3-5-sonnet-20241022'); + + // Convert to different formats + const markdown = convertToMarkdown(mockConversation, true); + const text = convertToText(mockConversation, true); + + expect(markdown).toContain('# Test Conversation'); + expect(text).toContain('Test Conversation'); + + // Generate filenames + const mdFilename = generateFilename( + mockConversation.name, + 'markdown', + mockConversation.uuid + ); + const txtFilename = generateFilename( + mockConversation.name, + 'text', + mockConversation.uuid + ); + + expect(mdFilename).toBe('test_conversation_conv-123.md'); + expect(txtFilename).toBe('test_conversation_conv-123.txt'); + }); + + it('should handle conversations with null model', () => { + // Infer model when null + const modelName = inferModel(mockConversationWithNullModel); + expect(modelName).toBe('claude-3-5-sonnet-20240620'); + + // Update conversation with inferred model + const updatedConversation = { + ...mockConversationWithNullModel, + model: modelName, + }; + + // Convert to markdown + const markdown = convertToMarkdown(updatedConversation, true); + expect(markdown).toContain('**Model:** claude-3-5-sonnet-20240620'); + }); + + it('should correctly handle branched conversations', () => { + // Get the current branch + const markdown = convertToMarkdown(mockConversationWithBranches, true); + + // Should only include messages from current branch + expect(markdown).toContain('Root message'); + expect(markdown).toContain('Branch A response'); + expect(markdown).toContain('Continuing branch A'); + + // Should not include branch B + expect(markdown).not.toContain('Branch B response'); + }); + }); + + describe('Format conversion consistency', () => { + it('should include the same messages in all formats', () => { + const markdown = convertToMarkdown(mockConversation, false); + const text = convertToText(mockConversation, false); + + // Check that key content appears in both + expect(markdown).toContain('Hello, Claude!'); + expect(text).toContain('Hello, Claude!'); + + expect(markdown).toContain('Hello! How can I help you today?'); + expect(text).toContain('Hello! How can I help you today?'); + + expect(markdown).toContain('Can you explain TypeScript?'); + expect(text).toContain('Can you explain TypeScript?'); + }); + + it('should handle metadata consistently across formats', () => { + const markdownWithMeta = convertToMarkdown(mockConversation, true); + const markdownWithoutMeta = convertToMarkdown(mockConversation, false); + const textWithMeta = convertToText(mockConversation, true); + const textWithoutMeta = convertToText(mockConversation, false); + + // With metadata + expect(markdownWithMeta).toContain('**Model:**'); + expect(textWithMeta).toContain('Model:'); + + // Without metadata + expect(markdownWithoutMeta).not.toContain('**Model:**'); + expect(textWithoutMeta).not.toContain('Model:'); + }); + }); + + describe('Filename generation', () => { + it('should generate unique filenames for different formats', () => { + const jsonFile = generateFilename('Test', 'json', 'id-123'); + const mdFile = generateFilename('Test', 'markdown', 'id-123'); + const txtFile = generateFilename('Test', 'text', 'id-123'); + + expect(jsonFile).toBe('test_id-123.json'); + expect(mdFile).toBe('test_id-123.md'); + expect(txtFile).toBe('test_id-123.txt'); + + // Filenames should be different + const filenames = new Set([jsonFile, mdFile, txtFile]); + expect(filenames.size).toBe(3); + }); + + it('should handle edge cases in conversation names', () => { + const emptyName = generateFilename('', 'json', 'id-123'); + const specialChars = generateFilename('Test!@#$', 'json', 'id-123'); + const unicode = generateFilename('Test 测试', 'json', 'id-123'); + + expect(emptyName).toMatch(/^.*_id-123\.json$/); + expect(specialChars).toMatch(/^[a-z0-9_]+_id-123\.json$/); + expect(unicode).toMatch(/^[a-z0-9_]+_id-123\.json$/); + }); + }); + + describe('Model inference edge cases', () => { + it('should handle various date ranges correctly', () => { + const testCases = [ + { date: '2023-12-31T23:59:59Z', expected: 'claude-3-sonnet-20240229' }, + { date: '2024-01-01T00:00:00Z', expected: 'claude-3-sonnet-20240229' }, + { date: '2024-06-20T00:00:00Z', expected: 'claude-3-5-sonnet-20240620' }, + { date: '2024-10-22T00:00:00Z', expected: 'claude-3-5-sonnet-20241022' }, + { date: '2025-02-29T00:00:00Z', expected: 'claude-3-7-sonnet-20250219' }, + { date: '2030-01-01T00:00:00Z', expected: 'claude-sonnet-4-5-20250929' }, + ]; + + testCases.forEach(({ date, expected }) => { + const conversation = { + ...mockConversationWithNullModel, + created_at: date, + model: null, + }; + const result = inferModel(conversation); + expect(result).toBe(expected); + }); + }); + }); +}); diff --git a/src/tests/mockData.ts b/src/tests/mockData.ts new file mode 100644 index 0000000..5892bf0 --- /dev/null +++ b/src/tests/mockData.ts @@ -0,0 +1,142 @@ +/** + * Mock data for testing + */ + +import type { Conversation, ChatMessage, ConversationListItem } from '../types'; + +export const mockChatMessages: ChatMessage[] = [ + { + uuid: 'msg-1', + sender: 'human', + content: [{ text: 'Hello, Claude!' }], + parent_message_uuid: null, + created_at: '2024-10-01T10:00:00Z', + }, + { + uuid: 'msg-2', + sender: 'assistant', + content: [{ text: 'Hello! How can I help you today?' }], + parent_message_uuid: 'msg-1', + created_at: '2024-10-01T10:00:05Z', + }, + { + uuid: 'msg-3', + sender: 'human', + content: [{ text: 'Can you explain TypeScript?' }], + parent_message_uuid: 'msg-2', + created_at: '2024-10-01T10:01:00Z', + }, + { + uuid: 'msg-4', + sender: 'assistant', + content: [ + { + text: 'TypeScript is a strongly typed programming language that builds on JavaScript.', + }, + ], + parent_message_uuid: 'msg-3', + created_at: '2024-10-01T10:01:10Z', + }, +]; + +export const mockConversation: Conversation = { + uuid: 'conv-123', + name: 'Test Conversation', + created_at: '2024-10-01T10:00:00Z', + updated_at: '2024-10-01T10:01:10Z', + model: 'claude-3-5-sonnet-20241022', + chat_messages: mockChatMessages, + current_leaf_message_uuid: 'msg-4', +}; + +export const mockConversationWithNullModel: Conversation = { + uuid: 'conv-456', + name: 'Old Conversation', + created_at: '2024-08-15T10:00:00Z', + updated_at: '2024-08-15T10:01:10Z', + model: null, + chat_messages: mockChatMessages, + current_leaf_message_uuid: 'msg-4', +}; + +export const mockConversationOldFormat: Conversation = { + uuid: 'conv-789', + name: 'Legacy Conversation', + created_at: '2024-01-15T10:00:00Z', + updated_at: '2024-01-15T10:01:10Z', + model: null, + chat_messages: [ + { + uuid: 'msg-old-1', + sender: 'human', + text: 'Hello from old format!', + parent_message_uuid: null, + created_at: '2024-01-15T10:00:00Z', + }, + { + uuid: 'msg-old-2', + sender: 'assistant', + text: 'Hi there! This is the old message format.', + parent_message_uuid: 'msg-old-1', + created_at: '2024-01-15T10:00:05Z', + }, + ], + current_leaf_message_uuid: 'msg-old-2', +}; + +export const mockConversationWithBranches: Conversation = { + uuid: 'conv-branched', + name: 'Branched Conversation', + created_at: '2024-10-01T10:00:00Z', + updated_at: '2024-10-01T10:05:00Z', + model: 'claude-3-5-sonnet-20241022', + chat_messages: [ + { + uuid: 'root', + sender: 'human', + content: [{ text: 'Root message' }], + parent_message_uuid: null, + created_at: '2024-10-01T10:00:00Z', + }, + { + uuid: 'branch-a', + sender: 'assistant', + content: [{ text: 'Branch A response' }], + parent_message_uuid: 'root', + created_at: '2024-10-01T10:01:00Z', + }, + { + uuid: 'branch-b', + sender: 'assistant', + content: [{ text: 'Branch B response' }], + parent_message_uuid: 'root', + created_at: '2024-10-01T10:02:00Z', + }, + { + uuid: 'branch-a-child', + sender: 'human', + content: [{ text: 'Continuing branch A' }], + parent_message_uuid: 'branch-a', + created_at: '2024-10-01T10:03:00Z', + }, + ], + current_leaf_message_uuid: 'branch-a-child', +}; + +export const mockConversationListItem: ConversationListItem = { + uuid: 'conv-list-1', + name: 'List Item Conversation', + created_at: '2024-10-01T10:00:00Z', + updated_at: '2024-10-01T10:01:10Z', + model: 'claude-3-5-sonnet-20241022', + is_starred: true, +}; + +export const mockConversationListItemWithNullModel: ConversationListItem = { + uuid: 'conv-list-2', + name: 'Old List Item', + created_at: '2024-03-01T10:00:00Z', + updated_at: '2024-03-01T10:01:10Z', + model: null, + is_starred: false, +}; diff --git a/src/tests/setup.ts b/src/tests/setup.ts new file mode 100644 index 0000000..c13a21a --- /dev/null +++ b/src/tests/setup.ts @@ -0,0 +1,62 @@ +/** + * Vitest setup file + * Global test configuration and browser API mocks + */ + +import { vi } from 'vitest'; + +// Mock browser/chrome API +const mockBrowserAPI = { + runtime: { + sendMessage: vi.fn(), + onMessage: { + addListener: vi.fn(), + removeListener: vi.fn(), + }, + getURL: vi.fn((path: string) => `chrome-extension://mock-id/${path}`), + }, + storage: { + local: { + get: vi.fn(), + set: vi.fn(), + }, + sync: { + get: vi.fn(), + set: vi.fn(), + }, + }, + tabs: { + query: vi.fn(), + sendMessage: vi.fn(), + executeScript: vi.fn(), + }, +}; + +// Set up global browser API mocks +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access +(global as any).browser = mockBrowserAPI; +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access +(global as any).chrome = mockBrowserAPI; + +// Mock Blob and URL for download tests +if (typeof Blob === 'undefined') { + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + (global as any).Blob = class MockBlob { + constructor( + public parts: unknown[], + public options?: unknown + ) {} + }; +} + +// Mock DOM methods +if (typeof document !== 'undefined') { + const originalCreateElement = document.createElement.bind(document); + document.createElement = function (tagName: string) { + const element = originalCreateElement(tagName); + if (tagName.toLowerCase() === 'a') { + element.click = vi.fn(); + } + return element; + }; +} diff --git a/src/tests/types.test.ts b/src/tests/types.test.ts new file mode 100644 index 0000000..0e465d4 --- /dev/null +++ b/src/tests/types.test.ts @@ -0,0 +1,157 @@ +/** + * Unit tests for types.ts + * Tests type guards and type validation + */ + +import { describe, it, expect } from 'vitest'; +import type { + Conversation, + ChatMessage, + ConversationListItem, + ExportFormat, + ExtensionMessage, + MessageContent, +} from '../types'; + +describe('Type definitions', () => { + it('should accept valid ChatMessage', () => { + const message: ChatMessage = { + uuid: 'test-uuid', + sender: 'human', + content: [{ text: 'Hello' }], + parent_message_uuid: null, + created_at: '2024-10-01T10:00:00Z', + }; + + expect(message.uuid).toBe('test-uuid'); + expect(message.sender).toBe('human'); + }); + + it('should accept valid ChatMessage with text field', () => { + const message: ChatMessage = { + uuid: 'test-uuid', + sender: 'assistant', + text: 'Hello from old format', + parent_message_uuid: 'parent-id', + created_at: '2024-10-01T10:00:00Z', + }; + + expect(message.text).toBe('Hello from old format'); + }); + + it('should accept valid MessageContent', () => { + const content: MessageContent = { + text: 'Test message', + type: 'text', + }; + + expect(content.text).toBe('Test message'); + expect(content.type).toBe('text'); + }); + + it('should accept valid Conversation', () => { + const conversation: Conversation = { + uuid: 'conv-123', + name: 'Test', + created_at: '2024-10-01T10:00:00Z', + updated_at: '2024-10-01T10:01:00Z', + model: 'claude-3-5-sonnet-20241022', + chat_messages: [], + current_leaf_message_uuid: 'leaf-id', + }; + + expect(conversation.uuid).toBe('conv-123'); + expect(conversation.model).toBe('claude-3-5-sonnet-20241022'); + }); + + it('should accept Conversation with null model', () => { + const conversation: Conversation = { + uuid: 'conv-123', + name: 'Test', + created_at: '2024-10-01T10:00:00Z', + updated_at: '2024-10-01T10:01:00Z', + model: null, + chat_messages: [], + current_leaf_message_uuid: 'leaf-id', + }; + + expect(conversation.model).toBeNull(); + }); + + it('should accept valid ConversationListItem', () => { + const item: ConversationListItem = { + uuid: 'conv-list-1', + name: 'Test Item', + created_at: '2024-10-01T10:00:00Z', + updated_at: '2024-10-01T10:01:00Z', + model: 'claude-3-5-sonnet-20241022', + is_starred: true, + }; + + expect(item.uuid).toBe('conv-list-1'); + expect(item.is_starred).toBe(true); + }); + + it('should accept valid ExportFormat values', () => { + const json: ExportFormat = 'json'; + const markdown: ExportFormat = 'markdown'; + const text: ExportFormat = 'text'; + + expect(json).toBe('json'); + expect(markdown).toBe('markdown'); + expect(text).toBe('text'); + }); + + it('should accept valid exportConversation message', () => { + const message: ExtensionMessage = { + action: 'exportConversation', + conversationId: 'conv-123', + orgId: 'org-456', + format: 'json', + includeMetadata: true, + }; + + expect(message.action).toBe('exportConversation'); + expect(message.format).toBe('json'); + }); + + it('should accept valid exportAllConversations message', () => { + const message: ExtensionMessage = { + action: 'exportAllConversations', + orgId: 'org-456', + format: 'markdown', + includeMetadata: false, + }; + + expect(message.action).toBe('exportAllConversations'); + expect(message.format).toBe('markdown'); + }); + + it('should accept valid ensureContentScript message', () => { + const message: ExtensionMessage = { + action: 'ensureContentScript', + }; + + expect(message.action).toBe('ensureContentScript'); + }); + + it('should accept valid fetchConversations message', () => { + const message: ExtensionMessage = { + action: 'fetchConversations', + orgId: 'org-456', + }; + + expect(message.action).toBe('fetchConversations'); + }); + + it('should accept valid fetchConversation message', () => { + const message: ExtensionMessage = { + action: 'fetchConversation', + orgId: 'org-456', + conversationId: 'conv-123', + }; + + expect(message.action).toBe('fetchConversation'); + expect(message.conversationId).toBe('conv-123'); + }); +}); diff --git a/src/tests/utils.test.ts b/src/tests/utils.test.ts new file mode 100644 index 0000000..dcc1117 --- /dev/null +++ b/src/tests/utils.test.ts @@ -0,0 +1,295 @@ +/** + * Unit tests for utils.ts + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { + inferModel, + getCurrentBranch, + convertToMarkdown, + convertToText, + generateFilename, + downloadFile, +} from '../utils'; +import { + mockConversation, + mockConversationWithNullModel, + mockConversationOldFormat, + mockConversationWithBranches, + mockConversationListItemWithNullModel, +} from './mockData'; + +describe('inferModel', () => { + it('should return the model if it is not null', () => { + const result = inferModel(mockConversation); + expect(result).toBe('claude-3-5-sonnet-20241022'); + }); + + it('should infer claude-3-5-sonnet-20240620 for dates in June-October 2024', () => { + const result = inferModel(mockConversationWithNullModel); + expect(result).toBe('claude-3-5-sonnet-20240620'); + }); + + it('should infer claude-3-sonnet-20240229 for dates in early 2024', () => { + const result = inferModel(mockConversationOldFormat); + expect(result).toBe('claude-3-sonnet-20240229'); + }); + + it('should infer claude-3-sonnet-20240229 for dates before 2024', () => { + const conversation = { + ...mockConversationWithNullModel, + created_at: '2023-12-01T10:00:00Z', + model: null, + }; + const result = inferModel(conversation); + expect(result).toBe('claude-3-sonnet-20240229'); + }); + + it('should work with ConversationListItem', () => { + const result = inferModel(mockConversationListItemWithNullModel); + expect(result).toBe('claude-3-sonnet-20240229'); + }); + + it('should infer newest model for future dates', () => { + const conversation = { + ...mockConversationWithNullModel, + created_at: '2026-01-01T10:00:00Z', + model: null, + }; + const result = inferModel(conversation); + expect(result).toBe('claude-sonnet-4-5-20250929'); + }); +}); + +describe('getCurrentBranch', () => { + it('should return all messages in the current branch', () => { + const result = getCurrentBranch(mockConversation); + expect(result).toHaveLength(4); + expect(result[0]?.uuid).toBe('msg-1'); + expect(result[1]?.uuid).toBe('msg-2'); + expect(result[2]?.uuid).toBe('msg-3'); + expect(result[3]?.uuid).toBe('msg-4'); + }); + + it('should return only the current branch when there are multiple branches', () => { + const result = getCurrentBranch(mockConversationWithBranches); + expect(result).toHaveLength(3); + expect(result[0]?.uuid).toBe('root'); + expect(result[1]?.uuid).toBe('branch-a'); + expect(result[2]?.uuid).toBe('branch-a-child'); + }); + + it('should return empty array when chat_messages is undefined', () => { + const conversation = { + ...mockConversation, + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment + chat_messages: undefined as any, + }; + const result = getCurrentBranch(conversation); + expect(result).toEqual([]); + }); + + it('should return empty array when current_leaf_message_uuid is undefined', () => { + const conversation = { + ...mockConversation, + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment + current_leaf_message_uuid: undefined as any, + }; + const result = getCurrentBranch(conversation); + expect(result).toEqual([]); + }); + + it('should handle messages with old text format', () => { + const result = getCurrentBranch(mockConversationOldFormat); + expect(result).toHaveLength(2); + expect(result[0]?.uuid).toBe('msg-old-1'); + expect(result[1]?.uuid).toBe('msg-old-2'); + }); +}); + +describe('convertToMarkdown', () => { + it('should convert conversation to markdown with metadata', () => { + const result = convertToMarkdown(mockConversation, true); + + expect(result).toContain('# Test Conversation'); + expect(result).toContain('**Created:**'); + expect(result).toContain('**Updated:**'); + expect(result).toContain('**Model:** claude-3-5-sonnet-20241022'); + expect(result).toContain('**You**:'); + expect(result).toContain('**Claude**:'); + expect(result).toContain('Hello, Claude!'); + expect(result).toContain('Hello! How can I help you today?'); + }); + + it('should convert conversation to markdown without metadata', () => { + const result = convertToMarkdown(mockConversation, false); + + expect(result).toContain('# Test Conversation'); + expect(result).not.toContain('**Created:**'); + expect(result).not.toContain('**Updated:**'); + expect(result).not.toContain('**Model:**'); + expect(result).toContain('**You**:'); + expect(result).toContain('**Claude**:'); + }); + + it('should handle conversation with old text format', () => { + const result = convertToMarkdown(mockConversationOldFormat, false); + + expect(result).toContain('Hello from old format!'); + expect(result).toContain('Hi there! This is the old message format.'); + }); + + it('should infer model when null', () => { + const result = convertToMarkdown(mockConversationWithNullModel, true); + + expect(result).toContain('**Model:** claude-3-5-sonnet-20240620'); + }); + + it('should handle untitled conversations', () => { + const conversation = { + ...mockConversation, + name: '', + }; + const result = convertToMarkdown(conversation, false); + + expect(result).toContain('# Untitled Conversation'); + }); +}); + +describe('convertToText', () => { + it('should convert conversation to text with metadata', () => { + const result = convertToText(mockConversation, true); + + expect(result).toContain('Test Conversation'); + expect(result).toContain('Created:'); + expect(result).toContain('Updated:'); + expect(result).toContain('Model: claude-3-5-sonnet-20241022'); + expect(result).toContain('Human: Hello, Claude!'); + expect(result).toContain('Assistant: Hello! How can I help you today?'); + }); + + it('should convert conversation to text without metadata', () => { + const result = convertToText(mockConversation, false); + + expect(result).not.toContain('Created:'); + expect(result).not.toContain('Updated:'); + expect(result).not.toContain('Model:'); + expect(result).toContain('Human: Hello, Claude!'); + expect(result).toContain('Assistant: Hello! How can I help you today?'); + }); + + it('should abbreviate sender labels after first occurrence', () => { + const result = convertToText(mockConversation, false); + + expect(result).toContain('Human: Hello, Claude!'); + expect(result).toContain('Assistant: Hello! How can I help you today?'); + expect(result).toContain('H: Can you explain TypeScript?'); + expect(result).toContain('A: TypeScript is a strongly typed programming language'); + }); + + it('should handle conversation with old text format', () => { + const result = convertToText(mockConversationOldFormat, false); + + expect(result).toContain('Hello from old format!'); + expect(result).toContain('Hi there! This is the old message format.'); + }); + + it('should handle untitled conversations', () => { + const conversation = { + ...mockConversation, + name: '', + }; + const result = convertToText(conversation, true); + + expect(result).toContain('Untitled Conversation'); + }); +}); + +describe('generateFilename', () => { + it('should generate filename for JSON format', () => { + const result = generateFilename('Test Conversation', 'json', 'conv-123'); + expect(result).toBe('test_conversation_conv-123.json'); + }); + + it('should generate filename for markdown format', () => { + const result = generateFilename('My Test', 'markdown', 'abc-456'); + expect(result).toBe('my_test_abc-456.md'); + }); + + it('should generate filename for text format', () => { + const result = generateFilename('Hello World', 'text', 'xyz-789'); + expect(result).toBe('hello_world_xyz-789.txt'); + }); + + it('should sanitize special characters', () => { + const result = generateFilename('Test!@#$%^&*()_+[]{}', 'json', 'id-123'); + expect(result).toBe('test_________________id-123.json'); + }); + + it('should truncate long names to 50 characters', () => { + const longName = 'a'.repeat(100); + const result = generateFilename(longName, 'json', 'id-123'); + expect(result.length).toBeLessThanOrEqual(62); // 50 + '_' + 8 char id + '.json' + }); + + it('should work without conversation ID', () => { + const result = generateFilename('Test', 'json'); + expect(result).toBe('test.json'); + }); + + it('should convert to lowercase', () => { + const result = generateFilename('UPPERCASE TEST', 'json', 'id-123'); + expect(result).toBe('uppercase_test_id-123.json'); + }); +}); + +describe('downloadFile', () => { + let mockCreateObjectURL: ReturnType; + let mockRevokeObjectURL: ReturnType; + + beforeEach(() => { + // Set up DOM mocks + document.body.innerHTML = ''; + + // Mock URL.createObjectURL and URL.revokeObjectURL + mockCreateObjectURL = vi.fn(() => 'blob:mock-url'); + mockRevokeObjectURL = vi.fn(); + global.URL.createObjectURL = mockCreateObjectURL; + global.URL.revokeObjectURL = mockRevokeObjectURL; + }); + + it('should create and download a file with default type', () => { + const content = '{"test": "data"}'; + const filename = 'test.json'; + + downloadFile(content, filename); + + expect(mockCreateObjectURL).toHaveBeenCalled(); + expect(mockRevokeObjectURL).toHaveBeenCalledWith('blob:mock-url'); + }); + + it('should create and download a file with custom type', () => { + const content = '# Test'; + const filename = 'test.md'; + const type = 'text/markdown'; + + downloadFile(content, filename, type); + + expect(mockCreateObjectURL).toHaveBeenCalled(); + expect(mockRevokeObjectURL).toHaveBeenCalledWith('blob:mock-url'); + }); + + it('should create and trigger anchor element click', () => { + const content = 'test content'; + const filename = 'test.txt'; + + // Spy on appendChild and removeChild + const appendChildSpy = vi.spyOn(document.body, 'appendChild'); + const removeChildSpy = vi.spyOn(document.body, 'removeChild'); + + downloadFile(content, filename, 'text/plain'); + + expect(appendChildSpy).toHaveBeenCalled(); + expect(removeChildSpy).toHaveBeenCalled(); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 5cea201..7ae7fbe 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -42,10 +42,11 @@ }, /* Browser Extension Types */ - "types": ["firefox-webext-browser"] + "types": ["firefox-webext-browser", "vitest/globals"] }, "include": [ - "src/**/*.ts" + "src/**/*.ts", + "vite.config.ts" ], "exclude": [ "node_modules", diff --git a/vite.config.ts b/vite.config.ts index a781b93..f132190 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -40,5 +40,23 @@ export default defineConfig(({ mode }) => { }, }, }, + test: { + globals: true, + environment: 'happy-dom', + setupFiles: ['./src/tests/setup.ts'], + include: ['src/tests/**/*.test.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + exclude: [ + 'node_modules/', + 'dist/', + 'src/tests/', + '**/*.d.ts', + '**/*.config.*', + '**/mockData', + ], + }, + }, }; });