# Playwright Automation — Template

> Tài liệu tham khảo đầy đủ để bootstrap một dự án test tự động Playwright theo chuẩn dự án này.
> Copy thư mục `template/` ra một thư mục mới, đổi tên, điều chỉnh `account.json` và các file `.env.*`, rồi bắt đầu viết test.

---

## Mục lục

1. [Cấu trúc thư mục](#1-cấu-trúc-thư-mục)
2. [Cài đặt ban đầu](#2-cài-đặt-ban-đầu)
3. [Biến môi trường (.env)](#3-biến-môi-trường-env)
4. [Tài khoản (account.json)](#4-tài-khoản-accountjson)
5. [Lệnh chạy test](#5-lệnh-chạy-test)
6. [Tham số CLI Playwright](#6-tham-số-cli-playwright)
7. [Cấu hình playwright.config.ts](#7-cấu-hình-playwrightconfigts)
8. [TIMEOUT — hằng số chờ](#8-timeout--hằng-số-chờ)
9. [Fixture & Accounts](#9-fixture--accounts)
10. [Helper PW](#10-helper-pw)
11. [Viết test — quy ước & pattern](#11-viết-test--quy-ước--pattern)
12. [Report & QA Review Mode](#12-report--qa-review-mode)

---

## 1. Cấu trúc thư mục

```
<project>/
├── .env.development          # Biến môi trường môi trường Dev
├── .env.sit                  # Biến môi trường môi trường SIT
├── .env.sitdev               # Biến môi trường môi trường SIT Dev (nếu có)
├── account.json              # Danh sách tài khoản test (KHÔNG commit lên git nếu prod)
├── playwright.config.ts      # Cấu hình Playwright trung tâm
├── tsconfig.json
├── package.json
├── src/
│   ├── constant/
│   │   └── timeout.ts        # TIMEOUT enum dùng chung
│   ├── fixture/
│   │   └── base-test.ts      # Khai báo fixture/account cho toàn dự án
│   ├── setting/
│   │   └── test.setting.ts   # Các thông số cấu hình test chung
│   ├── screen-instructions/  # Tài liệu kỹ thuật từng màn hình (1 file/màn)
│   └── utils/
│       ├── PW.ts             # Helper class bọc Playwright API
│       ├── account-loader.ts # Đọc account.json
│       ├── auth-helper.ts    # Tạo auth context từ session đã lưu
│       ├── excel-reporter.ts # Custom reporter xuất .xlsx
│       └── excel-utils.ts    # Tiện ích Excel
├── tests/
│   ├── README.md             # Hướng dẫn viết test (tài liệu dùng chung)
│   ├── auth.setup.ts         # Global setup: đăng nhập & lưu session
│   └── uc<nn>/               # Mỗi UC một thư mục
│       ├── uc<nn>.default.data.ts   # Dữ liệu mặc định của UC
│       └── 1.1.x/                   # Nhóm test theo chức năng
│           └── 1.1.x.spec.ts
├── playwright/
│   └── .auth/               # Session đăng nhập (tự sinh, KHÔNG commit)
│       └── ecm01.json
└── reports-history/         # Report tự động tạo theo ngày/lần chạy
    └── Report20260701.0/
        ├── html-report/
        ├── monocart-report/
        ├── test-results/     # Trace, video, screenshot khi fail
        └── Bao_Cao_Ket_Qua_Run.xlsx
```

---

## 2. Cài đặt ban đầu

```bash
# 1. Cài dependencies
yarn install
# hoặc: npm install

# 2. Cài Playwright browsers (cần làm 1 lần sau khi clone/cài)
npx playwright install

# 3. Cấu hình tài khoản test (xem mục 4)
cp account.json.example account.json
# Sửa email/password trong account.json cho đúng môi trường

# 4. Cấu hình biến môi trường (xem mục 3)
cp .env.example .env.development
# Điền BASE_URL và các biến khác
```

---

## 3. Biến môi trường (.env)

Mỗi môi trường có 1 file `.env.<tên>`. File được load tự động dựa vào biến `TEST_ENV`.

### Các file env

| File               | Môi trường          | Dùng cho                   |
| ------------------ | ------------------- | -------------------------- |
| `.env.development` | `TEST_ENV=development` | Dev local                  |
| `.env.sit`         | `TEST_ENV=sit`         | SIT server                 |
| `.env.sitdev`      | `TEST_ENV=sitdev`      | SIT Dev server (nếu có)    |

### Nội dung file `.env`

```dotenv
# URL gốc của ứng dụng (không có trailing slash)
BASE_URL="https://your-app.example.com/#"
```

Trong test, dùng:

```ts
const BASE_URL = process.env.BASE_URL!;
await page.goto(`${BASE_URL}/managed-records`);
```

### Biến môi trường runtime (không cần file .env)

| Biến              | Giá trị      | Mô tả                                               |
| ----------------- | ------------ | --------------------------------------------------- |
| `TEST_ENV`        | `development` / `sit` / `sitdev` | Chọn file `.env.*` tương ứng |
| `QA_REVIEW_MODE`  | `true`       | Quay video toàn bộ test (kể cả pass) để QA xem lại |
| `CI`              | `true`       | Tắt `test.only`, bật `retries`                      |

---

## 4. Tài khoản (account.json)

### Cấu trúc file

`account.json` là danh sách tất cả tài khoản test của dự án. Mỗi entry gồm 5 trường:

```json
[
  {
    "id": "ecm01",        // (BẮT BUỘC) ID duy nhất — dùng làm tên fixture và tên file session
    "email": "u@...",     // (BẮT BUỘC) Email / username đăng nhập
    "password": "P@ss",   // (BẮT BUỘC) Mật khẩu
    "des": "Thủ thư",     // (tuỳ chọn) Ghi chú vai trò, chỉ để đọc
    "role": "librarian"   // (tuỳ chọn) Nhóm vai — dùng nếu auth.setup.ts phân nhánh theo role
  }
]
```

> ⚠️ `account.json` chứa mật khẩu — **bắt buộc thêm vào `.gitignore`**.
> Commit file `account.json.example` (không có password thật) để đồng đội dễ bootstrap.

### Trường `id` là quan trọng nhất

`id` là sợi chỉ kết nối 3 nơi:

```
account.json          src/fixture/base-test.ts      playwright/.auth/
─────────────         ──────────────────────────    ────────────────────
"id": "ecm01"  →  buildRole("ecm01")           →   ecm01.json (session)
"id": "ecm09"  →  buildRole("ecm09")           →   ecm09.json
```

Đổi `id` ở một chỗ phải đổi ở cả 3 nơi.

### Thích nghi cho project mới — ví dụ cụ thể

**Project sun-ecm** (cũ):

```json
[
  { "id": "ecm01", "email": "ecm01@fxp.vn", "password": "...", "des": "Thủ thư",       "role": "librarian" },
  { "id": "ecm09", "email": "ecm09@fxp.vn", "password": "...", "des": "Admin",          "role": "admin"     },
  { "id": "ecm04", "email": "ecm04@fxp.vn", "password": "...", "des": "End user",       "role": "end_user"  },
  { "id": "ecm05", "email": "ecm05@fxp.vn", "password": "...", "des": "End user phụ",   "role": "end_user"  }
]
```

**Project CRM mới** (ví dụ với bộ role hoàn toàn khác):

```json
[
  { "id": "crm_admin",   "email": "admin@crm.vn",   "password": "...", "des": "Super admin",    "role": "admin"   },
  { "id": "crm_manager", "email": "mgr@crm.vn",     "password": "...", "des": "Sales manager",  "role": "manager" },
  { "id": "crm_staff1",  "email": "staff1@crm.vn",  "password": "...", "des": "Nhân viên 1",    "role": "staff"   },
  { "id": "crm_staff2",  "email": "staff2@crm.vn",  "password": "...", "des": "Nhân viên 2",    "role": "staff"   },
  { "id": "crm_viewer",  "email": "viewer@crm.vn",  "password": "...", "des": "Chỉ xem báo cáo","role": "viewer"  }
]
```

Sau khi thay `account.json`, cập nhật `base-test.ts` tương ứng (xem mục 9).

---

## 5. Lệnh chạy test

### NPM scripts (định nghĩa trong `package.json`)

```bash
# Chạy tất cả test — môi trường Development (headless)
npm run test:dev

# Chạy tất cả test — môi trường SIT (headless)
npm run test:sit

# Chạy với quay video tất cả case (QA Review) — Dev
npm run test:dev:qa

# Chạy với quay video tất cả case (QA Review) — SIT Dev
npm run test:sitdev:qa

# Chạy có giao diện browser (headed) — thấy browser chạy
npm run test:dev:headed
npm run test:sit:headed

# Chạy với Playwright UI (giao diện debug đồ hoạ)
npm run test:dev:ui
npm run test:sit:ui

# Xem report monocart sau khi chạy xong
npm run report

# Sửa video bị lỗi codec (cần ffmpeg)
npm run fix-videos
```

### Chạy trực tiếp với `npx playwright test`

```bash
# Môi trường SIT, chỉ chạy file cụ thể
TEST_ENV=sit npx playwright test tests/uc77/1.2.1/1.2.1.97.spec.ts

# Chạy theo pattern tên file
TEST_ENV=sit npx playwright test --grep "UC77"

# Chỉ chạy test có tên chứa "1.2.1.97"
TEST_ENV=sit npx playwright test --grep "1.2.1.97"

# Headed (thấy browser)
TEST_ENV=sit npx playwright test --headed

# Giới hạn số worker (giảm để debug)
TEST_ENV=sit npx playwright test --workers=1

# Không retry khi fail
TEST_ENV=sit npx playwright test --retries=0

# Bật QA Review Mode (quay video cả case pass)
QA_REVIEW_MODE=true TEST_ENV=sit npx playwright test

# Chạy UI mode (debug đồ hoạ tốt nhất)
TEST_ENV=sit npx playwright test --ui

# Xem trace của test đã chạy
npx playwright show-trace reports-history/Report20260701.0/test-results/<trace-file>.zip
```

---

## 6. Tham số CLI Playwright

Các tham số hay dùng với `npx playwright test`:

| Tham số                         | Ví dụ                              | Mô tả                                                  |
| ------------------------------- | ---------------------------------- | ------------------------------------------------------ |
| `--headed`                      | `--headed`                         | Mở browser thật, thấy được UI                          |
| `--ui`                          | `--ui`                             | Mở Playwright UI — debug, xem trace trực quan          |
| `--grep <pattern>`              | `--grep "UC77"`                    | Chỉ chạy test có tên khớp regex                        |
| `--grep-invert <pattern>`       | `--grep-invert "UC77"`             | Bỏ qua test có tên khớp                                |
| `--workers <n>`                 | `--workers=1`                      | Số luồng song song (1 = tuần tự, dễ debug)             |
| `--retries <n>`                 | `--retries=0`                      | Số lần retry khi fail (0 = không retry)                |
| `--timeout <ms>`                | `--timeout=120000`                 | Timeout mặc định cho từng test (ms)                    |
| `--project <name>`              | `--project=chromium`               | Chỉ chạy browser/project cụ thể                        |
| `--reporter <type>`             | `--reporter=list`                  | Override reporter: `list`, `html`, `json`, `dot`       |
| `--output <dir>`                | `--output=./my-results`            | Thư mục lưu kết quả test (trace, video, screenshot)    |
| `--last-failed`                 | `--last-failed`                    | Chỉ chạy lại các test đã fail trong lần chạy trước     |
| `--debug`                       | `--debug`                          | Chạy với Playwright Inspector (step từng dòng)         |
| `--update-snapshots`            | `--update-snapshots`               | Cập nhật snapshot baseline                             |

---

## 7. Cấu hình playwright.config.ts

Các thông số quan trọng trong `playwright.config.ts`:

### Thông số toàn cục

```ts
export default defineConfig({
  timeout: 8 * 60 * 1000,   // Timeout tối đa mỗi test: 8 phút
  globalSetup: "./tests/auth.setup.ts", // Chạy trước tất cả test: đăng nhập
  expect: {
    timeout: 120000,         // Timeout mặc định cho expect(): 2 phút
  },
  outputDir: `${REPORT_OUTPUT_DIR}/test-results`, // Trace, video, screenshot tạm
  testDir: "./tests",
  fullyParallel: true,       // Các file spec chạy song song
  forbidOnly: !!process.env.CI, // Cấm dùng test.only khi chạy CI
  retries: 2,                // Retry 2 lần trước khi báo fail
  workers: 5,                // 5 worker song song
```

### Thay đổi thường dùng

| Mục đích                       | Thay đổi                        |
| ------------------------------ | ------------------------------- |
| Chạy tuần tự để debug          | `workers: 1` hoặc `--workers=1` |
| Không retry                    | `retries: 0` hoặc `--retries=0` |
| Tăng timeout test              | `timeout: 15 * 60 * 1000`       |
| Tắt video để chạy nhanh hơn    | `video: "off"`                  |
| Bật video tất cả (QA review)   | `QA_REVIEW_MODE=true` khi chạy  |

### QA_REVIEW_MODE

```bash
# Bật: quay video toàn bộ test (kể cả pass) — dùng để QA quan sát thao tác
QA_REVIEW_MODE=true npm run test:sit

# Mặc định (tắt): chỉ giữ video khi test fail
npm run test:sit
```

### Report tự động

Mỗi lần chạy tạo 1 folder mới theo format `reports-history/ReportYYYYMMDD.Index`:

```
reports-history/
├── Report20260701.0/    ← lần chạy đầu tiên ngày 2026-07-01
├── Report20260701.1/    ← lần chạy thứ hai
└── Report20260701.2/
```

Mỗi folder chứa:
- `html-report/` — Playwright HTML report (mở `index.html`)
- `monocart-report/` — Monocart report (đẹp hơn, có chart)
- `test-results/` — Trace, video, screenshot của test fail
- `Bao_Cao_Ket_Qua_Run.xlsx` — Report Excel tóm tắt

---

## 8. TIMEOUT — hằng số chờ

Khai báo trong `src/constant/timeout.ts`. Import: `import { TIMEOUT } from "../../../src/constant/timeout";`

| Hằng               | Giá trị (ms) | Dùng cho                                         |
| ------------------ | ------------ | ------------------------------------------------ |
| `PAGE_LOADING`     | 60.000       | Chờ trang load xong / nút primary đầu tiên       |
| `ELEMENT`          | 5.000        | Element bình thường                              |
| `CONTROL_LOADING`  | 3.000        | Control trong modal (nút, dropdown hiện/ẩn)      |
| `ACTION_LOADING`   | 15.000       | Sau action (lưu form, mở modal, toast success)   |
| `VALIDATE_WAITING` | 5.000        | Sau khi nhập → chờ validate field                |
| `HARD_WAITING`     | 10.000       | Hard wait sau khi vào màn (chờ JS init)          |
| `LOGIN_NAVIGATE`   | 240.000      | Điều hướng đăng nhập                             |
| `DATA_LOADING`     | 15.000       | Sau search / filter — chờ dữ liệu load về bảng  |

**Quy tắc chọn TIMEOUT:**

```ts
// Chờ trang load xong lần đầu
await pw.isVisible("btn-primary", TIMEOUT.PAGE_LOADING);

// Sau khi vào màn, trước thao tác đầu tiên
await pw.wait(TIMEOUT.HARD_WAITING);

// Chờ toast / modal xuất hiện sau click
await expect(page.locator(".ant-message-success")).toBeVisible({
  timeout: TIMEOUT.ACTION_LOADING,
});

// Chờ kết quả search hiện trong bảng
await page.waitForTimeout(TIMEOUT.DATA_LOADING);

// Modal / dropdown / nút trong form
await expect(locator).toBeVisible({ timeout: TIMEOUT.CONTROL_LOADING });
```

---

## 9. Fixture & Accounts

Định nghĩa trong `src/fixture/base-test.ts`. Import thay thế `@playwright/test` trong **mọi spec file**:

```ts
import { test, expect, ACCOUNT } from "../../../src/fixture/base-test";
```

### Cơ chế hoạt động

Mỗi fixture là một **Page đã đăng nhập sẵn**. Khi test inject `{ admin }`, framework tự động:
1. Đọc session đã lưu từ `playwright/.auth/ecm09.json`
2. Tạo browser context với cookie/storage đó
3. Mở một Page mới — không cần login thủ công trong test
4. Sau test: giữ video nếu fail; xóa nếu pass (tiết kiệm dung lượng)

### Hai loại fixture

**Alias ngữ nghĩa** — tên gợi nhớ vai trò, dùng trong 95% test:

| Fixture     | Trỏ tới account | Hằng `ACCOUNT`      |
| ----------- | --------------- | ------------------- |
| `admin`     | `ecm09`         | `ACCOUNT.ADMIN`     |
| `librarian` | `ecm01`         | `ACCOUNT.LIBRARIAN` |
| `end_user`  | `ecm04`         | `ACCOUNT.END_USER`  |

**Inject trực tiếp theo id** — dùng khi cần vai phụ (ví dụ kiểm tra phân quyền với user cụ thể):

```ts
test("...", async ({ admin, ecm05 }) => {
  // admin = trang admin, ecm05 = trang của ecm05 — 2 browser context độc lập
});
```

### Cách thích nghi cho project mới

Cần sửa **3 chỗ** trong `base-test.ts` khi đổi bộ tài khoản:

**Bước 1 — Đổi hằng ACCOUNT** (alias ngữ nghĩa → `id` tương ứng):

```ts
// sun-ecm (cũ)
export const ACCOUNT = {
  LIBRARIAN: "ecm01",
  ADMIN: "ecm09",
  END_USER: "ecm04",
};

// CRM project (mới)
export const ACCOUNT = {
  ADMIN: "crm_admin",
  MANAGER: "crm_manager",
  STAFF: "crm_staff1",
  VIEWER: "crm_viewer",
};
```

**Bước 2 — Đổi type AppRoles** (khai báo để TypeScript autocomplete đúng fixture name):

```ts
// sun-ecm (cũ)
type AppRoles = {
  ecm01: Page; ecm02: Page; /* ... */ ecm09: Page;
  admin: Page; librarian: Page; end_user: Page;
};

// CRM project (mới)
type AppRoles = {
  crm_admin: Page;
  crm_manager: Page;
  crm_staff1: Page;
  crm_staff2: Page;
  crm_viewer: Page;
  // alias ngữ nghĩa:
  admin: Page;
  manager: Page;
  staff: Page;
  viewer: Page;
};
```

**Bước 3 — Đăng ký fixture** (map alias → id, và đăng ký từng id):

```ts
// sun-ecm (cũ)
export const test = base.extend<AppRoles>({
  ecm01: buildRole("ecm01"),
  /* ... */
  ecm09: buildRole("ecm09"),
  admin: buildRole(ACCOUNT.ADMIN),       // = buildRole("ecm09")
  librarian: buildRole(ACCOUNT.LIBRARIAN),
  end_user: buildRole(ACCOUNT.END_USER),
});

// CRM project (mới)
export const test = base.extend<AppRoles>({
  crm_admin:   buildRole("crm_admin"),
  crm_manager: buildRole("crm_manager"),
  crm_staff1:  buildRole("crm_staff1"),
  crm_staff2:  buildRole("crm_staff2"),
  crm_viewer:  buildRole("crm_viewer"),
  // alias ngữ nghĩa — trỏ vào id tương ứng:
  admin:   buildRole(ACCOUNT.ADMIN),    // = buildRole("crm_admin")
  manager: buildRole(ACCOUNT.MANAGER),
  staff:   buildRole(ACCOUNT.STAFF),
  viewer:  buildRole(ACCOUNT.VIEWER),
});
```

> **Quy tắc**: luôn đăng ký đủ cả **id thật** lẫn **alias ngữ nghĩa**.
> - Id thật (`crm_staff2`) → dùng khi test cần vai phụ không có alias: `async ({ crm_staff2 }) => ...`
> - Alias (`staff`) → dùng trong test thông thường và trong hằng `ACCOUNT`

### Sử dụng fixture trong test

```ts
// Một vai — alias
test("CRM 1.1 - Tạo lead - Admin" + ACCOUNT.ADMIN, async ({ admin }) => { ... });

// Hai vai song song (2 browser context riêng)
test("CRM 1.2 - Manager duyệt lead của Staff", async ({ manager, crm_staff2 }) => {
  // manager và crm_staff2 là 2 Page độc lập
});

// Cùng logic, nhiều vai (mỗi vai là 1 test riêng)
const runTest = async (page: Page) => { /* ... */ };

test("CRM 1.3 - Admin" + ACCOUNT.ADMIN,     async ({ admin })   => runTest(admin));
test("CRM 1.3 - Manager" + ACCOUNT.MANAGER, async ({ manager }) => runTest(manager));
```

### Đặt tên test

```
"<UC/Tính năng> <mã> - <mô tả ngắn> - <Tên vai>" + ACCOUNT.<VAI>
```

Ví dụ:

```ts
"UC77 1.2.1.97 - Cột Kế thừa khi bản ghi kế thừa - Admin" + ACCOUNT.ADMIN
"CRM 1.1.5 - Tạo lead thành công - Manager" + ACCOUNT.MANAGER
```

---

## 10. Helper PW

Khởi tạo: `const pw = new PW(page);`

### Các method chính

| Method | Mô tả |
| ------ | ----- |
| `pw.isVisible(testId, timeout?)` | Assert element visible (throw nếu không thấy) |
| `pw.wait(ms)` | `page.waitForTimeout(ms)` |
| `pw.clickButton(testId)` | Click element theo `data-testid` |
| `pw.inputText(testId, value)` | Điền text input |
| `pw.inputTextArea(testId, value)` | Điền textarea |
| `pw.inputDropDownList(testId, optionText?)` | Chọn dropdown đơn (bỏ trống → chọn đầu tiên) |
| `pw.inputTreeDropDown(testId, optionText?, extraLocator?, matchMode?)` | Chọn tree-select dropdown |
| `pw.inputDropDownTagsList(testId, tags[])` | Nhập nhiều tag |
| `pw.inputDatetime(testId, value)` | Điền ngày/giờ |
| `pw.inputPeoplePicker(testId, value)` | Nhập people-picker (`value` dạng `"a,b"`) |
| `pw.batchInput(data[], checkBranch?)` | Điền cả form tự động theo prefix testId |
| `pw.getElement(testId)` | Trả về `Locator` theo testId |
| `pw.valueShouldBe(testId, v)` | Assert giá trị control bằng đúng `v` |
| `pw.valueShouldContain(testId, v)` | Assert giá trị chứa `v` |
| `pw.clearValue(testId)` | Xóa giá trị select/text |
| `pw.isEmpty(testId)` | Assert control rỗng |

### Quy ước prefix trong `batchInput`

`batchInput` tự route theo prefix `testId`:

| Prefix | Loại control | Method được gọi |
| ------ | ------------ | --------------- |
| `txt-` | text input | `inputText` |
| `txa-` | textarea | `inputTextArea` |
| `date-` | datepicker | `inputDatetime` |
| `sel-` | dropdown đơn | `inputDropDownList` |
| `sel-tags-` | dropdown multi (tags) | `inputDropDownTagsList` |
| `tree-sel-` | tree dropdown | `inputTreeDropDown` |
| `pp-multi-` | people picker | `inputPeoplePicker` |
| `btn-add-related-ecm` | nút mở modal liên quan | `inputRelatedECM` |

```ts
// Ví dụ batchInput — điền cả form, lọc bỏ những field xử lý riêng
await pw.batchInput(
  uc29DefaultData.filter(
    (o) =>
      !o.testId.startsWith("pp-multi-") &&   // people picker → gọi riêng
      o.testId !== "txt-tenHoSo" &&           // tên unique → gọi riêng
      o.testId !== "btn-add-related-ecm",     // liên quan → xử lý riêng
  ),
  true, // checkBranch: tự điền sel-duAn khi loại = "dự án"
);
await pw.inputText("txt-tenHoSo", `AT-UC29-${Date.now()}`);
await pw.inputPeoplePicker("pp-multi-usersRightOwner", "ecm05");
```

---

## 11. Viết test — quy ước & pattern

### File data mặc định của UC

Mỗi UC có file `tests/uc<nn>/uc<nn>.default.data.ts` khai báo:
- `uc<nn>DefaultUrl` — URL màn hình
- `uc<nn>DefaultData` — mảng `{ testId, value }` cho `batchInput`

```ts
// tests/uc77/uc77.default.data.ts
export { uc29DefaultData as uc77DefaultData, uc29DefaultUrl as uc77DefaultUrl }
  from "../uc29/uc29.default.data";
```

### Import chuẩn

```ts
import { Page } from "@playwright/test";
import { test, expect, ACCOUNT } from "../../../src/fixture/base-test";
import { TIMEOUT } from "../../../src/constant/timeout";
import { PW } from "../../../src/utils/PW";
import { uc77DefaultData, uc77DefaultUrl } from "../uc77.default.data";

const BASE_URL = process.env.BASE_URL!;
```

> Số cấp `../` phụ thuộc độ sâu: `tests/uc77/1.2.1/` → `../../../src`.

### Skeleton spec cơ bản

```ts
test(
  "UC77 1.2.1.97 - <mô tả> - Admin" + ACCOUNT.ADMIN,
  async ({ admin }) => {
    const pw = new PW(admin);
    const ts = Date.now();
    const itemName = `AT-UC77-97-${ts}`; // tên unique tránh trùng

    await test.step("1. Tạo dữ liệu cần thiết", async () => {
      await admin.goto(`${BASE_URL}${uc77DefaultUrl}`);
      await pw.isVisible("btn-primary", TIMEOUT.PAGE_LOADING);
      await pw.wait(TIMEOUT.HARD_WAITING);
      // ...
    });

    await test.step("2. Thực hiện thao tác", async () => {
      // ...
    });

    await test.step("3. Kiểm tra kết quả", async () => {
      await expect(admin.locator(".expected-element"), {
        message: "Lỗi: <mô tả rõ ràng>",
      }).toBeVisible({ timeout: TIMEOUT.ACTION_LOADING });
    });
  },
);
```

### Quy ước đặt tên file

```
tests/uc<nn>/<nhóm>/<mã>.spec.ts
```

Ví dụ: `tests/uc77/1.2.1/1.2.1.97.spec.ts`

### Selectors AntD hay dùng

| Mục đích | Locator |
| -------- | ------- |
| Modal đang mở | `page.locator(".ant-modal-content").last()` |
| Toast thành công | `.ant-message-success` |
| Toast bất kỳ | `.ant-message-notice` |
| Dòng bảng level 0 | `.ant-table-row.ant-table-row-level-0` |
| Checkbox chọn dòng | `.ant-table-row .ant-checkbox-wrapper` |
| Phân trang | `.ant-pagination` |
| Dropdown option | `.ant-select-item-option` |
| Tag trong picker | `.ant-tag` |

### Lưu ý quan trọng

1. **`HARD_WAITING` sau khi vào màn** — `pw.wait(TIMEOUT.HARD_WAITING)` trước thao tác đầu tiên.
2. **Tên dữ liệu unique** — luôn dùng `` `AT-...-${Date.now()}` `` để test song song không xung đột.
3. **Hover trước khi click checkbox** trong bảng — checkbox chỉ hiện khi hover.
4. **Search trong modal chậm** — sau `press("Enter")` chờ `waitForTimeout(10000)`.
5. **People picker tách khỏi batchInput** — `pp-multi-*` thường cần gọi riêng `inputPeoplePicker`.
6. **Goto lại recordUrl** trước mỗi lần tạo folder/document để reset DOM.
7. **Assert message rõ ràng** — luôn dùng `{ message: "Lỗi: ..." }` trong `expect` để dễ debug.

---

## 12. Report & QA Review Mode

### Xem report sau khi chạy

```bash
# Mở Playwright HTML report (lần chạy mới nhất)
npx playwright show-report reports-history/$(ls -t reports-history | head -1)/html-report

# Mở Monocart report
npm run report

# Xem trace của test fail
npx playwright show-trace reports-history/<folder>/test-results/<hash>/trace.zip
```

### Fix video bị lỗi codec (cần ffmpeg)

```bash
# Fix toàn bộ video trong reports-history
npm run fix-videos

# Fix video của 1 folder cụ thể
DIR=reports-history/Report20260701.0 npm run fix-videos
```

### QA Review Mode

```bash
# Bật: quay video toàn bộ case (kể cả pass) để QA quan sát thao tác
QA_REVIEW_MODE=true TEST_ENV=sit npx playwright test

# Hoặc dùng script đã định nghĩa sẵn
npm run test:dev:qa
npm run test:sitdev:qa
```

Video được đính kèm vào report, xem trong `html-report` hoặc `monocart-report`.

---

## Tóm tắt nhanh

```bash
# Dev — chạy tất cả
npm run test:dev

# SIT — chạy tất cả
npm run test:sit

# Debug 1 file, headed, không retry
TEST_ENV=sit npx playwright test tests/uc77/1.2.1/1.2.1.97.spec.ts --headed --workers=1 --retries=0

# Chỉ chạy case tên có "UC77"
TEST_ENV=sit npx playwright test --grep "UC77"

# QA xem thao tác
QA_REVIEW_MODE=true npm run test:sit

# Playwright UI (debug tốt nhất)
npm run test:sit:ui
```
