Testing (Playwright + QA)
Chạy test
Phần tiêu đề “Chạy test”# 1. Cần dev server + backend đang chạy sẵn (KHÔNG có webServer auto-start trong config)# admin: pnpm dev (:8888) · backend ODP API: :8055
# 2. Copy tests/.env.example → tests/.env.local, điền HELPDESK_EMAIL/HELPDESK_PASSWORDcp packages/helpdesk/tests/.env.example packages/helpdesk/tests/.env.local
# 3. Chạy suitepnpm run test:e2e # playwright testpnpm run test:e2e:ui # Playwright UI modepnpm run test:e2e:report # mở HTML reportKhông có project filter riêng cho “helpdesk” — admin/playwright.config.ts chỉ định nghĩa 1 project chromium và testDir đã trỏ thẳng vào packages/helpdesk/tests/e2e, nên chạy playwright test = chạy toàn bộ suite helpdesk.
Cấu trúc
Phần tiêu đề “Cấu trúc”Thư mụcadmin/
- playwright.config.ts config DUY NHẤT — testDir trỏ vào helpdesk
Thư mụcpackages/helpdesk/tests/
- global-setup.ts login 1 lần → ghi storageState
- .env.example template biến môi trường
- .env.local gitignored, credential thật
- .auth/state.json storageState dùng chung mọi spec
Thư mục.report/ HTML report
- …
Thư mục.artifacts/ trace/screenshot khi fail
- …
- support/helpers.ts selector const + helper (createThrowawayConversation, …)
Thư mụce2e/
- conversation-actions.spec.ts 276 dòng — nguồn từ conversation-actions-qa.md
- conversation-detail.spec.ts mở conversation, deep-link
- conversations-list.spec.ts listing, quick filter, sort
- inbox-detail-tabs.spec.ts nested-route tab shell (regression guard)
- inbox-outbound.spec.ts SMTP outbound tab + reveal password
- inbox-routing.spec.ts plus-address + sender filter
- zzz-b1-pagination.spec.ts seed 35 conv, verify server pagination + counts
- test-plan.json 148 case / 34 nhóm / 7 page
- conversation-actions-qa.md QA checklist gốc — nguồn viết spec
Config: chạy tuần tự trên server có sẵn
Phần tiêu đề “Config: chạy tuần tự trên server có sẵn”import { defineConfig, devices } from "@playwright/test"import { fileURLToPath } from "node:url"import { config as loadEnv } from "dotenv"
// Local-only Playwright setup for the helpdesk module. NOT wired into CI —// it targets the already-running `pnpm dev` server (:8888) for fast smoke// checks during development. Credentials come from tests/.env.local (gitignored).loadEnv({ path: fileURLToPath(new URL("./packages/helpdesk/tests/.env.local", import.meta.url)) })
const BASE_URL = process.env.HELPDESK_BASE_URL ?? "http://localhost:8888"
export default defineConfig({ testDir: "./packages/helpdesk/tests/e2e", // Sequential by default — the smoke suite shares one logged-in session and // reads live backend data; parallel workers would race on shared state. fullyParallel: false, workers: 1, forbidOnly: false, // One retry absorbs first-run flakes: against a dev server the initial hit on // a route pays a cold Vite compile, which can make the first interaction race // the render. A retry runs the route warm. retries: 1, timeout: 30_000, expect: { timeout: 7_000 }, globalSetup: "./packages/helpdesk/tests/global-setup.ts", reporter: [["list"], ["html", { open: "never", outputFolder: "packages/helpdesk/tests/.report" }]], outputDir: "./packages/helpdesk/tests/.artifacts", use: { baseURL: BASE_URL, storageState: "./packages/helpdesk/tests/.auth/state.json", trace: "retain-on-failure", screenshot: "only-on-failure", actionTimeout: 10_000, navigationTimeout: 15_000 }, projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }]})global-setup.ts: login một lần, dùng chung storageState
Phần tiêu đề “global-setup.ts: login một lần, dùng chung storageState”- Đọc
HELPDESK_EMAIL/HELPDESK_PASSWORD(bắt buộc) +HELPDESK_BASE_URL(tuỳ chọn) từtests/.env.local. Thiếu credential → throw lỗi rõ ràng kèm hướng dẫn copy.env.example. - Mở Chromium, vào
${baseURL}/auth/login(chờdomcontentloaded, không chờnetworkidlevì Nuxt dev server có HMR websocket luôn “busy”). - Chờ response
/api/auth/sso/providers(trang login re-render sau 2 lần boot probe, có thể xoá mất input đã gõ sớm), rồi gõ email/password bằngpressSequentially(không dùng.fill()— v-model của NuxtUI không phản ứng với.fill()), retry tối đa 6 lần, đọc lạiinputValue()để confirm giá trị “dính”. - Click nút “Sign in” theo accessible role/name (có 1 button
type=submitẩn Playwright có thể resolve nhầm), chờ URL rời khỏi/auth/login. - Xác nhận session có quyền helpdesk: vào
${baseURL}/helpdesk/conversations, nếu vẫn ở/auth/loginthì throw. - Ghi
storageStateratests/.auth/state.json— mọi spec sau đó dùng chung, không login lại.
HELPDESK_EMAIL=admin@example.comHELPDESK_PASSWORD='change-me'
# Optional — defaults to http://localhost:8888 (the running `pnpm dev` server).# HELPDESK_BASE_URL=http://localhost:8888Selector convention
Phần tiêu đề “Selector convention”Component gắn data-* để spec bắt chắc thay vì đoán qua class/text:
<div class="group relative flex items-start gap-2.5 px-3 py-2.5 cursor-pointer transition-colors border-b border-default" role="listitem" data-conv-row :aria-current="isActive ? 'true' : undefined"><USelect data-testid="conv-status-select" :model-value="status" :items="STATUS_OPTIONS" value-key="value" label-key="label"/>| Selector | Ở đâu |
|---|---|
[data-helpdesk-list] |
conversation-list.vue — container danh sách |
[data-conv-row] (+ [aria-current="true"] cho row active) |
conversation-list-item.vue |
data-testid="conv-status-select" |
conversation-header-status.vue |
data-testid="conv-assignee-trigger" |
conversation-header-assignee.vue |
data-testid="conv-priority-trigger" |
conversation-header-priority.vue |
[data-helpdesk-resolve-btn] |
conversation-header-overflow.vue (sr-only, target cho phím tắt e) |
Seed data trong spec: createThrowawayConversation
Phần tiêu đề “Seed data trong spec: createThrowawayConversation”support/helpers.ts seed data qua BFF API, không qua UI — “mượn” inbox_id/contact_id của một conversation có sẵn để chắc chắn tạo hợp lệ:
/** Create a throwaway conversation for destructive tests. Returns its id. * Reuses an existing conversation's inbox + requester so the create path always * has a valid contact (skips the test cleanly if the DB is empty). */export async function createThrowawayConversation(page: Page, subject: string): Promise<string | null> { const listResp = await page.request.get("/api/helpdesk/conversations?limit=1") const seed = listResp.ok() ? unwrap<any[]>(await listResp.json())?.[0] : null if (!seed?.inbox_id || !seed?.contact_id) return null const resp = await page.request.post("/api/helpdesk/conversations", { data: { inbox_id: String(seed.inbox_id), contact_id: String(seed.contact_id), subject, status: "open", message: { direction: "incoming", sender_type: "contact", content: subject, content_type: "text" }, }, }) if (!resp.ok()) return null return unwrap(await resp.json())?.id ?? null}Trả null nếu DB rỗng — caller nên test.skip() thay vì fail cứng. Dọn dẹp bằng deleteConversation(page, id) (cũng gọi BFF DELETE).
Verify qua BFF API, không chỉ tin DOM
Phần tiêu đề “Verify qua BFF API, không chỉ tin DOM”Pattern chuẩn của suite: drive UI thật, rồi expect.poll trên response BFF (nguồn sự thật):
await page.getByTestId("conv-status-select").click()await page.getByRole("option", { name: "Pending", exact: true }).click()
await expect.poll(() => getConversation(page, id!).then(c => c?.status)).toBe("pending")// The double-write bug added TWO activity rows per change; assert exactly one.await expect.poll(() => getMessages(page, id!).then(m => m.length)).toBe(before + 1)await expect.poll(async () => (await page.request.get(`/api/helpdesk/conversations/${throwId}`)).status()) .not.toBe(200)test-plan.json — nguồn viết spec
Phần tiêu đề “test-plan.json — nguồn viết spec”148 case / 34 nhóm / 7 page, mỗi case có shape cố định và id case dùng làm tiền tố tên test thật:
{ "total_pages": 7, "total_groups": 34, "total_cases": 148, "priority_breakdown": { "critical": 21, "high": 85, "medium": 36, "low": 6 }}{ "id": "C-D01", "action": "Navigate to /helpdesk (index)", "expect": "Redirects to /helpdesk/conversations (replace), list pane + empty detail state shown", "priority": "critical" }C-D01 map trực tiếp sang tên test thật trong conversations-list.spec.ts:10: "C-D01 /helpdesk redirects to conversations with empty detail". Khi viết spec mới, giữ quy ước này để test-plan và spec luôn tra cứu chéo được.
7 page trong test-plan: conversations-list (11 nhóm/51 case), conversation-detail (7/35), settings (5/22), reports (3/9), contacts-tab (1/6), setup-wizard (1/4), cross-cutting (6/21).
Known-flake
Phần tiêu đề “Known-flake”Đọc tiếp
Phần tiêu đề “Đọc tiếp”- Debugging & điều tra bug — cách dùng
tests/.auth/state.jsonđể chẩn đoán auth chết sau restart backend - Frontend (Nuxt layer + BFF) — composable đứng sau các BFF route được spec verify
- Scale & vận hành —
zzz-b1-pagination.spec.tsverify đúng cap pagination mô tả ở đây