Bỏ qua để đến nội dung

Testing (Playwright + QA)

từ admin/
# 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_PASSWORD
cp packages/helpdesk/tests/.env.example packages/helpdesk/tests/.env.local
# 3. Chạy suite
pnpm run test:e2e # playwright test
pnpm run test:e2e:ui # Playwright UI mode
pnpm run test:e2e:report # mở HTML report

Không có project filter riêng cho “helpdesk” — admin/playwright.config.ts chỉ định nghĩa 1 project chromiumtestDir đã trỏ thẳng vào packages/helpdesk/tests/e2e, nên chạy playwright test = chạy toàn bộ suite helpdesk.

  • 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
admin/playwright.config.ts (đầy đủ)
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”
  1. Đọ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.
  2. Mở Chromium, vào ${baseURL}/auth/login (chờ domcontentloaded, không chờ networkidle vì Nuxt dev server có HMR websocket luôn “busy”).
  3. 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ằng pressSequentially (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ại inputValue() để confirm giá trị “dính”.
  4. 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.
  5. Xác nhận session có quyền helpdesk: vào ${baseURL}/helpdesk/conversations, nếu vẫn ở /auth/login thì throw.
  6. Ghi storageState ra tests/.auth/state.json — mọi spec sau đó dùng chung, không login lại.
tests/.env.example
HELPDESK_EMAIL=admin@example.com
HELPDESK_PASSWORD='change-me'
# Optional — defaults to http://localhost:8888 (the running `pnpm dev` server).
# HELPDESK_BASE_URL=http://localhost:8888

Component gắn data-* để spec bắt chắc thay vì đoán qua class/text:

conversation-list-item.vue — data-conv-row
<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"
>
conversation-header-status.vue — data-testid
<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)

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ệ:

tests/support/helpers.ts:119-137
/** 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).

Pattern chuẩn của suite: drive UI thật, rồi expect.poll trên response BFF (nguồn sự thật):

conversation-actions.spec.ts:48-53
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)
conversation-actions.spec.ts:225-226 — xoá xong thì GET phải fail
await expect.poll(async () => (await page.request.get(`/api/helpdesk/conversations/${throwId}`)).status())
.not.toBe(200)

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:

test-plan.json — summary
{
"total_pages": 7,
"total_groups": 34,
"total_cases": 148,
"priority_breakdown": { "critical": 21, "high": 85, "medium": 36, "low": 6 }
}
test-plan.json — một case entry
{ "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).