Skip to main content

elf-e2e

Elf Express E2E 端對端測試規範(Playwright + 已安裝的 Chrome):團隊定本 `playwright.config.ts`、 `e2e/` 目錄結構、selector 優先順序、頁面物件模式(POM)、錄影 / 截圖 / trace、retries 與 workers、CI job。 預設以 mock 模式(`VITE_USE_MOCK` 預設開)跑、不需要後端與登入;登入 / 真後端為 opt-in 變體。 當任務涉及 Playwright 設定、撰寫或重構 `e2e/*.spec.ts`、selector 選擇、POM、登入狀態重用、 flaky 測試排查、CI 上跑 E2E、或 jsdom 測不到的焦點 / IME / 版面行為時觸發。

Source facts

Repository
elf-express/skillshare-source
Last source activity
September 27, 2026 at 01:26
Detected SKILL.md language
Chinese
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
2 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
elf-e2e
description
Elf Express E2E 端對端測試規範(Playwright + 已安裝的 Chrome):團隊定本 `playwright.config.ts`、 `e2e/` 目錄結構、selector 優先順序、頁面物件模式(POM)、錄影 / 截圖 / trace、retries 與 workers、CI job。 預設以 mock 模式(`VITE_USE_MOCK` 預設開)跑、不需要後端與登入;登入 / 真後端為 opt-in 變體。 當任務涉及 Playwright 設定、撰寫或重構 `e2e/*.spec.ts`、selector 選擇、POM、登入狀態重用、 flaky 測試排查、CI 上跑 E2E、或 jsdom 測不到的焦點 / IME / 版面行為時觸發。
metadata
{"version":"1.0.0","owner":"Elf Express","upstream":{"name":"Playwright","version":"1.x","docs":"https://playwright.dev/"}}
# Elf Express E2E 測試規範 > 相關 skill:`elf-unit`(邏輯用 Vitest 測)、`elf-integration`(API / DB)、`elf-allure-report`(報告 UI)、 > `elf-vue`(`data-testid` 命名)、`elf-cicd-frontend`(完整 workflow)、`elf-stack`(Node 24.18 / pnpm)。 > > 參考專案:`TypingTrainer` 的 `playwright.config.ts`、`e2e/*.spec.ts`、`.github/workflows/ci.yml`(`e2e` job)。 ## 1. 何時使用 - 新專案導入 Playwright、或修改 `playwright.config.ts` - 撰寫 / 重構 `e2e/*.spec.ts`、建立頁面物件(POM) - 測 jsdom 觀察不到的行為:焦點、IME 組字、捲軸 / 版面是否溢出、真實事件順序、跨頁流程 - 排查 flaky 測試(retries、trace、video) - CI 上執行 E2E、上傳報告 **不要**用 E2E 測可以用 Vitest 測的計算邏輯或 API 回應格式(見 `elf-unit` §2.6)。 ## 2. 固定規則 ### 2.1 位置與命名 1. **MUST** `playwright.config.ts` 放 repo 根目錄;測試放根目錄 `e2e/`,檔名 `<功能>.spec.ts`(kebab-case,例如 `order-checkout.spec.ts`)。 2. **MUST** `.spec.ts` 只用於 E2E;單元測試一律 `.test.ts`(見 `elf-unit`)。 3. **MUST** 目錄結構固定為: ``` e2e/ ├── pages/ ← 頁面物件(一頁一個 class,PascalCase + Page) │ ├── LoginPage.ts │ └── OrderListPage.ts ├── fixtures.ts ← test.extend:注入頁面物件 ├── helpers/ ← 與頁面無關的工具(例如 overflows(el)) ├── order-list.spec.ts └── auth.setup.ts ← 只有 opt-in 的「登入 / 真後端」變體才有(§2.5) playwright/.auth/ ← 登入變體的 storageState 輸出,MUST 加入 .gitignore ``` 4. **MUST** 每個 spec 檔開頭寫 JSDoc:這組測試保護什麼行為、為什麼 jsdom / 單元測試抓不到(參考 `e2e/chinese-ime.spec.ts`)。 5. **MUST** `test.describe('<功能>')` + `test('<預期行為的英文句子>')`。 ### 2.2 Selector 策略(優先順序由高到低) 1. `page.getByTestId('order-submit')` —— **首選**。元件上加 `data-testid="<區塊>-<元素>"`(kebab-case)。 2. `page.getByRole('button', { name: 'Submit' })` —— 無 testid 且有明確無障礙名稱時。 3. `page.getByLabel(...)` / `page.getByPlaceholder(...)` —— 表單欄位。 4. `page.getByText(...)` —— 只用於驗證文字本身,不用來定位要點擊的元素。 5. CSS(`page.locator('.xxx')`)—— 只有在「測的就是該 class 的樣式 / 版面」時使用(例如量測 `.prefs` 是否溢出)。 - **MUST NOT** 使用 XPath、`nth-child`、UI 函式庫產生的 class(`.ant-btn`、`.el-button`)當定位。 參考專案的 `page.locator('button.ant-btn').first()` 是**反例**,新測試不得照抄。 - **MUST NOT** 用 `page.waitForTimeout()` 等待;用 web-first assertion(`await expect(locator).toBeVisible()`)自動等待。 ### 2.3 語系 1. **MUST** 在每個測試明確設定語系(`test.use({ locale: 'en-US' })` 或 `addInitScript` 寫入 app 的語系設定), 並**斷言該語系實際會出現的字串**。 2. **MUST NOT** 斷言「某個其他語系字串不存在」—— 參考專案曾因畫面實際是英文,兩個「中文字不存在」的斷言永遠通過。 ### 2.4 頁面物件模式(POM) 1. **MUST** 一個頁面(或大型區塊)一個 class,放 `e2e/pages/`,建構子收 `Page`,locator 宣告為 `readonly` 屬性。 2. **MUST** 頁面物件提供「動作」方法(`goto()`、`search(keyword)`、`submit()`),**MUST NOT** 在頁面物件內寫 `expect` 斷言 (例外:`goto()` 結尾可 `await expect(this.root).toBeVisible()` 確認頁面載入完成)。 3. **MUST** 頁面物件以 `e2e/fixtures.ts` 的 `test.extend` 注入,spec 從 `./fixtures` import `test` / `expect`,不從 `@playwright/test`。 4. 只在單一 spec 用到的小工具可直接寫在該 spec 內(參考 `settings-layout.spec.ts` 的 `overflows()`);兩個以上 spec 用到就移到 `helpers/`。 ### 2.5 執行模式:預設 mock,登入 / 真後端為 opt-in 1. **MUST** 預設(本機與 CI)以 **mock 模式**跑:前端 `VITE_USE_MOCK` 預設為開(`!== 'false'`,見 `elf-vue` / `elf-api-contract`), `webServer` 只起前端 dev server,**沒有** `setup` project、**沒有** `storageState`、**不需要**後端與 `E2E_USER` / `E2E_PASSWORD`。 預設 mock 資料由 `apps/src/api/mock/fixtures.ts` 提供,斷言以 fixture 內容為準。 2. 需要測「真登入 / 真 API」時才開 **opt-in 登入變體**(§3.1 第二段):`webServer` 同時起後端(`dotnet run`)與前端 (`VITE_USE_MOCK=false`),加 `setup` project(`auth.setup.ts`)登入一次存 `playwright/.auth/user.json`, 其他 project 以 `storageState` 重用;CI 需設定 secrets `E2E_USER` / `E2E_PASSWORD`。 3. **MUST NOT** 在每個測試的 `beforeEach` 走一次 UI 登入。 4. 測試帳密只從環境變數讀(`E2E_USER` / `E2E_PASSWORD`),**MUST NOT** 寫死在程式碼。 5. `page.route` 攔截只在 app 真的發 HTTP 時有作用(`VITE_USE_MOCK=false`);預設 mock 模式下 app 不打 API, 要測錯誤畫面就用登入變體 + `page.route`(範本見 `references/pom-templates.md` §7)。 ### 2.6 平行、retries、紀錄 | 設定 | 值 | 為什麼 | |------|---|--------| | `fullyParallel` | `false` | 測試共用同一個 dev server(與登入變體的後端狀態),先求穩定 | | `workers` | `1` | 同上,避免多 worker 競爭共享狀態 | | `forbidOnly` | CI 時 `true` | 禁止 `.only` 殘留導致漏跑 | | `retries` | CI `2` / 本地 `0` | CI 容忍偶發 flaky,本地暴露真實問題 | | `headless` | `true` | 要看畫面時用 `pnpm test:e2e:ui` 或 `--headed`,不改設定檔 | | `video` | `'on'` | 全錄,故障時資料完整 | | `screenshot` | `'on'` | 全截,搭配 Allure 報告 attach | | `trace` | `'on-first-retry'` | trace 檔大,只在重試時存 | | `actionTimeout` | `30_000` | 30 秒上限,涵蓋網路延遲 | | `projects` | 只有 `chrome`(`channel: 'chrome'`) | 用使用者實際使用的已安裝 Chrome,不下載 Playwright 自帶 Chromium | | `webServer` | `pnpm dev`(mock 模式),CI 不重用既有 server | Playwright 自己起 dev server | | `reporter`(CI) | `github` + `html`(可再加 allure) | `github` 讓失敗顯示在 PR 註解;`html` 讓失敗時上傳的報告不是空的。**MUST NOT** 拿掉 `github` | > **`fullyParallel: true` / `workers > 1`:沒有團隊決議就不開。** 單一 PR 不得自行改動這兩個值; > 需要時先提團隊決議(附 `--repeat-each` 的穩定性證據),決議後再改本定本。 - **MUST** flaky 測試先用 `pnpm exec playwright test <file> --repeat-each=10` 重現,再看 `test-results/**/trace.zip` (`pnpm exec playwright show-trace <path>`)與影片,**MUST NOT** 用加大 retries 或 `waitForTimeout` 掩蓋。 - **MUST** 修 bug 的 E2E:把修正改回去確認測試會紅,再改回來。 ## 3. 標準範本 ### 3.1 `playwright.config.ts`(團隊定本,直接複製) 預設版(mock 模式、無登入、無後端): ```ts import { defineConfig, devices } from '@playwright/test' /** * End-to-end tests. * * These cover what vitest structurally cannot: focus, IME composition, layout, * and anything that depends on a real browser's event ordering. * Runs against the mock API (VITE_USE_MOCK defaults to on), so no backend and no login are needed. */ export default defineConfig({ testDir: './e2e', // The suite drives one shared dev server, so tests must not race each other over its state. // Changing these two needs a team decision, not a PR. fullyParallel: false, workers: 1, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, reporter: process.env.CI ? [['github'], ['html', { open: 'never', outputFolder: 'playwright-report' }]] : [['list'], ['html', { open: 'never', outputFolder: 'playwright-report' }]], outputDir: 'test-results', use: { baseURL: 'http://localhost:5173', headless: true, video: 'on', screenshot: 'on', trace: 'on-first-retry', actionTimeout: 30_000, locale: 'en-US', }, projects: [ { name: 'chrome', use: { ...devices['Desktop Chrome'], // The installed browser rather than a Playwright download: test the browser users actually run. channel: 'chrome', }, }, ], webServer: { command: 'pnpm dev', url: 'http://localhost:5173', reuseExistingServer: !process.env.CI, timeout: 120_000, }, }) ``` **Opt-in 登入 / 真後端變體**:只替換 `projects` 與 `webServer`,其他設定不變。 ```ts projects: [ { name: 'setup', testMatch: /.*\.setup\.ts/ }, { name: 'chrome', use: { ...devices['Desktop Chrome'], channel: 'chrome', storageState: 'playwright/.auth/user.json', }, dependencies: ['setup'], }, ], webServer: [ { // Real API against a throwaway database; health endpoint decided in elf-api-design. command: 'dotnet run --project server/src/<App>.Api --urls http://localhost:5080', url: 'http://localhost:5080/api/health', // Throwaway DB only (credentials elf / elf_test_pw / elf_test, Timezone=UTC); never a shared dev database. env: { ConnectionStrings__Default: process.env.E2E_DB_CONNECTION ?? '' }, reuseExistingServer: !process.env.CI, timeout: 180_000, }, { command: 'pnpm dev', url: 'http://localhost:5173', // Talk to the backend above instead of the mock fixtures. env: { VITE_USE_MOCK: 'false' }, reuseExistingServer: !process.env.CI, timeout: 120_000, }, ], ``` - 登入變體需要:`e2e/auth.setup.ts`(`references/pom-templates.md` §4)、後端可連的拋棄式資料庫(連線字串放環境變數 `E2E_DB_CONNECTION`)、 CI secrets `E2E_USER` / `E2E_PASSWORD`,以及 CI job 內的 `actions/setup-dotnet@v5`(`global-json-file: global.json`)。 - 後端埠號(上例 5080)與 dev server 的 API proxy 目標以專案設定為準,兩邊要一致。 - `baseURL` / `webServer.url` 的前端埠號以專案 Vite 設定為準(參考專案為 5173)。 ### 3.2 `package.json` scripts 與安裝 ```json { "scripts": { "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", "test:e2e:report": "playwright show-report" } } ``` ```bash pnpm add -D -w @playwright/test # Playwright lives at the repo root, next to playwright.config.ts pnpm exec playwright install chrome # 本機已有 Chrome 可略過 pnpm test:e2e ``` `.gitignore` 加入: ``` /test-results/ /playwright-report/ /playwright/.auth/ /allure-results/ /allure-report/ ``` ### 3.3 POM、fixtures、auth.setup、spec 範本 完整可複製檔案見 [`references/pom-templates.md`](references/pom-templates.md): `pages/LoginPage.ts`、`pages/OrderListPage.ts`、`fixtures.ts`、`auth.setup.ts`(登入變體)、`order-list.spec.ts`、 `helpers/layout.ts`、`page.route` 攔截 API 範例。 ### 3.4 CI job(GitHub Actions) 預設 job(mock 模式,不需要後端與 secrets): ```yaml e2e: name: e2e (browser) runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 # Version comes from the "packageManager" field in package.json; must run before setup-node. - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v5 with: node-version-file: .nvmrc cache: pnpm - run: pnpm install --frozen-lockfile # The config targets the installed Chrome rather than a Playwright download, # so the browser has to be present here too. - name: Install Chrome run: pnpm exec playwright install --with-deps chrome - run: pnpm test:e2e - uses: actions/upload-artifact@v5 if: failure() with: name: playwright-report path: | playwright-report/ test-results/ retention-days: 7 ``` - 開啟登入變體時,在 `pnpm install` 前加 `actions/setup-dotnet@v5`(`with: global-json-file: global.json`), 並在 `pnpm test:e2e` 步驟加 `env: E2E_USER: ${{ secrets.E2E_USER }}`、`E2E_PASSWORD: ${{ secrets.E2E_PASSWORD }}`。 - 參考專案 CI 只設 `reporter: 'github'`,失敗時上傳的 `playwright-report/` 是空的;本定本在 CI 同時輸出 `github` + `html`,修正此問題。 - 完整 workflow(與 lint / unit / build 的相依)見 `elf-cicd-frontend`。 ### 3.5 與 Allure Report 整合 預設 reporter 為 Playwright 內建 HTML。專案採用 Allure 統一報告時(見 `elf-allure-report`): ```bash pnpm add -D -w allure-playwright allure ``` ```ts // playwright.config.ts —— 只加 allure reporter,其他設定不變。CI 仍保留 github + html。 reporter: process.env.CI ? [ ['github'], ['html', { open: 'never', outputFolder: 'playwright-report' }], ['allure-playwright', { detail: true, resultsDir: 'allure-results', suiteTitle: false }], ] : [ ['list'], ['allure-playwright', { detail: true, resultsDir: 'allure-results', suiteTitle: false }], ], ``` ```bash pnpm test:e2e # 寫 JSON 到 allure-results/ pnpm exec allure generate allure-results -o allure-report pnpm exec allure open allure-report ``` ## 4. 檢查清單 - [ ] `playwright.config.ts` 在 repo 根目錄,內容與 §3.1 一致(偏離處 PR 有說明) - [ ] 測試在 `e2e/*.spec.ts`,頁面物件在 `e2e/pages/`,spec 從 `./fixtures` import - [ ] 每個 spec 開頭有 JSDoc 說明保護的行為與 jsdom 抓不到的原因
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub