yuwen
2025-11-12 3f575f22e2943845bb9b617e2592abba39c909b0
feat: integrate MSW for API mocking and testing

- Added MSW (Mock Service Worker) for intercepting network requests in development and testing.
- Created mock data and handlers for example API responses.
- Implemented a new ExamplePage component with a button to trigger API requests.
- Updated router to include a route for the ExamplePage.
- Added tests for ExamplePage using Testing Library and Vitest, ensuring proper rendering and API interaction.
- Configured testing setup to clean up after tests and start the MSW server.
- Updated package.json to include necessary dependencies for testing and mocking.
修改8個檔案
新增13個檔案
3490 ■■■■■ 已變更過的檔案
.github/instructions/testing.instructions.md 363 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
docs/testing-guide.md 451 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
e2e/features/example/user-flow.spec.ts 79 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
package-lock.json 1898 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
package.json 10 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
public/mockServiceWorker.js 349 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
setupTests.ts 21 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
src/App.vue 2 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
src/__tests__/App.spec.ts 8 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
src/features/example/api.ts 12 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
src/features/example/index.spec.ts 176 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
src/features/example/index.vue 55 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
src/main.ts 6 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
src/mocks/browser.ts 4 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
src/mocks/data/example.ts 10 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
src/mocks/data/index.ts 7 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
src/mocks/handlers.ts 21 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
src/mocks/node.ts 4 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
src/router/index.ts 11 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
tsconfig.app.json 2 ●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
vitest.config.ts 1 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
.github/instructions/testing.instructions.md
比對新檔案
@@ -0,0 +1,363 @@
---
applyTo: '**/*.{test,spec}.{ts,js,vue}'
---
# Testing Best Practices Instructions
## Reference Documentation
For detailed testing guidelines and background information, refer to: [Testing Guide](../../### Mock Data Guidelines
### Realistic Test Data
Use realistic, domain-specific data that mirrors production scenarios:
```typescript
// ✅ Good - Realistic data
const customerData = {
  name: '陳小明',
  email: 'chen.xiaoming@example.com',
  phone: '0912-345-678',
  birthDate: '1985/03/15', // ROC format
  policyNumber: 'INS-2024-000123',
}
// ❌ Bad - Generic test data
const customerData = {
  name: 'foo',
  email: 'test@test.com',
  phone: '123',
  birthDate: '2000/01/01',
}
```
### Avoid Test Data Anti-Patterns
- Don't use overly simple test strings like 'test', 'foo', 'bar'
- Use domain-relevant data that mirrors production scenarios
- Include edge cases specific to your business domain
- Consider using property-based testing for comprehensive input combinations
- Generate varied test data to catch edge cases production bugs often surface with unusual inputsuide.md)
  For detailed Testing with MSW guidelines and examples, refer to: [Testing with MSW](../../docs/msw-testing-guide.md)
The following instructions provide focused guidance for writing tests in this codebase.
## Testing Framework & Architecture
Use **Vitest + Vue Testing Library + Pinia Testing** for comprehensive Vue 3 + TypeScript component testing.
### Testing Stack
- **Vitest**: Primary testing framework with MSW mocking
- **@testing-library/vue**: Component rendering and interaction testing
- **@pinia/testing**: Store state management testing with `createTestingPinia`
- **MSW (Mock Service Worker)**: API response mocking
- **@testing-library/jest-dom**: Enhanced DOM assertions
### Testing Philosophy & Golden Rule
**Design for lean testing**: Tests should be extremely simple, short, flat, and delightful to work with. A test should be readable at first glance and require minimal mental effort to understand its purpose. Avoid complex test logic that competes with your main application code for mental bandwidth.
## Core Testing Principles
### Test Naming Convention
Each test name should include three parts:
1. **What is being tested** (e.g., `ProductService.addNewProduct`)
2. **Under what scenario** (e.g., "when price is not provided")
3. **Expected result** (e.g., "product is not approved")
```typescript
// ✅ Good - Descriptive three-part name
it('should reject product when price is missing and return validation error', async () => {
  // Test implementation
})
// ❌ Bad - Vague name
it('should add product', async () => {
  // Test implementation
})
```
### 3A Pattern (Arrange-Act-Assert)
Always structure tests with clear separation:
```typescript
it('should close popup when confirm button clicked', async () => {
  // Arrange - Setup test environment
  const testStore = createTestingPinia({ createSpy: vi.fn })
  render(Component, { global: { plugins: [testStore] } })
  // Act - Execute the behavior being tested
  const button = await screen.findByText('確認')
  await userEvent.click(button)
  // Assert - Verify expected results
  await waitFor(() => {
    expect(screen.queryByText('測試彈窗')).not.toBeInTheDocument()
  })
})
```
### Testing Philosophy
Key principles:
- **Test user behavior, not implementation details**
- **One behavior per test case**
- **Use realistic test data that mirrors actual business scenarios**
- **Maintain test isolation to prevent state pollution**
- **Use declarative assertions with BDD-style language** (prefer `expect().toBe()` over complex conditional logic)
## Pinia Store Testing Patterns
### Store State Isolation Strategy
Choose between two approaches based on test complexity:
**Option 1: Independent Store Instances (Recommended for <10 tests)**
```typescript
it('should work correctly', async () => {
  const testStore = createTestingPinia({ createSpy: vi.fn })
  render(Component, {
    global: { plugins: [testStore] },
  })
  const store = useMyStore() // Get store AFTER render
})
```
**Option 2: Shared Store with Cleanup (For larger test suites)**
```typescript
const mockedStore = createTestingPinia({ createSpy: vi.fn })
beforeEach(() => {
  // Reset store state
  mockedStore.state.value = {}
})
```
### Critical Timing Rules
- **ALWAYS get store instances AFTER `render`**
- **Use store instances from the rendered component context**
## Async Testing Patterns
### Query Method Selection
- **`findByText()`**: Wait for element to appear, throws if not found
- **`queryByText()`**: Immediate query, returns null if not found
- **`getByText()`**: Immediate query, throws if not found
### Async Testing Flow
```typescript
// 1. Trigger async operation
store.popup.fire({ title: '測試彈窗', showConfirmButton: true })
// 2. Wait for interactive elements to appear
const confirmButton = await screen.findByText('確認')
// 3. Execute user action
await userEvent.click(confirmButton)
// 4. Wait for result state
await waitFor(() => {
  expect(screen.queryByText('測試彈窗')).not.toBeInTheDocument()
})
```
### Vue-Specific Timing
- **`nextTick()`**: Wait for Vue's reactive updates
- **`findBy*()`**: Wait for DOM elements (includes Vue updates + rendering)
- **`waitFor()`**: Wait for any condition to become true
## API Testing with MSW
### Mock Strategy
- Use MSW handlers for API mocking
- Test both success and error scenarios
- Mock authentication responses
- Test network failure scenarios
### Request Testing Pattern
```typescript
// Test API integration
const { success, data, msg } = await useRequest('/api/endpoint')
expect(success).toBe(true)
expect(data).toMatchObject(expectedResponse)
```
## Form Testing
### Multi-Step Form Testing
- Test form state persistence
- Validate form schemas with realistic data
- Test step navigation and validation
- Verify form submission handling
### Form Validation Pattern
```typescript
// Test validation with realistic business data
const validFormData = {
  name: '王小明',
  email: 'test@example.com',
  phone: '0912345678',
}
```
## Component Testing Best Practices
### Test Structure & Organization
Apply at least two levels of describe blocks for clear test structure:
```typescript
describe('PolicyCalculator', () => {
  describe('when calculating premium rates', () => {
    it('should return correct rate for standard coverage', () => {
      // Test implementation
    })
    it('should apply discount for long-term customers', () => {
      // Test implementation
    })
  })
  describe('when validation fails', () => {
    it('should show error message for invalid age', () => {
      // Test implementation
    })
  })
})
```
### Role-Based Testing
- Test permission-based access
- Verify user role functionality
- Test different user scenarios
### Business Logic Testing
- Test calculations and computations
- Verify business rule enforcement
- Test data processing functions
- Validate insurance-specific logic (premium calculations, coverage rules)
## Error Handling & Edge Cases
### Exception Testing Best Practices
When testing for expected errors, use dedicated assertion methods instead of try-catch blocks:
```typescript
// ✅ Good - Use dedicated assertion
it('should throw validation error when required field is missing', () => {
  expect(() => validatePolicy({})).toThrow('Policy number is required')
  expect(() => validatePolicy({})).toThrow(ValidationError) // Specific error type
})
// ❌ Bad - Manual try-catch
it('should throw validation error when required field is missing', () => {
  let errorThrown = false
  try {
    validatePolicy({})
  } catch (error) {
    errorThrown = true
    expect(error.message).toBe('Policy number is required')
  }
  expect(errorThrown).toBe(true)
})
```
### Common Test Scenarios
- Network failures during operations
- Invalid input data combinations
- Authentication failures and token expiry
- Concurrent user sessions
- Browser storage limitations
- API timeout scenarios
- Edge cases with Taiwan-specific data (ROC dates, phone formats, etc.)
## Mock Data Guidelines
### Realistic Test Data
- Use authentic-looking data that matches your domain
- Valid data patterns (emails, phone numbers, etc.)
- Realistic amounts and values
- Proper date formats
### Avoid Test Data Anti-Patterns
- Don't use overly simple test strings like 'test', 'foo', 'bar'
- Use domain-relevant data that mirrors production scenarios
- Include edge cases specific to your business domain
## Debugging Test Failures
### Common Issues & Solutions
- **Element not found**: Use `screen.debug()` to inspect DOM
- **Timing issues**: Check if using correct `findBy*` vs `queryBy*`
- **Store state pollution**: Verify store isolation between tests
- **Async race conditions**: Ensure proper `waitFor` usage
### Test Environment Debugging
```typescript
// Debug DOM state
console.log(screen.debug())
// Debug store state
console.log(store.$state)
// Debug test timing
await waitFor(() => {
  console.log('Current DOM:', screen.getByTestId('container').innerHTML)
})
```
## Additional Testing Guidelines
### Test Doubles (Mocks, Stubs, Spies) Best Practices
Use test doubles judiciously and for the right reasons:
```typescript
// ✅ Good - Mock external dependencies to test behavior
it('should send notification email when payment fails', async () => {
  const emailService = vi.fn()
  const paymentService = vi.fn().mockRejectedValue(new Error('Payment failed'))
  await processPayment(paymentData, { emailService, paymentService })
  expect(emailService).toHaveBeenCalledWith({
    to: customer.email,
    subject: 'Payment Failed',
    // ... other expected parameters
  })
})
// ❌ Bad - Mocking internal implementation details
it('should call internal helper method', () => {
  const spy = vi.spyOn(component, 'privateHelperMethod')
  component.publicMethod()
  expect(spy).toHaveBeenCalled() // Testing implementation, not behavior
})
```
- Focus on critical business logic coverage
Remember: Tests should be reliable, maintainable, and provide confidence in the application's functionality while being easy to understand and debug.
docs/testing-guide.md
比對新檔案
@@ -0,0 +1,451 @@
# 🧪 前端測試撰寫準則(Frontend Testing Guide)
### 參考
- [javascript-testing-best-practices](https://github.com/goldbergyoni/javascript-testing-best-practices/blob/master/readme-zh-TW.md)
- [Unit Testing Vue Apps: Tips, Tricks, and Best Practices](https://docs.google.com/presentation/d/1qr0JXF78UbXPmp0thK2uqgxRwU0kXzE0zykAPC0rQsE/edit?pli=1&slide=id.g13111f83864_0_3181#slide=id.g13111f83864_0_3181)
## 🎯 文件目的
此文件旨在建立團隊撰寫測試的共識,提升測試品質、可讀性與可維護性,避免冗餘或難以維護的測試,並促進穩定的開發流程。
---
## ✅ 核心原則
### 1. 3A 原則:Arrange - Act - Assert
撰寫測試時應依照以下三個階段清楚分區:
- **Arrange**:準備測試所需的環境、資料與元件
- **Act**:執行被測行為,例如觸發事件、模擬操作
- **Assert**:驗證預期結果,確認程式行為正確
> 建議每段用空行或註解清楚區隔,提高可讀性。
```ts
it('should call onSubmit when button is clicked', async () => {
  // Arrange
  const onSubmit = vi.fn()
  render(MyForm, { props: { onSubmit } })
  // Act
  await fireEvent.click(screen.getByText('Submit'))
  // Assert
  expect(onSubmit).toHaveBeenCalled()
})
```
---
## 🧱 撰寫測試的基本常識
### 2. 可讀性優先於抽象化
- 避免過度抽象測試流程(尤其是 Act 與 Assert)
- 測試應該讓讀者「一眼看懂在測什麼」
#### ✅ 可以抽出:
- 重複使用的 `render()` 或 `setup()` 程式
- 重複的 mock 資料
#### ❌ 不建議抽出:
- 特定場景下才會執行的斷言(避免藏在 helper 裡)
- 整段測試邏輯
---
### 3. 一個測試只驗證一個「行為」
- 每個 `it()` 應該只測一件事(單一行為)
- 減少一個測試中同時驗證多個結果,否則錯誤時難以追蹤原因
---
### 4. 測試命名要清楚描述「行為與結果」
- ❌ `it('should work')`
- ✅ `it('should disable the button when input is empty')`
---
### 5. 測試資料要貼近實際情境
- 測試用的資料(props、mock response)盡可能模擬真實場景
- 減少硬編字串,提升信任度與可維護性
---
### 6. 保持測試綠燈,清理過時測試
- 若測試已不符合目前規格(產品或邏輯改變):
  - ✅ 應更新測試內容
  - ✅ 或註記原因後移除
> 測試若長期失敗而不修,會讓團隊漸漸無視錯誤,破壞測試的信任度。
---
### 7. 僅測「有價值」的行為,不要測框架細節
- ❌ 不要測某元素是否有特定 class(除非該 class 有功能影響)
- ✅ 測按鈕點擊是否導致狀態變更或觸發 callback
---
### 8. Mock 是為了隔離副作用,不是逃避驗證
- mock 的目的是控制輸入與排除副作用(例如 API 請求)
- 不應以 mock 遮蔽應該被測試的邏輯或流程
---
## ⚙️ 測試結構建議
```text
MyComponent/
  ├── MyComponent.vue
  ├── MyComponent.test.ts      <- 測試檔與元件同層
  ├── __mocks__/                <- 可選:放複雜 mock data
```
---
## 🧪 使用 Testing Library 的最佳實踐
- Queries 的優先順序:
  - 使用者可視的最優先,例如 `getByRole`, `getByLabelText`, `getByPlaceholderText`
  - 有語意的次要,例如 `getByAltText`, `getByTitle`
  - 沒辦法的才用 `getByTestId`。
- Query Container 的優先順序: screen 最優先,container 次要。
- 非同步狀態使用 `findBy...` 或 `waitFor`,避免 race condition
- 每個測試結束都要執行 cleanup(),目前已寫在 setupTests.ts 中。
---
## 📌 小提醒清單(Cheatsheet)
| 原則           | 說明                                      |
| -------------- | ----------------------------------------- |
| 3A 原則        | 清楚分區 Arrange / Act / Assert           |
| 可讀性優先     | 測試邏輯應直接閱讀理解                    |
| 適度抽象       | 可抽出 setup,但保留每個場景關鍵的斷言    |
| 保持綠燈       | 測試要穩定通過,過時就更新或移除          |
| 單一責任       | 一個 `it()` 測一件事                      |
| 模擬真實       | 使用貼近實際情況的資料與流程              |
| 測行為         | 測使用者觀點的功能,不測框架或視覺細節    |
| Store 隔離     | 避免測試間 Pinia store 狀態汙染           |
| 正確等待       | `findBy*` 等待出現,`queryBy*` 檢查不存在 |
| 分離操作等待   | 先等待元素出現,再執行操作,最後等待結果  |
| Store 實例時機 | 在 `render` 後取得 store 實例             |
### 🔍 Query 方法快速參考
| 情境                   | 使用方法               | 說明                                 |
| ---------------------- | ---------------------- | ------------------------------------ |
| 測試元素存在           | `findByText()`         | 等待元素出現,找不到會拋錯(預設一秒) |
| 測試元素不存在         | `queryByText()`        | 立即查詢,找不到返回 null            |
| 立即取得已存在的元素   | `getByText()`          | 立即查詢,找不到會拋錯               |
| 等待元素出現後進行操作 | `await findByText()`   | 適合需要等待 DOM 更新的互動元素      |
| 等待狀態變化           | `waitFor(() => {...})` | 適合等待非同步操作完成後的狀態檢查   |
---
## 套件簡介
| 步驟 | 主要負責套件              | 執行的任務                                                                            | 比喻                 |
| :--: | :------------------------ | :------------------------------------------------------------------------------------ | :------------------- |
|  1   | Vitest                    | 啟動並管理整個測試流程(Test runner)。                                                 | 導演 / 測試監督      |
|  2   | jsdom                     | 在 Node.js 中建立一個模擬的瀏覽器 DOM 環境 (提供 document, window 等)。               | 搭建虛擬舞台         |
|  3   | @testing-library/vue      | 將要測試的元件渲染到 jsdom 所建立的虛擬 DOM 中。                                      | 演員登台表演         |
|  4   | @testing-library/jest-dom | 提供語意化的斷言函式(matcher) (如 .toBeInTheDocument()) 來檢查 DOM 狀態是否符合預期。 | 使用專業工具驗收舞台 |
---
---
## 測試小知識
### 1. 在 Vue 測試中如何正確等待 DOM 更新與非同步操作
在進行 Vue 元件測試時,一個常見的挑戰是處理「非同步」行為。這包括 Vue 本身的 DOM 更新機制 (tick),以及像 API 請求這樣的 Promise。如果沒有正確地等待,測試就可能會在畫面或資料還沒準備好時就進行斷言 (assert),導致測試失敗。
以下是正確的處理流程與觀念:
**核心觀念**
1. Vue 的 Tick 機制
   - Vue 不會在你每次更改資料時,都「立即」更新畫面 (DOM)。
   - 相反地,它會將同一個事件循環 (event loop) 中的所有更新打包起來,在「下一個 tick」再一次性地完成。
   - 這樣做是為了效能,避免不必要的重複渲染。
2. 非同步操作 (Promise)
   - 像 axios 或 fetch 這類的 API 請求是非同步的,它們會回傳一個 Promise。
   - 程式碼不會停下來等它完成,而是會繼續往下執行。測試必須明確地等待這些 Promise 完成。
**正確的測試流程**
假設我們要測試元件在錨定 (onMounted) 時會做的兩件事:
1. 顯示載入狀態。
2. 發送 API 請求去取得資料,結束後載入狀態消失。
在 Vue 中測試包含 API 請求的非同步行為時,關鍵在於等待兩個不同的時機點:畫面更新與非同步任務完成。
第一步:等待「載入中」狀態出現
- 當元件渲染並觸發 API 請求後,它會立即更新內部狀態 (例如 isLoading = true),但這個變化不會馬上渲染到畫面上。
- 使用 await nextTick() 來暫停測試,並等待 Vue 完成其 DOM 更新週期。
- 此時,可以斷言「載入中」的 UI 元素 (如 .el-loading-mask) 已經出現在畫面上。
第二步:等待「載入完成」狀態
- API 請求需要時間來完成。當請求結束後,元件會再次更新狀態 (例如 isLoading = false),並移除「載入中」的 UI。
- 使用 await waitFor() 來反覆檢查畫面,直到「載入中」的 UI 元素從畫面上消失為止。
- waitFor 會在指定的超時 (timeout) 時間內,持續輪詢檢查您的斷言是否成立,這非常適合用來等待一個非同步操作的結束。
總結來說:使用 nextTick 捕捉初始狀態的畫面變化;使用 waitFor 等待非同步操作結束後的最終畫面狀態。
---
## 🏪 Pinia Store 測試最佳實踐
### Store 狀態隔離策略
在測試使用 Pinia store 的元件時,最重要的考量是**避免測試間的狀態汙染**。以下提供兩種常見的策略,各有優缺點:
#### 策略 1:每個測試獨立建立 Store 實例
```typescript
// ✅ 策略 1:完全隔離,每個測試都建立新的 store
it('should work correctly', () => {
  const testStore = createTestingPinia({ createSpy: vi.fn })
  render(Component, {
    global: { plugins: [testStore] },
  })
  const store = useMyStore() // 在 render 後取得 store 實例
  // 測試邏輯...
})
```
**優點**:
- 完全避免測試間汙染
- 每個測試都有乾淨的初始狀態
- 測試間完全獨立,執行順序不影響結果
**缺點**:
- 程式碼較冗長,需要重複建立 store
- 測試執行時間可能稍長
#### 策略 2:共享 Store 實例 + 清理機制
```typescript
// ✅ 策略 2:共享實例,但在測試間清理狀態
const mockedStore = createTestingPinia({ createSpy: vi.fn })
beforeEach(() => {
  // 選項 A: 重置所有 store 狀態
  mockedStore.state.value = {}
  // 選項 B: 手動重置特定 store
  // const store = useMyStore()
  // store.$reset() 或手動重置特定屬性
})
// 或使用 afterEach
afterEach(() => {
  // 清理邏輯同上
})
```
**優點**:
- 程式碼較簡潔
- 測試執行效率較高
- 適合大量測試的場景
**缺點**:
- 需要確保清理邏輯完整
- 如果清理不當,仍可能有狀態汙染
- 需要了解 store 的內部結構來正確清理
#### 建議選擇標準
- **小型測試檔案**(<10 個測試):建議使用策略 1
- **大型測試檔案**或**頻繁使用相同 store 配置**:考慮策略 2
- **複雜的 store 狀態**:建議使用策略 1 以避免清理遺漏
- **簡單的 store 狀態**:兩種策略都可以
### Store 實例取得時機
⚠️ **重要**:務必在 `render` 之後才取得 store 實例
```typescript
// ❌ 錯誤:在 render 前取得 store,可能取得錯誤的實例
const store = useMyStore()
render(Component, { global: { plugins: [testStore] } })
// ✅ 正確:在 render 後取得 store
render(Component, { global: { plugins: [testStore] } })
const store = useMyStore()
```
---
## 🔄 非同步測試的常見模式與陷阱
### findBy vs queryBy 的選擇
了解何時使用不同的查詢方法是成功測試的關鍵:
```typescript
// ✅ 測試元素存在:使用 findBy* (會等待元素出現)
expect(await screen.findByText('載入完成')).toBeInTheDocument()
// ✅ 測試元素不存在:使用 queryBy* (立即查詢,不等待)
expect(screen.queryByText('已移除')).not.toBeInTheDocument()
// ❌ 錯誤:用 findBy 測試不存在會拋錯
expect(await screen.findByText('不存在')).not.toBeInTheDocument() // 會拋錯!
```
### 非同步操作的正確等待模式
當需要等待元素出現後進行操作時:
```typescript
// ✅ 推薦模式:分步等待
it('should close popup when confirm button clicked', async () => {
  // 1. 觸發顯示彈窗
  store.popup.fire({ title: '測試彈窗', showConfirmButton: true })
  // 2. 等待互動元素出現
  const confirmButton = await screen.findByText('確認')
  // 3. 執行使用者操作
  await userEvent.click(confirmButton)
  // 4. 等待操作結果
  await waitFor(() => {
    expect(screen.queryByText('測試彈窗')).not.toBeInTheDocument()
  })
})
```
### waitFor 的使用原則
```typescript
// ✅ 正確:waitFor 內只做斷言,不執行操作
await userEvent.click(button)
await waitFor(() => {
  expect(element).not.toBeInTheDocument()
})
// ⚠️  特殊情況:當操作目標也需要等待出現時
await waitFor(async () => {
  const dynamicButton = screen.getByText('動態按鈕')
  await userEvent.click(dynamicButton)
  expect(result).toBeTruthy()
})
// ✅ 更好的方式:使用 findBy 分離等待和操作
const dynamicButton = await screen.findByText('動態按鈕')
await userEvent.click(dynamicButton)
await waitFor(() => {
  expect(result).toBeTruthy()
})
```
### Vue 特有的等待時機
```typescript
// nextTick:等待 Vue 的響應式更新
await nextTick()
// findBy*:等待 DOM 元素出現(包含 Vue 更新 + 渲染)
const element = await screen.findByText('目標文字')
// waitFor:等待任意條件成立(通常用於狀態變化)
  expect(condition).toBeTruthy()
})
```
---
## 💡 實際案例:Popup 元件測試
以下是一個完整的 Popup 測試案例,展示了本文提到的最佳實踐:
```typescript
import { describe, vi, it, expect } from 'vitest'
import { createTestingPinia } from '@pinia/testing'
import { screen, waitFor, render } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import App from '@/App.vue'
import { usePopupStore } from '@/stores/popup'
vi.mock('vue-router', () => ({
  useRoute: vi.fn(() => ({ meta: {} })),
}))
describe('使用者可以互動 popup 並關閉', () => {
  it('使用 popup.fire(config) 應正確依照 config.title 參數顯示彈窗文字', async () => {
    // Arrange - 準備測試環境
    const mockedStore = createTestingPinia({ createSpy: vi.fn })
    render(App, {
      global: { plugins: [mockedStore] },
    })
    // Act - 執行被測行為
    const store = usePopupStore() // 在 render 後取得 store
    store.popup.fire({ title: '測試彈窗', showConfirmButton: true })
    // Assert - 驗證結果
    expect(await screen.findByText('測試彈窗')).toBeInTheDocument()
  })
  it('點擊確認按鈕後,會關閉彈窗', async () => {
    // Arrange
    const mockedStore = createTestingPinia({ createSpy: vi.fn })
    render(App, {
      global: { plugins: [mockedStore] },
    })
    const store = usePopupStore()
    // Act - 分步執行:顯示彈窗 → 等待按鈕出現 → 點擊 → 等待關閉
    store.popup.fire({ title: '測試彈窗', showConfirmButton: true })
    const confirmButton = await screen.findByText('確認') // 等待元素出現
    await userEvent.click(confirmButton) // 執行操作
    // Assert - 等待彈窗消失
    await waitFor(() => {
      expect(screen.queryByText('測試彈窗')).not.toBeInTheDocument()
    })
  })
})
```
### 此案例展示的重點:
1. **Store 隔離**:每個測試建立獨立的 `createTestingPinia` 實例
2. **正確的實例取得時機**:在 `render` 後才取得 store
3. **適當的等待策略**:
   - 使用 `findByText` 等待彈窗標題出現
   - 使用 `findByText` 等待確認按鈕出現
   - 使用 `queryByText` + `waitFor` 等待彈窗消失
4. **清晰的 3A 結構**:Arrange、Act、Assert 分明
5. **分離操作和等待**:先等待元素出現,再執行點擊,最後等待結果
---
e2e/features/example/user-flow.spec.ts
比對新檔案
@@ -0,0 +1,79 @@
import { test, expect } from '@playwright/test'
/**
 * ExamplePage - 完整使用者流程測試
 *
 * 測試目標:驗證使用者從進入頁面到完成 API 請求的完整流程
 * 測試環境:真實瀏覽器環境,不使用 mock
 */
test.describe('ExamplePage - 完整使用者流程', () => {
  test.beforeEach(async ({ page }) => {
    // 導航到 ExamplePage
    await page.goto('/')
  })
  test('使用者應該能看到頁面標題和請求按鈕', async ({ page }) => {
    // Arrange & Act - 頁面已在 beforeEach 中載入
    // Assert - 驗證頁面標題存在(至少出現一次)
    await expect(page.getByText('I am Example Page').first()).toBeVisible()
    // Assert - 驗證請求按鈕存在並可見
    const button = page.getByRole('button', {
      name: /click to call example request/i,
    })
    await expect(button).toBeVisible()
  })
  test('使用者點擊按鈕後應該看到完整的請求流程:loading → 結果顯示', async ({ page }) => {
    // Arrange - 取得按鈕元素
    const button = page.getByRole('button', {
      name: /click to call example request/i,
    })
    // Assert - 初始狀態不應該顯示 loading 或結果
    await expect(page.getByText('fetching...')).toBeHidden()
    await expect(page.getByText('the mock response is:')).toBeHidden()
    // Act - 點擊按鈕觸發請求
    await button.click()
    // Assert - 驗證 loading 狀態出現(需要快速檢查,因為可能很快消失)
    // 使用 waitFor 而不是 expect,因為 loading 可能很快就消失
    try {
      await expect(page.getByText('fetching...')).toBeVisible({ timeout: 1000 })
    } catch {
      // 如果 API 太快回應,loading 可能已經消失,這是可接受的
      console.log('Loading state was too fast to capture')
    }
    // Assert - 驗證最終結果顯示
    await expect(page.getByText('the mock response is:')).toBeVisible({
      timeout: 5000,
    })
    // Assert - 驗證回應資料出現
    await expect(page.getByText(/hello world/i)).toBeVisible()
    // Assert - 驗證 loading 狀態已消失
    await expect(page.getByText('fetching...')).toBeHidden()
  })
  test('驗證回應資料的格式正確(JSON 字串)', async ({ page }) => {
    // Arrange & Act
    const button = page.getByRole('button', {
      name: /click to call example request/i,
    })
    await button.click()
    // Assert - 等待結果顯示
    await expect(page.getByText('the mock response is:')).toBeVisible({
      timeout: 5000,
    })
    // Assert - 驗證資料包含引號(JSON 字串格式)
    const responseText = await page.getByText(/"hello world"/i).textContent()
    expect(responseText).toContain('"')
    expect(responseText).toContain('hello world')
  })
})
package-lock.json
@@ -9,6 +9,7 @@
      "version": "0.0.0",
      "dependencies": {
        "@tailwindcss/vite": "^4.1.16",
        "axios": "^1.13.2",
        "pinia": "^3.0.3",
        "tailwind-merge": "^3.4.0",
        "tailwindcss": "^4.1.16",
@@ -16,7 +17,11 @@
        "vue-router": "^4.6.3"
      },
      "devDependencies": {
        "@pinia/testing": "^1.0.3",
        "@playwright/test": "^1.56.1",
        "@testing-library/jest-dom": "^6.9.1",
        "@testing-library/user-event": "^14.6.1",
        "@testing-library/vue": "^8.1.0",
        "@tsconfig/node22": "^22.0.2",
        "@types/jsdom": "^27.0.0",
        "@types/node": "^22.18.11",
@@ -32,6 +37,7 @@
        "eslint-plugin-vue": "~10.5.0",
        "jiti": "^2.6.1",
        "jsdom": "^27.0.1",
        "msw": "^2.12.1",
        "npm-run-all2": "^8.0.4",
        "prettier": "^3.6.2",
        "prettier-plugin-tailwindcss": "^0.7.1",
@@ -49,6 +55,13 @@
      "version": "0.9.20",
      "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.20.tgz",
      "integrity": "sha512-YUSA5jW8qn/c6nZUlFsn2Nt5qFFRBcGTgL9CzbiZbJCtEFY0Nv/ycO3BHT9tLjus9++zOYWe5mLCRIesuay25g==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@adobe/css-tools": {
      "version": "4.4.4",
      "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz",
      "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==",
      "dev": true,
      "license": "MIT"
    },
@@ -514,6 +527,16 @@
      },
      "peerDependencies": {
        "@babel/core": "^7.0.0-0"
      }
    },
    "node_modules/@babel/runtime": {
      "version": "7.28.4",
      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
      "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/@babel/template": {
@@ -1347,6 +1370,154 @@
        "url": "https://github.com/sponsors/nzakas"
      }
    },
    "node_modules/@inquirer/ansi": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz",
      "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@inquirer/confirm": {
      "version": "5.1.20",
      "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.20.tgz",
      "integrity": "sha512-HDGiWh2tyRZa0M1ZnEIUCQro25gW/mN8ODByicQrbR1yHx4hT+IOpozCMi5TgBtUdklLwRI2mv14eNpftDluEw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@inquirer/core": "^10.3.1",
        "@inquirer/type": "^3.0.10"
      },
      "engines": {
        "node": ">=18"
      },
      "peerDependencies": {
        "@types/node": ">=18"
      },
      "peerDependenciesMeta": {
        "@types/node": {
          "optional": true
        }
      }
    },
    "node_modules/@inquirer/core": {
      "version": "10.3.1",
      "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.1.tgz",
      "integrity": "sha512-hzGKIkfomGFPgxKmnKEKeA+uCYBqC+TKtRx5LgyHRCrF6S2MliwRIjp3sUaWwVzMp7ZXVs8elB0Tfe682Rpg4w==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@inquirer/ansi": "^1.0.2",
        "@inquirer/figures": "^1.0.15",
        "@inquirer/type": "^3.0.10",
        "cli-width": "^4.1.0",
        "mute-stream": "^3.0.0",
        "signal-exit": "^4.1.0",
        "wrap-ansi": "^6.2.0",
        "yoctocolors-cjs": "^2.1.3"
      },
      "engines": {
        "node": ">=18"
      },
      "peerDependencies": {
        "@types/node": ">=18"
      },
      "peerDependenciesMeta": {
        "@types/node": {
          "optional": true
        }
      }
    },
    "node_modules/@inquirer/core/node_modules/ansi-regex": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/@inquirer/core/node_modules/emoji-regex": {
      "version": "8.0.0",
      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@inquirer/core/node_modules/string-width": {
      "version": "4.2.3",
      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "emoji-regex": "^8.0.0",
        "is-fullwidth-code-point": "^3.0.0",
        "strip-ansi": "^6.0.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/@inquirer/core/node_modules/strip-ansi": {
      "version": "6.0.1",
      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "ansi-regex": "^5.0.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/@inquirer/core/node_modules/wrap-ansi": {
      "version": "6.2.0",
      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
      "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "ansi-styles": "^4.0.0",
        "string-width": "^4.1.0",
        "strip-ansi": "^6.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/@inquirer/figures": {
      "version": "1.0.15",
      "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz",
      "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@inquirer/type": {
      "version": "3.0.10",
      "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz",
      "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=18"
      },
      "peerDependencies": {
        "@types/node": ">=18"
      },
      "peerDependenciesMeta": {
        "@types/node": {
          "optional": true
        }
      }
    },
    "node_modules/@isaacs/cliui": {
      "version": "8.0.2",
      "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -1410,6 +1581,24 @@
        "@jridgewell/sourcemap-codec": "^1.4.14"
      }
    },
    "node_modules/@mswjs/interceptors": {
      "version": "0.40.0",
      "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.40.0.tgz",
      "integrity": "sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@open-draft/deferred-promise": "^2.2.0",
        "@open-draft/logger": "^0.3.0",
        "@open-draft/until": "^2.0.0",
        "is-node-process": "^1.2.0",
        "outvariant": "^1.4.3",
        "strict-event-emitter": "^0.5.1"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@nodelib/fs.scandir": {
      "version": "2.1.5",
      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1454,6 +1643,44 @@
      "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@open-draft/deferred-promise": {
      "version": "2.2.0",
      "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz",
      "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@open-draft/logger": {
      "version": "0.3.0",
      "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz",
      "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "is-node-process": "^1.2.0",
        "outvariant": "^1.4.0"
      }
    },
    "node_modules/@open-draft/until": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz",
      "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@pinia/testing": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/@pinia/testing/-/testing-1.0.3.tgz",
      "integrity": "sha512-g+qR49GNdI1Z8rZxKrQC3GN+LfnGTNf5Kk8Nz5Cz6mIGva5WRS+ffPXQfzhA0nu6TveWzPNYTjGl4nJqd3Cu9Q==",
      "dev": true,
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/posva"
      },
      "peerDependencies": {
        "pinia": ">=3.0.4"
      }
    },
    "node_modules/@pkgjs/parseargs": {
      "version": "0.11.0",
@@ -2052,10 +2279,133 @@
        "vite": "^5.2.0 || ^6 || ^7"
      }
    },
    "node_modules/@testing-library/dom": {
      "version": "10.4.1",
      "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
      "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@babel/code-frame": "^7.10.4",
        "@babel/runtime": "^7.12.5",
        "@types/aria-query": "^5.0.1",
        "aria-query": "5.3.0",
        "dom-accessibility-api": "^0.5.9",
        "lz-string": "^1.5.0",
        "picocolors": "1.1.1",
        "pretty-format": "^27.0.2"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@testing-library/jest-dom": {
      "version": "6.9.1",
      "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
      "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@adobe/css-tools": "^4.4.0",
        "aria-query": "^5.0.0",
        "css.escape": "^1.5.1",
        "dom-accessibility-api": "^0.6.3",
        "picocolors": "^1.1.1",
        "redent": "^3.0.0"
      },
      "engines": {
        "node": ">=14",
        "npm": ">=6",
        "yarn": ">=1"
      }
    },
    "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
      "version": "0.6.3",
      "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
      "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@testing-library/user-event": {
      "version": "14.6.1",
      "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
      "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=12",
        "npm": ">=6"
      },
      "peerDependencies": {
        "@testing-library/dom": ">=7.21.4"
      }
    },
    "node_modules/@testing-library/vue": {
      "version": "8.1.0",
      "resolved": "https://registry.npmjs.org/@testing-library/vue/-/vue-8.1.0.tgz",
      "integrity": "sha512-ls4RiHO1ta4mxqqajWRh8158uFObVrrtAPoxk7cIp4HrnQUj/ScKzqz53HxYpG3X6Zb7H2v+0eTGLSoy8HQ2nA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/runtime": "^7.23.2",
        "@testing-library/dom": "^9.3.3",
        "@vue/test-utils": "^2.4.1"
      },
      "engines": {
        "node": ">=14"
      },
      "peerDependencies": {
        "@vue/compiler-sfc": ">= 3",
        "vue": ">= 3"
      },
      "peerDependenciesMeta": {
        "@vue/compiler-sfc": {
          "optional": true
        }
      }
    },
    "node_modules/@testing-library/vue/node_modules/@testing-library/dom": {
      "version": "9.3.4",
      "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz",
      "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@babel/code-frame": "^7.10.4",
        "@babel/runtime": "^7.12.5",
        "@types/aria-query": "^5.0.1",
        "aria-query": "5.1.3",
        "chalk": "^4.1.0",
        "dom-accessibility-api": "^0.5.9",
        "lz-string": "^1.5.0",
        "pretty-format": "^27.0.2"
      },
      "engines": {
        "node": ">=14"
      }
    },
    "node_modules/@testing-library/vue/node_modules/aria-query": {
      "version": "5.1.3",
      "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz",
      "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==",
      "dev": true,
      "license": "Apache-2.0",
      "dependencies": {
        "deep-equal": "^2.0.5"
      }
    },
    "node_modules/@tsconfig/node22": {
      "version": "22.0.2",
      "resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.2.tgz",
      "integrity": "sha512-Kmwj4u8sDRDrMYRoN9FDEcXD8UpBSaPQQ24Gz+Gamqfm7xxn+GBR7ge/Z7pK8OXNGyUzbSwJj+TH6B+DS/epyA==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@types/aria-query": {
      "version": "5.0.4",
      "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
      "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
      "dev": true,
      "license": "MIT"
    },
@@ -2111,6 +2461,13 @@
      "dependencies": {
        "undici-types": "~6.21.0"
      }
    },
    "node_modules/@types/statuses": {
      "version": "2.0.6",
      "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz",
      "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@types/tough-cookie": {
      "version": "4.0.5",
@@ -3082,6 +3439,33 @@
      "dev": true,
      "license": "Python-2.0"
    },
    "node_modules/aria-query": {
      "version": "5.3.0",
      "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
      "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
      "dev": true,
      "license": "Apache-2.0",
      "dependencies": {
        "dequal": "^2.0.3"
      }
    },
    "node_modules/array-buffer-byte-length": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
      "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.3",
        "is-array-buffer": "^3.0.5"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/assertion-error": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
@@ -3090,6 +3474,39 @@
      "license": "MIT",
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/asynckit": {
      "version": "0.4.0",
      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
      "license": "MIT"
    },
    "node_modules/available-typed-arrays": {
      "version": "1.0.7",
      "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
      "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "possible-typed-array-names": "^1.0.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/axios": {
      "version": "1.13.2",
      "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
      "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
      "license": "MIT",
      "dependencies": {
        "follow-redirects": "^1.15.6",
        "form-data": "^4.0.4",
        "proxy-from-env": "^1.1.0"
      }
    },
    "node_modules/balanced-match": {
@@ -3219,6 +3636,55 @@
        "node": ">=8"
      }
    },
    "node_modules/call-bind": {
      "version": "1.0.8",
      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
      "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.0",
        "es-define-property": "^1.0.0",
        "get-intrinsic": "^1.2.4",
        "set-function-length": "^1.2.2"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/call-bind-apply-helpers": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/call-bound": {
      "version": "1.0.4",
      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "get-intrinsic": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/callsites": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -3294,6 +3760,94 @@
        "node": ">= 16"
      }
    },
    "node_modules/cli-width": {
      "version": "4.1.0",
      "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
      "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
      "dev": true,
      "license": "ISC",
      "engines": {
        "node": ">= 12"
      }
    },
    "node_modules/cliui": {
      "version": "8.0.1",
      "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
      "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
      "dev": true,
      "license": "ISC",
      "dependencies": {
        "string-width": "^4.2.0",
        "strip-ansi": "^6.0.1",
        "wrap-ansi": "^7.0.0"
      },
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/cliui/node_modules/ansi-regex": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/cliui/node_modules/emoji-regex": {
      "version": "8.0.0",
      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/cliui/node_modules/string-width": {
      "version": "4.2.3",
      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "emoji-regex": "^8.0.0",
        "is-fullwidth-code-point": "^3.0.0",
        "strip-ansi": "^6.0.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/cliui/node_modules/strip-ansi": {
      "version": "6.0.1",
      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "ansi-regex": "^5.0.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/cliui/node_modules/wrap-ansi": {
      "version": "7.0.0",
      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "ansi-styles": "^4.0.0",
        "string-width": "^4.1.0",
        "strip-ansi": "^6.0.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
      }
    },
    "node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -3313,6 +3867,18 @@
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/combined-stream": {
      "version": "1.0.8",
      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
      "license": "MIT",
      "dependencies": {
        "delayed-stream": "~1.0.0"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/commander": {
      "version": "10.0.1",
@@ -3348,6 +3914,16 @@
      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/cookie": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
      "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/copy-anything": {
      "version": "4.0.5",
@@ -3392,6 +3968,13 @@
      "engines": {
        "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
      }
    },
    "node_modules/css.escape": {
      "version": "1.5.1",
      "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
      "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/cssesc": {
      "version": "3.0.0",
@@ -3476,6 +4059,39 @@
        "node": ">=6"
      }
    },
    "node_modules/deep-equal": {
      "version": "2.2.3",
      "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz",
      "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "array-buffer-byte-length": "^1.0.0",
        "call-bind": "^1.0.5",
        "es-get-iterator": "^1.1.3",
        "get-intrinsic": "^1.2.2",
        "is-arguments": "^1.1.1",
        "is-array-buffer": "^3.0.2",
        "is-date-object": "^1.0.5",
        "is-regex": "^1.1.4",
        "is-shared-array-buffer": "^1.0.2",
        "isarray": "^2.0.5",
        "object-is": "^1.1.5",
        "object-keys": "^1.1.1",
        "object.assign": "^4.1.4",
        "regexp.prototype.flags": "^1.5.1",
        "side-channel": "^1.0.4",
        "which-boxed-primitive": "^1.0.2",
        "which-collection": "^1.0.1",
        "which-typed-array": "^1.1.13"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/deep-is": {
      "version": "0.1.4",
      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -3513,6 +4129,24 @@
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/define-data-property": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
      "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "es-define-property": "^1.0.0",
        "es-errors": "^1.3.0",
        "gopd": "^1.0.1"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/define-lazy-prop": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
@@ -3524,6 +4158,64 @@
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/define-properties": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
      "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "define-data-property": "^1.0.1",
        "has-property-descriptors": "^1.0.0",
        "object-keys": "^1.1.1"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/delayed-stream": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
      "license": "MIT",
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/dequal": {
      "version": "2.0.3",
      "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
      "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/dom-accessibility-api": {
      "version": "0.5.16",
      "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
      "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/dunder-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.1",
        "es-errors": "^1.3.0",
        "gopd": "^1.2.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/eastasianwidth": {
@@ -3630,12 +4322,78 @@
        "url": "https://github.com/sponsors/antfu"
      }
    },
    "node_modules/es-define-property": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-errors": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-get-iterator": {
      "version": "1.1.3",
      "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz",
      "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "call-bind": "^1.0.2",
        "get-intrinsic": "^1.1.3",
        "has-symbols": "^1.0.3",
        "is-arguments": "^1.1.1",
        "is-map": "^2.0.2",
        "is-set": "^2.0.2",
        "is-string": "^1.0.7",
        "isarray": "^2.0.5",
        "stop-iteration-iterator": "^1.0.0"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/es-module-lexer": {
      "version": "1.7.0",
      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
      "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/es-object-atoms": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-set-tostringtag": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.6",
        "has-tostringtag": "^1.0.2",
        "hasown": "^2.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/esbuild": {
      "version": "0.25.12",
@@ -4177,6 +4935,42 @@
      "dev": true,
      "license": "ISC"
    },
    "node_modules/follow-redirects": {
      "version": "1.15.11",
      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
      "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
      "funding": [
        {
          "type": "individual",
          "url": "https://github.com/sponsors/RubenVerborgh"
        }
      ],
      "license": "MIT",
      "engines": {
        "node": ">=4.0"
      },
      "peerDependenciesMeta": {
        "debug": {
          "optional": true
        }
      }
    },
    "node_modules/for-each": {
      "version": "0.3.5",
      "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
      "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "is-callable": "^1.2.7"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/foreground-child": {
      "version": "3.3.1",
      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
@@ -4194,6 +4988,22 @@
        "url": "https://github.com/sponsors/isaacs"
      }
    },
    "node_modules/form-data": {
      "version": "4.0.4",
      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
      "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
      "license": "MIT",
      "dependencies": {
        "asynckit": "^0.4.0",
        "combined-stream": "^1.0.8",
        "es-set-tostringtag": "^2.1.0",
        "hasown": "^2.0.2",
        "mime-types": "^2.1.12"
      },
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/fsevents": {
      "version": "2.3.2",
      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
@@ -4208,6 +5018,25 @@
        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
      }
    },
    "node_modules/function-bind": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/functions-have-names": {
      "version": "1.2.3",
      "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
      "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
      "dev": true,
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/gensync": {
      "version": "1.0.0-beta.2",
      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -4216,6 +5045,53 @@
      "license": "MIT",
      "engines": {
        "node": ">=6.9.0"
      }
    },
    "node_modules/get-caller-file": {
      "version": "2.0.5",
      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
      "dev": true,
      "license": "ISC",
      "engines": {
        "node": "6.* || 8.* || >= 10.*"
      }
    },
    "node_modules/get-intrinsic": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "es-define-property": "^1.0.1",
        "es-errors": "^1.3.0",
        "es-object-atoms": "^1.1.1",
        "function-bind": "^1.1.2",
        "get-proto": "^1.0.1",
        "gopd": "^1.2.0",
        "has-symbols": "^1.1.0",
        "hasown": "^2.0.2",
        "math-intrinsics": "^1.1.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
      "license": "MIT",
      "dependencies": {
        "dunder-proto": "^1.0.1",
        "es-object-atoms": "^1.0.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/glob": {
@@ -4265,6 +5141,18 @@
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/gopd": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/graceful-fs": {
      "version": "4.2.11",
      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -4278,6 +5166,29 @@
      "dev": true,
      "license": "MIT"
    },
    "node_modules/graphql": {
      "version": "16.12.0",
      "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz",
      "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
      }
    },
    "node_modules/has-bigints": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
      "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -4287,6 +5198,65 @@
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/has-property-descriptors": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
      "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "es-define-property": "^1.0.0"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/has-symbols": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/has-tostringtag": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
      "license": "MIT",
      "dependencies": {
        "has-symbols": "^1.0.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/hasown": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
      "license": "MIT",
      "dependencies": {
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/headers-polyfill": {
      "version": "4.0.3",
      "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz",
      "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/hookable": {
      "version": "5.5.3",
@@ -4385,12 +5355,135 @@
        "node": ">=0.8.19"
      }
    },
    "node_modules/indent-string": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
      "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/ini": {
      "version": "1.3.8",
      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
      "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
      "dev": true,
      "license": "ISC"
    },
    "node_modules/internal-slot": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
      "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "hasown": "^2.0.2",
        "side-channel": "^1.1.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/is-arguments": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
      "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "has-tostringtag": "^1.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/is-array-buffer": {
      "version": "3.0.5",
      "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
      "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "call-bind": "^1.0.8",
        "call-bound": "^1.0.3",
        "get-intrinsic": "^1.2.6"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/is-bigint": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
      "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "has-bigints": "^1.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/is-boolean-object": {
      "version": "1.2.2",
      "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
      "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.3",
        "has-tostringtag": "^1.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/is-callable": {
      "version": "1.2.7",
      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
      "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/is-date-object": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
      "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "has-tostringtag": "^1.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/is-docker": {
      "version": "3.0.0",
@@ -4460,6 +5553,26 @@
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/is-map": {
      "version": "2.0.3",
      "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
      "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/is-node-process": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz",
      "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/is-number": {
      "version": "7.0.0",
      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -4470,12 +5583,142 @@
        "node": ">=0.12.0"
      }
    },
    "node_modules/is-number-object": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
      "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.3",
        "has-tostringtag": "^1.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/is-potential-custom-element-name": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
      "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/is-regex": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
      "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "gopd": "^1.2.0",
        "has-tostringtag": "^1.0.2",
        "hasown": "^2.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/is-set": {
      "version": "2.0.3",
      "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
      "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/is-shared-array-buffer": {
      "version": "1.0.4",
      "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
      "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/is-string": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
      "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.3",
        "has-tostringtag": "^1.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/is-symbol": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
      "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "has-symbols": "^1.1.0",
        "safe-regex-test": "^1.1.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/is-weakmap": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
      "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/is-weakset": {
      "version": "2.0.4",
      "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
      "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.3",
        "get-intrinsic": "^1.2.6"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/is-what": {
      "version": "5.5.0",
@@ -4504,6 +5747,13 @@
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/isarray": {
      "version": "2.0.5",
      "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
      "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/isexe": {
      "version": "2.0.0",
@@ -5051,6 +6301,16 @@
        "yallist": "^3.0.2"
      }
    },
    "node_modules/lz-string": {
      "version": "1.5.0",
      "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
      "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
      "dev": true,
      "license": "MIT",
      "bin": {
        "lz-string": "bin/bin.js"
      }
    },
    "node_modules/magic-string": {
      "version": "0.30.21",
      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -5058,6 +6318,15 @@
      "license": "MIT",
      "dependencies": {
        "@jridgewell/sourcemap-codec": "^1.5.5"
      }
    },
    "node_modules/math-intrinsics": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/mdn-data": {
@@ -5098,6 +6367,37 @@
      },
      "engines": {
        "node": ">=8.6"
      }
    },
    "node_modules/mime-db": {
      "version": "1.52.0",
      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/mime-types": {
      "version": "2.1.35",
      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
      "license": "MIT",
      "dependencies": {
        "mime-db": "1.52.0"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/min-indent": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
      "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/minimatch": {
@@ -5149,12 +6449,68 @@
      "dev": true,
      "license": "MIT"
    },
    "node_modules/msw": {
      "version": "2.12.1",
      "resolved": "https://registry.npmjs.org/msw/-/msw-2.12.1.tgz",
      "integrity": "sha512-arzsi9IZjjByiEw21gSUP82qHM8zkV69nNpWV6W4z72KiLvsDWoOp678ORV6cNfU/JGhlX0SsnD4oXo9gI6I2A==",
      "dev": true,
      "hasInstallScript": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@inquirer/confirm": "^5.0.0",
        "@mswjs/interceptors": "^0.40.0",
        "@open-draft/deferred-promise": "^2.2.0",
        "@types/statuses": "^2.0.4",
        "cookie": "^1.0.2",
        "graphql": "^16.8.1",
        "headers-polyfill": "^4.0.2",
        "is-node-process": "^1.2.0",
        "outvariant": "^1.4.3",
        "path-to-regexp": "^6.3.0",
        "picocolors": "^1.1.1",
        "rettime": "^0.7.0",
        "statuses": "^2.0.2",
        "strict-event-emitter": "^0.5.1",
        "tough-cookie": "^6.0.0",
        "type-fest": "^4.26.1",
        "until-async": "^3.0.2",
        "yargs": "^17.7.2"
      },
      "bin": {
        "msw": "cli/index.js"
      },
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "url": "https://github.com/sponsors/mswjs"
      },
      "peerDependencies": {
        "typescript": ">= 4.8.x"
      },
      "peerDependenciesMeta": {
        "typescript": {
          "optional": true
        }
      }
    },
    "node_modules/muggle-string": {
      "version": "0.4.1",
      "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
      "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/mute-stream": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz",
      "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==",
      "dev": true,
      "license": "ISC",
      "engines": {
        "node": "^20.17.0 || >=22.9.0"
      }
    },
    "node_modules/nanoid": {
      "version": "3.3.11",
@@ -5306,6 +6662,67 @@
        "url": "https://github.com/fb55/nth-check?sponsor=1"
      }
    },
    "node_modules/object-inspect": {
      "version": "1.13.4",
      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/object-is": {
      "version": "1.1.6",
      "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
      "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "call-bind": "^1.0.7",
        "define-properties": "^1.2.1"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/object-keys": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/object.assign": {
      "version": "4.1.7",
      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
      "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "call-bind": "^1.0.8",
        "call-bound": "^1.0.3",
        "define-properties": "^1.2.1",
        "es-object-atoms": "^1.0.0",
        "has-symbols": "^1.1.0",
        "object-keys": "^1.1.1"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/ohash": {
      "version": "2.0.11",
      "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
@@ -5349,6 +6766,13 @@
      "engines": {
        "node": ">= 0.8.0"
      }
    },
    "node_modules/outvariant": {
      "version": "1.4.3",
      "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz",
      "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/p-limit": {
      "version": "3.1.0",
@@ -5479,6 +6903,13 @@
      "dev": true,
      "license": "ISC"
    },
    "node_modules/path-to-regexp": {
      "version": "6.3.0",
      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
      "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/pathe": {
      "version": "2.0.3",
      "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@@ -5539,6 +6970,7 @@
      "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
      "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@vue/devtools-api": "^7.7.7"
      },
@@ -5585,6 +7017,16 @@
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/possible-typed-array-names": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
      "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/postcss": {
@@ -5748,12 +7190,56 @@
        }
      }
    },
    "node_modules/pretty-format": {
      "version": "27.5.1",
      "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
      "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "ansi-regex": "^5.0.1",
        "ansi-styles": "^5.0.0",
        "react-is": "^17.0.1"
      },
      "engines": {
        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
      }
    },
    "node_modules/pretty-format/node_modules/ansi-regex": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/pretty-format/node_modules/ansi-styles": {
      "version": "5.2.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
      "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/proto-list": {
      "version": "1.2.4",
      "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
      "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
      "dev": true,
      "license": "ISC"
    },
    "node_modules/proxy-from-env": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
      "license": "MIT"
    },
    "node_modules/punycode": {
      "version": "2.3.1",
@@ -5786,6 +7272,13 @@
      ],
      "license": "MIT"
    },
    "node_modules/react-is": {
      "version": "17.0.2",
      "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
      "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/read-package-json-fast": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz",
@@ -5798,6 +7291,51 @@
      },
      "engines": {
        "node": "^18.17.0 || >=20.5.0"
      }
    },
    "node_modules/redent": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
      "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "indent-string": "^4.0.0",
        "strip-indent": "^3.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/regexp.prototype.flags": {
      "version": "1.5.4",
      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
      "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "call-bind": "^1.0.8",
        "define-properties": "^1.2.1",
        "es-errors": "^1.3.0",
        "get-proto": "^1.0.1",
        "gopd": "^1.2.0",
        "set-function-name": "^2.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/require-directory": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/require-from-string": {
@@ -5819,6 +7357,13 @@
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/rettime": {
      "version": "0.7.0",
      "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.7.0.tgz",
      "integrity": "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/reusify": {
      "version": "1.1.0",
@@ -5915,6 +7460,24 @@
        "queue-microtask": "^1.2.2"
      }
    },
    "node_modules/safe-regex-test": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
      "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "es-errors": "^1.3.0",
        "is-regex": "^1.2.1"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/safer-buffer": {
      "version": "2.1.2",
      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -5943,6 +7506,40 @@
      "license": "ISC",
      "bin": {
        "semver": "bin/semver.js"
      }
    },
    "node_modules/set-function-length": {
      "version": "1.2.2",
      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
      "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "define-data-property": "^1.1.4",
        "es-errors": "^1.3.0",
        "function-bind": "^1.1.2",
        "get-intrinsic": "^1.2.4",
        "gopd": "^1.0.1",
        "has-property-descriptors": "^1.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/set-function-name": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
      "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "define-data-property": "^1.1.4",
        "es-errors": "^1.3.0",
        "functions-have-names": "^1.2.3",
        "has-property-descriptors": "^1.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/shebang-command": {
@@ -5974,6 +7571,82 @@
      "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
      "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "object-inspect": "^1.13.3",
        "side-channel-list": "^1.0.0",
        "side-channel-map": "^1.0.1",
        "side-channel-weakmap": "^1.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-list": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
      "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "object-inspect": "^1.13.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-map": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.5",
        "object-inspect": "^1.13.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-weakmap": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.5",
        "object-inspect": "^1.13.3",
        "side-channel-map": "^1.0.1"
      },
      "engines": {
        "node": ">= 0.4"
      },
@@ -6041,10 +7714,41 @@
      "dev": true,
      "license": "MIT"
    },
    "node_modules/statuses": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
      "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/std-env": {
      "version": "3.10.0",
      "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
      "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/stop-iteration-iterator": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
      "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "internal-slot": "^1.1.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/strict-event-emitter": {
      "version": "0.5.1",
      "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz",
      "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==",
      "dev": true,
      "license": "MIT"
    },
@@ -6148,6 +7852,19 @@
      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/strip-indent": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
      "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "min-indent": "^1.0.0"
      },
      "engines": {
        "node": ">=8"
      }
@@ -6447,6 +8164,19 @@
        "node": ">= 0.8.0"
      }
    },
    "node_modules/type-fest": {
      "version": "4.41.0",
      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
      "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
      "dev": true,
      "license": "(MIT OR CC0-1.0)",
      "engines": {
        "node": ">=16"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/typescript": {
      "version": "5.9.3",
      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -6521,6 +8251,16 @@
      },
      "funding": {
        "url": "https://github.com/sponsors/jonschlinkert"
      }
    },
    "node_modules/until-async": {
      "version": "3.0.2",
      "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz",
      "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==",
      "dev": true,
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/kettanaito"
      }
    },
    "node_modules/update-browserslist-db": {
@@ -7154,6 +8894,67 @@
        "node": ">= 8"
      }
    },
    "node_modules/which-boxed-primitive": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
      "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "is-bigint": "^1.1.0",
        "is-boolean-object": "^1.2.1",
        "is-number-object": "^1.1.1",
        "is-string": "^1.1.1",
        "is-symbol": "^1.1.1"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/which-collection": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
      "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "is-map": "^2.0.3",
        "is-set": "^2.0.3",
        "is-weakmap": "^2.0.2",
        "is-weakset": "^2.0.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/which-typed-array": {
      "version": "1.1.19",
      "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
      "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "available-typed-arrays": "^1.0.7",
        "call-bind": "^1.0.8",
        "call-bound": "^1.0.4",
        "for-each": "^0.3.5",
        "get-proto": "^1.0.1",
        "gopd": "^1.2.0",
        "has-tostringtag": "^1.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/why-is-node-running": {
      "version": "2.3.0",
      "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
@@ -7331,12 +9132,96 @@
      "dev": true,
      "license": "MIT"
    },
    "node_modules/y18n": {
      "version": "5.0.8",
      "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
      "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
      "dev": true,
      "license": "ISC",
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/yallist": {
      "version": "3.1.1",
      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
      "dev": true,
      "license": "ISC"
    },
    "node_modules/yargs": {
      "version": "17.7.2",
      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
      "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "cliui": "^8.0.1",
        "escalade": "^3.1.1",
        "get-caller-file": "^2.0.5",
        "require-directory": "^2.1.1",
        "string-width": "^4.2.3",
        "y18n": "^5.0.5",
        "yargs-parser": "^21.1.1"
      },
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/yargs-parser": {
      "version": "21.1.1",
      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
      "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
      "dev": true,
      "license": "ISC",
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/yargs/node_modules/ansi-regex": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/yargs/node_modules/emoji-regex": {
      "version": "8.0.0",
      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/yargs/node_modules/string-width": {
      "version": "4.2.3",
      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "emoji-regex": "^8.0.0",
        "is-fullwidth-code-point": "^3.0.0",
        "strip-ansi": "^6.0.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/yargs/node_modules/strip-ansi": {
      "version": "6.0.1",
      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "ansi-regex": "^5.0.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/yocto-queue": {
      "version": "0.1.0",
@@ -7350,6 +9235,19 @@
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/yoctocolors-cjs": {
      "version": "2.1.3",
      "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz",
      "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    }
  }
}
package.json
@@ -27,7 +27,11 @@
    "vue-router": "^4.6.3"
  },
  "devDependencies": {
    "@pinia/testing": "^1.0.3",
    "@playwright/test": "^1.56.1",
    "@testing-library/jest-dom": "^6.9.1",
    "@testing-library/user-event": "^14.6.1",
    "@testing-library/vue": "^8.1.0",
    "@tsconfig/node22": "^22.0.2",
    "@types/jsdom": "^27.0.0",
    "@types/node": "^22.18.11",
@@ -43,6 +47,7 @@
    "eslint-plugin-vue": "~10.5.0",
    "jiti": "^2.6.1",
    "jsdom": "^27.0.1",
    "msw": "^2.12.1",
    "npm-run-all2": "^8.0.4",
    "prettier": "^3.6.2",
    "prettier-plugin-tailwindcss": "^0.7.1",
@@ -51,5 +56,10 @@
    "vite-plugin-vue-devtools": "^8.0.3",
    "vitest": "^3.2.4",
    "vue-tsc": "^3.1.1"
  },
  "msw": {
    "workerDirectory": [
      "public"
    ]
  }
}
public/mockServiceWorker.js
比對新檔案
@@ -0,0 +1,349 @@
/* eslint-disable */
/* tslint:disable */
/**
 * Mock Service Worker.
 * @see https://github.com/mswjs/msw
 * - Please do NOT modify this file.
 */
const PACKAGE_VERSION = '2.12.1'
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
addEventListener('install', function () {
  self.skipWaiting()
})
addEventListener('activate', function (event) {
  event.waitUntil(self.clients.claim())
})
addEventListener('message', async function (event) {
  const clientId = Reflect.get(event.source || {}, 'id')
  if (!clientId || !self.clients) {
    return
  }
  const client = await self.clients.get(clientId)
  if (!client) {
    return
  }
  const allClients = await self.clients.matchAll({
    type: 'window',
  })
  switch (event.data) {
    case 'KEEPALIVE_REQUEST': {
      sendToClient(client, {
        type: 'KEEPALIVE_RESPONSE',
      })
      break
    }
    case 'INTEGRITY_CHECK_REQUEST': {
      sendToClient(client, {
        type: 'INTEGRITY_CHECK_RESPONSE',
        payload: {
          packageVersion: PACKAGE_VERSION,
          checksum: INTEGRITY_CHECKSUM,
        },
      })
      break
    }
    case 'MOCK_ACTIVATE': {
      activeClientIds.add(clientId)
      sendToClient(client, {
        type: 'MOCKING_ENABLED',
        payload: {
          client: {
            id: client.id,
            frameType: client.frameType,
          },
        },
      })
      break
    }
    case 'CLIENT_CLOSED': {
      activeClientIds.delete(clientId)
      const remainingClients = allClients.filter((client) => {
        return client.id !== clientId
      })
      // Unregister itself when there are no more clients
      if (remainingClients.length === 0) {
        self.registration.unregister()
      }
      break
    }
  }
})
addEventListener('fetch', function (event) {
  const requestInterceptedAt = Date.now()
  // Bypass navigation requests.
  if (event.request.mode === 'navigate') {
    return
  }
  // Opening the DevTools triggers the "only-if-cached" request
  // that cannot be handled by the worker. Bypass such requests.
  if (
    event.request.cache === 'only-if-cached' &&
    event.request.mode !== 'same-origin'
  ) {
    return
  }
  // Bypass all requests when there are no active clients.
  // Prevents the self-unregistered worked from handling requests
  // after it's been terminated (still remains active until the next reload).
  if (activeClientIds.size === 0) {
    return
  }
  const requestId = crypto.randomUUID()
  event.respondWith(handleRequest(event, requestId, requestInterceptedAt))
})
/**
 * @param {FetchEvent} event
 * @param {string} requestId
 * @param {number} requestInterceptedAt
 */
async function handleRequest(event, requestId, requestInterceptedAt) {
  const client = await resolveMainClient(event)
  const requestCloneForEvents = event.request.clone()
  const response = await getResponse(
    event,
    client,
    requestId,
    requestInterceptedAt,
  )
  // Send back the response clone for the "response:*" life-cycle events.
  // Ensure MSW is active and ready to handle the message, otherwise
  // this message will pend indefinitely.
  if (client && activeClientIds.has(client.id)) {
    const serializedRequest = await serializeRequest(requestCloneForEvents)
    // Clone the response so both the client and the library could consume it.
    const responseClone = response.clone()
    sendToClient(
      client,
      {
        type: 'RESPONSE',
        payload: {
          isMockedResponse: IS_MOCKED_RESPONSE in response,
          request: {
            id: requestId,
            ...serializedRequest,
          },
          response: {
            type: responseClone.type,
            status: responseClone.status,
            statusText: responseClone.statusText,
            headers: Object.fromEntries(responseClone.headers.entries()),
            body: responseClone.body,
          },
        },
      },
      responseClone.body ? [serializedRequest.body, responseClone.body] : [],
    )
  }
  return response
}
/**
 * Resolve the main client for the given event.
 * Client that issues a request doesn't necessarily equal the client
 * that registered the worker. It's with the latter the worker should
 * communicate with during the response resolving phase.
 * @param {FetchEvent} event
 * @returns {Promise<Client | undefined>}
 */
async function resolveMainClient(event) {
  const client = await self.clients.get(event.clientId)
  if (activeClientIds.has(event.clientId)) {
    return client
  }
  if (client?.frameType === 'top-level') {
    return client
  }
  const allClients = await self.clients.matchAll({
    type: 'window',
  })
  return allClients
    .filter((client) => {
      // Get only those clients that are currently visible.
      return client.visibilityState === 'visible'
    })
    .find((client) => {
      // Find the client ID that's recorded in the
      // set of clients that have registered the worker.
      return activeClientIds.has(client.id)
    })
}
/**
 * @param {FetchEvent} event
 * @param {Client | undefined} client
 * @param {string} requestId
 * @param {number} requestInterceptedAt
 * @returns {Promise<Response>}
 */
async function getResponse(event, client, requestId, requestInterceptedAt) {
  // Clone the request because it might've been already used
  // (i.e. its body has been read and sent to the client).
  const requestClone = event.request.clone()
  function passthrough() {
    // Cast the request headers to a new Headers instance
    // so the headers can be manipulated with.
    const headers = new Headers(requestClone.headers)
    // Remove the "accept" header value that marked this request as passthrough.
    // This prevents request alteration and also keeps it compliant with the
    // user-defined CORS policies.
    const acceptHeader = headers.get('accept')
    if (acceptHeader) {
      const values = acceptHeader.split(',').map((value) => value.trim())
      const filteredValues = values.filter(
        (value) => value !== 'msw/passthrough',
      )
      if (filteredValues.length > 0) {
        headers.set('accept', filteredValues.join(', '))
      } else {
        headers.delete('accept')
      }
    }
    return fetch(requestClone, { headers })
  }
  // Bypass mocking when the client is not active.
  if (!client) {
    return passthrough()
  }
  // Bypass initial page load requests (i.e. static assets).
  // The absence of the immediate/parent client in the map of the active clients
  // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
  // and is not ready to handle requests.
  if (!activeClientIds.has(client.id)) {
    return passthrough()
  }
  // Notify the client that a request has been intercepted.
  const serializedRequest = await serializeRequest(event.request)
  const clientMessage = await sendToClient(
    client,
    {
      type: 'REQUEST',
      payload: {
        id: requestId,
        interceptedAt: requestInterceptedAt,
        ...serializedRequest,
      },
    },
    [serializedRequest.body],
  )
  switch (clientMessage.type) {
    case 'MOCK_RESPONSE': {
      return respondWithMock(clientMessage.data)
    }
    case 'PASSTHROUGH': {
      return passthrough()
    }
  }
  return passthrough()
}
/**
 * @param {Client} client
 * @param {any} message
 * @param {Array<Transferable>} transferrables
 * @returns {Promise<any>}
 */
function sendToClient(client, message, transferrables = []) {
  return new Promise((resolve, reject) => {
    const channel = new MessageChannel()
    channel.port1.onmessage = (event) => {
      if (event.data && event.data.error) {
        return reject(event.data.error)
      }
      resolve(event.data)
    }
    client.postMessage(message, [
      channel.port2,
      ...transferrables.filter(Boolean),
    ])
  })
}
/**
 * @param {Response} response
 * @returns {Response}
 */
function respondWithMock(response) {
  // Setting response status code to 0 is a no-op.
  // However, when responding with a "Response.error()", the produced Response
  // instance will have status code set to 0. Since it's not possible to create
  // a Response instance with status code 0, handle that use-case separately.
  if (response.status === 0) {
    return Response.error()
  }
  const mockedResponse = new Response(response.body, response)
  Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
    value: true,
    enumerable: true,
  })
  return mockedResponse
}
/**
 * @param {Request} request
 */
async function serializeRequest(request) {
  return {
    url: request.url,
    mode: request.mode,
    method: request.method,
    headers: Object.fromEntries(request.headers.entries()),
    cache: request.cache,
    credentials: request.credentials,
    destination: request.destination,
    integrity: request.integrity,
    redirect: request.redirect,
    referrer: request.referrer,
    referrerPolicy: request.referrerPolicy,
    body: await request.arrayBuffer(),
    keepalive: request.keepalive,
  }
}
setupTests.ts
比對新檔案
@@ -0,0 +1,21 @@
import { beforeAll, afterAll, beforeEach, afterEach } from 'vitest'
import { cleanup } from '@testing-library/vue'
import { server } from './src/mocks/node'
import '@testing-library/jest-dom/vitest'
// Start the server before all tests
beforeAll(() => {
  server.listen({ onUnhandledRequest: 'error' })
})
beforeEach(() => {
  server.resetHandlers()
})
// Clean up after the tests are finished
afterAll(() => {
  server.close()
})
afterEach(() => {
  cleanup()
})
src/App.vue
@@ -6,6 +6,8 @@
    Visit <a href="https://vuejs.org/" target="_blank" rel="noopener">vuejs.org</a> to read the
    documentation
  </p>
  <hr />
  <router-view></router-view>
</template>
<style scoped></style>
src/__tests__/App.spec.ts
@@ -1,11 +1,15 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import App from '../App.vue'
import router from '../router'
describe('App', () => {
  it('mounts renders properly', () => {
    const wrapper = mount(App)
    const wrapper = mount(App, {
      global: {
        plugins: [router],
      },
    })
    expect(wrapper.text()).toContain('You did it!')
  })
})
src/features/example/api.ts
比對新檔案
@@ -0,0 +1,12 @@
import { defineRequest, API_MODE } from '@/hooks/useRequest'
const mode = API_MODE.TEST
export function exampleRequest(data: Record<string, unknown>) {
  return defineRequest<{ result: string }>({
    url: '/example_url',
    method: 'POST',
    mode,
    data,
  })
}
src/features/example/index.spec.ts
比對新檔案
@@ -0,0 +1,176 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, waitFor } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import { createTestingPinia } from '@pinia/testing'
import { http, HttpResponse, delay } from 'msw'
import { server } from '@/mocks/node'
import ExamplePage from './index.vue'
const BASE_API = import.meta.env.VITE_BASE_API
/**
 * ExamplePage 元件測試
 *
 * 測試策略:
 * - 使用 Testing Library 從使用者角度測試
 * - 每個測試建立獨立的 Pinia store 實例,避免狀態汙染
 * - 使用 MSW 模擬 API 回應,並可透過 server.use() 客製化延遲時間
 * - 專注測試使用者可見的行為,而非實作細節
 */
describe('ExamplePage', () => {
  beforeEach(() => {
    // Arrange - 客製化 MSW handler,加入 1 秒延遲模擬真實 API 請求
    server.use(
      http.post(`${BASE_API}/example_url`, async () => {
        await delay(1000)
        return HttpResponse.json({
          data: 'hello world',
          msg: 'success',
          code: '200',
          sysDate: new Date().toISOString(),
        })
      }),
    )
  })
  describe('初始渲染', () => {
    it('should display page title on mount', () => {
      // Arrange
      const testStore = createTestingPinia({ createSpy: vi.fn })
      // Act
      render(ExamplePage, {
        global: { plugins: [testStore] },
      })
      // Assert
      expect(screen.getAllByText('I am Example Page').length).toBeGreaterThan(0)
    })
    it('should display call request button with correct text', () => {
      // Arrange
      const testStore = createTestingPinia({ createSpy: vi.fn })
      // Act
      render(ExamplePage, {
        global: { plugins: [testStore] },
      })
      // Assert
      expect(
        screen.getByRole('button', { name: /click to call example request/i }),
      ).toBeInTheDocument()
    })
    it('should not show loading or response messages before interaction', () => {
      // Arrange
      const testStore = createTestingPinia({ createSpy: vi.fn })
      // Act
      render(ExamplePage, {
        global: { plugins: [testStore] },
      })
      // Assert
      expect(screen.queryByText('fetching...')).not.toBeInTheDocument()
      expect(screen.queryByText('the mock response is:')).not.toBeInTheDocument()
    })
  })
  describe('當使用者點擊請求按鈕', () => {
    it('should display successful API response data after request completes', async () => {
      // Arrange
      const testStore = createTestingPinia({ createSpy: vi.fn })
      render(ExamplePage, {
        global: { plugins: [testStore] },
      })
      const user = userEvent.setup()
      // Act
      const button = screen.getByRole('button', { name: /click to call example request/i })
      await user.click(button)
      await waitFor(
        async () => {
          // Assert - 等待載入完成並顯示結果
          expect(await screen.findByText('the mock response is:')).toBeInTheDocument()
        },
        { interval: 1000, timeout: 2000 },
      )
    })
    it('should display JSON formatted response data', async () => {
      // Arrange
      const testStore = createTestingPinia({ createSpy: vi.fn })
      render(ExamplePage, {
        global: { plugins: [testStore] },
      })
      const user = userEvent.setup()
      // Act
      const button = screen.getByRole('button', { name: /click to call example request/i })
      await user.click(button)
      // Assert - 驗證回應是 JSON 格式
      await waitFor(
        () => {
          const responseElement = screen.getByText(/"hello world"/i)
          expect(responseElement).toBeInTheDocument()
        },
        { interval: 1000, timeout: 2000 },
      )
    })
  })
  /**
   * 測試請求進行中的狀態
   *
   * 使用 server.use() 臨時覆蓋預設的 MSW handler,
   * 加入 1 秒延遲來模擬真實的網路請求情況。
   * 這讓我們能夠捕捉到 loading 狀態的變化。
   */
  describe('請求進行中的狀態', () => {
    it('should show loading indicator immediately after button click', async () => {
      // Arrange
      const testStore = createTestingPinia({ createSpy: vi.fn })
      render(ExamplePage, {
        global: { plugins: [testStore] },
      })
      const user = userEvent.setup()
      // Act - 點擊按鈕觸發請求
      const button = screen.getByRole('button', { name: /click to call example request/i })
      await user.click(button)
      // Assert - 驗證載入狀態出現
      expect(await screen.findByText('fetching...')).toBeInTheDocument()
      expect(screen.queryByText('the mock response is:')).not.toBeInTheDocument()
    })
    it('should transition from loading to completed state correctly', async () => {
      // Arrange
      const testStore = createTestingPinia({ createSpy: vi.fn })
      render(ExamplePage, {
        global: { plugins: [testStore] },
      })
      const user = userEvent.setup()
      // Act - 觸發請求
      const button = screen.getByRole('button', { name: /click to call example request/i })
      await user.click(button)
      // Assert - 驗證 loading 狀態
      expect(await screen.findByText('fetching...')).toBeInTheDocument()
      expect(screen.queryByText('the mock response is:')).not.toBeInTheDocument()
      // Assert - 等待並驗證完成狀態
      await waitFor(
        () => {
          expect(screen.queryByText('fetching...')).not.toBeInTheDocument()
          expect(screen.getByText('the mock response is:')).toBeInTheDocument()
          expect(screen.getByText(/"hello world"/i)).toBeInTheDocument()
        },
        { interval: 1000, timeout: 2000 }, // 給予足夠的時間等待延遲完成
      )
    })
  })
})
src/features/example/index.vue
比對新檔案
@@ -0,0 +1,55 @@
<script setup lang="ts">
import { ref } from 'vue'
import useRequest from '@/hooks/useRequest'
import { exampleRequest } from './api'
import cn from '@/utils/cn'
const { apiRequest } = useRequest()
const text = ref('')
const loading = ref(false)
const done = ref(false)
async function handleRequest() {
  loading.value = true
  done.value = false
  text.value = ''
  try {
    const { success, data } = await apiRequest(exampleRequest({}))
    if (success) {
      text.value = JSON.stringify(data)
    } else {
      text.value = 'fetch failed'
    }
  } catch (e) {
    text.value = e as string
  } finally {
    loading.value = false
    done.value = true
  }
}
</script>
<template>
  <div>I am Example Page</div>
  <div class="m-2 text-lg">
    I am Example Page
    <button
      :class="cn('block rounded border bg-blue-100 p-2 hover:outline hover:outline-blue-500')"
      @click="handleRequest"
    >
      click to call example request
    </button>
    <div
      :class="
        cn('flex w-1/2 flex-col', {
          'text-green-500': done,
          'text-blue-500': loading,
        })
      "
    >
      <p v-if="loading">fetching...</p>
      <p v-if="done">the mock response is:</p>
      <p>{{ text }}</p>
    </div>
  </div>
</template>
src/main.ts
@@ -1,10 +1,14 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { worker } from '@/mocks/browser.js'
import App from './App.vue'
import router from './router'
import '@/assets/css/main.css'
if (import.meta.env.VITE_NODE_ENV === 'development') {
  await worker.start({ quiet: true, onUnhandledRequest: 'bypass' })
}
const app = createApp(App)
app.use(createPinia())
src/mocks/browser.ts
比對新檔案
@@ -0,0 +1,4 @@
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'
export const worker = setupWorker(...handlers)
src/mocks/data/example.ts
比對新檔案
@@ -0,0 +1,10 @@
const exampleMocks = {
  '/example_url': {
    data: 'hello world',
    msg: 'success',
    code: '200',
    sysDate: new Date().toISOString(),
  },
}
export default exampleMocks
src/mocks/data/index.ts
比對新檔案
@@ -0,0 +1,7 @@
import exampleMocks from './example'
const mockJsons = {
  ...exampleMocks,
}
export default mockJsons
src/mocks/handlers.ts
比對新檔案
@@ -0,0 +1,21 @@
import { http, passthrough, HttpResponse } from 'msw'
import mockJsons from './data'
import type { HttpResponseResolver } from 'msw'
const BASE_API = import.meta.env.VITE_BASE_API
const baseResolver: HttpResponseResolver = async ({ request }) => {
  if (!request.url.includes('mode=test')) {
    return passthrough()
  }
  const apiDomain = BASE_API || /(.*localhost:\d+)/
  /** 不包含主網域及參數的路由 */
  const pathName = request.url
    .replace(apiDomain, '')
    .replace(/\?(.*)/, '') as keyof typeof mockJsons
  const mockData = mockJsons[pathName]
  return HttpResponse.json(mockData)
}
export const handlers = [http.post(`${BASE_API}/example_url`, baseResolver)]
src/mocks/node.ts
比對新檔案
@@ -0,0 +1,4 @@
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)
src/router/index.ts
@@ -2,7 +2,16 @@
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [],
  routes: [
    {
      path: '/',
      redirect: '/example',
    },
    {
      path: '/example',
      component: () => import('@/features/example/index.vue'),
    },
  ],
})
export default router
tsconfig.app.json
@@ -4,7 +4,7 @@
  "exclude": ["src/**/__tests__/*"],
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    "types": ["@testing-library/jest-dom"],
    "paths": {
      "@/*": ["./src/*"]
    }
vitest.config.ts
@@ -9,6 +9,7 @@
      environment: 'jsdom',
      exclude: [...configDefaults.exclude, 'e2e/**'],
      root: fileURLToPath(new URL('./', import.meta.url)),
      setupFiles: ['./setupTests.ts'],
    },
  }),
)