---
name: E2E 端到端測試規範 (E2E Testing with Playwright)
description: 規範 Playwright 端到端測試的撰寫慣例、目錄結構、共用工具與執行方式，確保所有 E2E 測試保持一致性與可維護性。
---

# E2E 端到端測試規範 (E2E Testing with Playwright)

本技能定義了 Star ERP 系統中端到端 (E2E) 測試的實作標準，使用 Playwright 模擬真實使用者操作瀏覽器，驗證 UI 顯示與功能流程的正確性。

---

## 1. 專案結構

### 1.1 目錄配置

```
star-erp/
├── playwright.config.ts       # Playwright 設定檔
├── e2e/                        # E2E 測試根目錄
│   ├── helpers/                # 共用工具函式
│   │   └── auth.ts             # 登入 helper
│   ├── screenshots/            # 測試截圖存放
│   ├── auth.spec.ts            # 認證相關測試（登入、登出）
│   ├── inventory.spec.ts       # 庫存模組測試
│   ├── products.spec.ts        # 商品模組測試
│   └── {module}.spec.ts        # 依模組命名
├── playwright-report/          # HTML 測試報告（自動產生，已 gitignore）
└── test-results/               # 失敗截圖與錄影（自動產生，已 gitignore）
```

### 1.2 命名規範

| 項目 | 規範 | 範例 |
|---|---|---|
| 測試檔案 | 小寫，依模組命名 `.spec.ts` | `inventory.spec.ts` |
| 測試群組 | `test.describe('中文功能名稱')` | `test.describe('庫存查詢')` |
| 測試案例 | 中文描述「**應**」開頭 | `test('應顯示庫存清單')` |
| 截圖檔案 | `{module}-{scenario}.png` | `inventory-search-result.png` |

---

## 2. 設定檔 (playwright.config.ts)

### 2.1 核心設定

```typescript
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:8081',    // Sail 開發伺服器
    screenshot: 'only-on-failure',       // 失敗時自動截圖
    video: 'retain-on-failure',          // 失敗時保留錄影
    trace: 'on-first-retry',            // 重試時收集 trace
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
});
```

### 2.2 重要注意事項

> [!IMPORTANT]
> `baseURL` 必須指向本機 Sail 開發伺服器（預設 `http://localhost:8081`）。
> 確保測試前已執行 `./vendor/bin/sail up -d` 與 `./vendor/bin/sail npm run dev`。

---

## 3. 共用工具 (Helpers)

### 3.1 登入 Helper

位置：`e2e/helpers/auth.ts`

```typescript
import { Page } from '@playwright/test';

/**
 * 共用登入函式
 * 使用測試帳號登入 ERP 系統
 */
export async function login(page: Page, username = 'mama', password = 'mama9453') {
  await page.goto('/');
  await page.fill('#username', username);
  await page.fill('#password', password);
  await page.getByRole('button', { name: '登入系統' }).click();
  // 等待儀表板載入完成
  await page.waitForSelector('text=系統概況', { timeout: 10000 });
}
```

### 3.2 使用方式

```typescript
import { login } from './helpers/auth';

test('應顯示庫存清單', async ({ page }) => {
  await login(page);
  await page.goto('/inventory/stock-query');
  // ...斷言
});
```

---

## 4. 測試撰寫規範

### 4.1 測試結構模板

```typescript
import { test, expect } from '@playwright/test';
import { login } from './helpers/auth';

test.describe('模組功能名稱', () => {

  // 若整個 describe 都需要登入，使用 beforeEach
  test.beforeEach(async ({ page }) => {
    await login(page);
  });

  test('應正確顯示頁面標題與關鍵元素', async ({ page }) => {
    await page.goto('/target-page');

    // 驗證頁面標題
    await expect(page.getByText('頁面標題')).toBeVisible();

    // 驗證表格存在
    await expect(page.locator('table')).toBeVisible();
  });

  test('應能執行 CRUD 操作', async ({ page }) => {
    // ...
  });
});
```

### 4.2 斷言 (Assertions) 慣例

| 場景 | 優先使用 | 避免使用 |
|---|---|---|
| 驗證頁面載入 | `page.getByText('關鍵文字')` | `page.waitForURL()` ※ |
| 驗證元素存在 | `expect(locator).toBeVisible()` | `.count() > 0` |
| 驗證表格資料 | `page.locator('table tbody tr')` | 硬編碼行數 |
| 等待操作完成 | `expect().toBeVisible({ timeout })` | `page.waitForTimeout()` |

> [!NOTE]
> ※ Star ERP 使用 Inertia.js，頁面導航不一定改變 URL（例如儀表板路由為 `/`）。
> 因此**優先使用頁面內容驗證**，而非依賴 URL 變化。

### 4.3 選擇器優先順序

依照 Playwright 官方建議，選擇器優先順序為：

1. **Role** — `page.getByRole('button', { name: '登入系統' })`
2. **Text** — `page.getByText('系統概況')`
3. **Label** — `page.getByLabel('帳號')`
4. **Placeholder** — `page.getByPlaceholder('請輸入...')`
5. **Test ID** — `page.getByTestId('submit-btn')`（需在元件加 `data-testid`）
6. **CSS** — `page.locator('#username')`（最後手段）

### 4.4 禁止事項

```typescript
// ❌ 禁止：硬等待（不可預期的等待時間）
await page.waitForTimeout(5000);

// ✅ 正確：等待特定條件
await expect(page.getByText('操作成功')).toBeVisible({ timeout: 5000 });

// ❌ 禁止：在測試中寫死測試資料的 ID
await page.goto('/products/42/edit');

// ✅ 正確：從頁面互動導航
await page.locator('table tbody tr').first().getByRole('button', { name: '編輯' }).click();
```

---

## 5. 截圖與視覺回歸

### 5.1 手動截圖（文件用途）

```typescript
// 成功截圖存於 e2e/screenshots/
await page.screenshot({
  path: 'e2e/screenshots/inventory-list.png',
  fullPage: true,
});
```

### 5.2 視覺回歸測試（偵測 UI 變化）

```typescript
test('庫存頁面 UI 應保持一致', async ({ page }) => {
  await login(page);
  await page.goto('/inventory/stock-query');
  // 比對截圖，pixel 級差異會報錯
  await expect(page).toHaveScreenshot('stock-query.png', {
    maxDiffPixelRatio: 0.01,  // 容許 1% 差異（動態資料）
  });
});
```

> [!NOTE]
> 首次執行 `toHaveScreenshot()` 會自動建立基準截圖。
> 後續執行會與基準比對，更新基準用：`npx playwright test --update-snapshots`

---

## 6. 執行指令速查

```bash
# 執行所有 E2E 測試
npx playwright test

# 執行特定模組測試
npx playwright test e2e/login.spec.ts

# UI 互動模式（可視化瀏覽器操作）
npx playwright test --ui

# 帶頭模式（顯示瀏覽器畫面）
npx playwright test --headed

# 產生 HTML 報告並開啟
npx playwright test --reporter=html
npx playwright show-report

# 更新視覺回歸基準截圖
npx playwright test --update-snapshots

# 只執行特定測試案例（用 -g 篩選名稱）
npx playwright test -g "登入"

# Debug 模式（逐步執行）
npx playwright test --debug
```

---

## 7. 開發檢核清單 (Checklist)

### 新增頁面或功能時：

- [ ] 是否已為新頁面建立對應的 `.spec.ts` 測試檔？
- [ ] 測試是否覆蓋主要的 Happy Path（正常操作流程）？
- [ ] 測試是否覆蓋關鍵的 Error Path（錯誤處理）？
- [ ] 共用的登入步驟是否使用 `helpers/auth.ts`？
- [ ] 斷言是否優先使用頁面內容而非 URL？
- [ ] 選擇器是否遵循優先順序（Role > Text > Label > CSS）？
- [ ] 測試是否可獨立執行（不依賴其他測試的狀態）？

### 提交程式碼前：

- [ ] 全部 E2E 測試是否通過？（`npx playwright test`）
- [ ] 是否有遺留的 `test.only` 或 `test.skip`？
