# CẤP ĐỘ 3 — Project Thật & Quy Trình Làm Việc Với Claude

> **Điều kiện:** Đã hoàn thành Cấp độ 2 (tự viết và pass 7 case login trên saucedemo).
> **Mục tiêu của cấp độ này:** Làm việc được trong **project nội bộ thật** theo quy ước của team, và vận hành trọn vẹn **quy trình 4 bước với Claude**: mô tả màn hình → skill → function → sinh test case.

## ✅ Bạn hoàn thành Cấp độ 3 khi:

- [ ] Cấu hình xong `.env` + `account.json`, chạy pass `auth.setup.ts`
- [ ] Hiểu và dùng đúng PW helper, TIMEOUT, quy ước đặt tên test của team
- [ ] Tự viết được 1 file instruction màn hình theo template (mục 3.2)
- [ ] Tự viết (hoặc nhờ Claude viết) được 1 file function cho màn hình đó
- [ ] Mô tả test case theo template và để Claude sinh ra spec chạy pass
- [ ] Test mới viết chạy ổn định cả `--headed` lẫn không headed, đã commit

---

# PHẦN A — LÀM VIỆC TRONG PROJECT NỘI BỘ

## A.1 Cấu hình môi trường (.env và account.json)

1. Sao chép file mẫu:
   ```bash
   cp .env.example .env
   cp account.json.example account.json
   ```
   (Windows cmd: dùng `copy` thay cho `cp`)
2. Mở `.env`, điền URL môi trường test:
   ```
   BASE_URL=https://your-app-url.com
   ```
3. Mở `account.json`, điền tài khoản test:
   ```json
   [
     { "id": "ecm01", "username": "user01@example.com", "password": "pass123" },
     { "id": "ecm09", "username": "admin@example.com", "password": "pass456" }
   ]
   ```
4. Chạy auth setup để tạo cookie đăng nhập sẵn (test sau đó không phải login lại từng lần):
   ```bash
   npx playwright test auth.setup.ts
   ```
   Thành công khi thấy các file `.auth/ecm01.json`, `.auth/ecm09.json`...

### ⚠️ BẢO MẬT — đọc kỹ, lỗi này gây hậu quả thật

`.env`, `account.json`, và thư mục `.auth/` chứa **mật khẩu và cookie đăng nhập thật**. Tuyệt đối không được đưa lên Git.

Kiểm tra ngay: mở file `.gitignore` ở gốc project, phải có đủ các dòng:

```
.env
account.json
.auth/
```

Kiểm tra chắc chắn hơn bằng lệnh (kết quả phải **trống**, không liệt kê 3 file trên):

```bash
git status
```

Nếu thấy `.env` hay `account.json` trong danh sách "Changes" → **dừng lại, báo leader ngay**, đừng commit.

## A.2 playwright.config.ts — bộ não cấu hình

File `playwright.config.ts` ở gốc project quyết định "luật chơi" chung. Bạn **chưa cần sửa**, nhưng cần biết nó điều khiển gì để không thắc mắc:

| Cấu hình | Ý nghĩa |
| --- | --- |
| `use.baseURL` | URL gốc — nhờ nó mà `page.goto("/dashboard")` hiểu là `BASE_URL/dashboard` |
| `timeout` | Mỗi test được chạy tối đa bao lâu trước khi bị tính là fail |
| `use.testIdAttribute` | Khai báo `data-test` là attribute test — nhờ đó `getByTestId("username")` hoạt động |
| `projects` | Danh sách trình duyệt sẽ chạy (chromium, firefox...) — vì sao đôi khi 1 test chạy thành 3 lần |
| `workers` | Số test chạy song song cùng lúc |
| `use.trace`, `use.screenshot` | Khi nào tự lưu trace/ảnh (thường: chỉ khi fail) |
| `webServer` | Tự khởi động ứng dụng trước khi test (nếu có) |

Khi test có hành vi "lạ" mà code đúng → nhìn vào config trước.

## A.3 PW Helper — cách viết rút gọn của team

Project có class `PW` gói sẵn các thao tác lặp lại, đặc biệt với UI phức tạp (dropdown dạng cây, people picker):

| Thao tác | Playwright thuần | PW Helper |
| --- | --- | --- |
| Click nút | `page.getByTestId("id").click()` | `pw.clickButton("id")` |
| Điền input | `page.getByTestId("id").fill("v")` | `pw.inputText("id", "v")` |
| Dropdown thường | `page.getByTestId("id").selectOption("v")` | `pw.inputDropDownList("id", "v")` |
| Dropdown dạng cây | (nhiều bước) | `pw.inputTreeDropDown("id", "v")` |
| People picker | (nhiều bước) | `pw.inputPeoplePicker("id", "v")` |
| Điền nhiều field | (từng dòng) | `pw.batchInput(dataArray)` |
| Chờ element | `expect(l).toBeVisible({timeout})` | `pw.isVisible("id", timeout)` |
| Assert giá trị | `expect(l).toHaveValue("v")` | `pw.valueShouldBe("id", "v")` |
| Assert chứa | `expect(l).toContainText("v")` | `pw.valueShouldContain("id", "v")` |
| Lấy locator | `page.getByTestId("id")` | `pw.getElement("id")` |
| Hard wait | `page.waitForTimeout(N)` | `pw.wait(N)` |

```typescript
// Ví dụ: điền form tạo mới bằng PW
const pw = new PW(page);

await pw.batchInput([
  { testId: "txt-ho-ten", value: "Nguyễn Văn A" },
  { testId: "sel-phong-ban", value: "Phòng IT" },
  { testId: "tree-sel-don-vi", value: "Chi nhánh HCM" },
]);

await pw.clickButton("btn-luu");
await pw.isVisible(".ant-message-success", 5000);
```

## A.4 Cấu trúc chuẩn 1 spec file trong project

```typescript
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";

const BASE_URL = process.env.BASE_URL!;

// ─── Helper: thao tác dùng lại trong nhiều test ───────────────────────
const createRecord = async (page: Page, pw: PW, name: string) => {
  await page.goto(`${BASE_URL}/your-screen`);
  await pw.isVisible("btn-tao-moi", TIMEOUT.PAGE_LOADING);
  await pw.clickButton("btn-tao-moi");
  await pw.inputText("txt-ten", name);
  await pw.clickButton("btn-luu");
  await expect(page.locator(".ant-message-success"), {
    message: "Tạo mới thất bại",
  }).toBeVisible({ timeout: TIMEOUT.ACTION_LOADING });
};

// ─── Test cases ───────────────────────────────────────────────────────
test(
  "UC77 1.1.1 - Tạo mới thành công - Admin" + ACCOUNT.ADMIN,
  async ({ admin }) => {
    const pw = new PW(admin);
    const name = `AT-TEST-${Date.now()}`;

    await test.step("1. Tạo mới bản ghi", async () => {
      await createRecord(admin, pw, name);
    });

    await test.step("2. Bản ghi xuất hiện trong danh sách", async () => {
      await admin.goto(`${BASE_URL}/your-screen`);
      await expect(admin.locator(".ant-table-tbody").getByText(name), {
        message: `Bản ghi '${name}' không hiện trong danh sách`,
      }).toBeVisible({ timeout: TIMEOUT.DATA_LOADING });
    });
  },
);
```

## A.5 Nhóm test và hooks — dùng khi file có nhiều case

```typescript
test.describe("UC77 - Kho tài liệu - Nhóm tạo mới", () => {
  // Chạy TRƯỚC MỖI test trong nhóm
  test.beforeEach(async ({ admin }) => {
    await admin.goto(`${BASE_URL}/kho-tai-lieu`);
  });

  // Chạy SAU MỖI test — dọn dữ liệu nếu cần
  test.afterEach(async ({ admin }) => {
    // xoá bản ghi test vừa tạo (nếu quy ước team yêu cầu)
  });

  test("UC77 1.1.1 - ...", async ({ admin }) => { /* ... */ });
  test("UC77 1.1.2 - ...", async ({ admin }) => { /* ... */ });
});
```

- `describe` giúp report gọn theo nhóm và chạy lọc theo nhóm (`-g "Nhóm tạo mới"`)
- `beforeEach` giúp bỏ đoạn `goto` lặp lại ở đầu mỗi test
- Vẫn giữ nguyên tắc vàng: **mỗi test độc lập hoàn toàn** — không test nào phụ thuộc kết quả test khác

## A.6 Đặt tên test — format bắt buộc

```
UC<số> <mã> - <Mô tả> - <Vai trò> + ACCOUNT.<VAI>
```

```typescript
"UC77 1.1.1 - Tạo bộ hồ sơ thành công với dữ liệu hợp lệ - Admin" + ACCOUNT.ADMIN
"UC77 1.1.2 - Tạo bộ hồ sơ thất bại khi để trống tên - Thủ thư" + ACCOUNT.LIBRARIAN
```

Tên test dùng để **lọc khi chạy** và **đọc trong report**. Tên rõ = báo cáo rõ = truy vết bug nhanh.

## A.7 Thời gian chờ — dùng hằng số TIMEOUT

| Hằng số | Mặc định | Dùng khi |
| --- | --- | --- |
| `TIMEOUT.PAGE_LOADING` | 15 000ms | Chờ trang load sau `goto()` |
| `TIMEOUT.ACTION_LOADING` | 5 000ms | Chờ message success/fail sau thao tác |
| `TIMEOUT.DATA_LOADING` | 10 000ms | Chờ dữ liệu đổ vào bảng/danh sách |
| `TIMEOUT.CONTROL_LOADING` | 3 000ms | Chờ UI control nhỏ (badge, tag...) |
| `TIMEOUT.HARD_WAITING` | 800ms | Hard wait — chỉ khi thật sự bất khả kháng |

## A.8 Tips & lưu ý của team

**Làm:**

- Tên dữ liệu tạo mới luôn kèm timestamp: `` `AT-TEST-${Date.now()}` `` — không bao giờ trùng
- Chia test thành `test.step()` — trace dễ đọc gấp nhiều lần
- Luôn kèm `message` trong `expect()` — fail biết ngay ở bước nào
- Debug bằng `--headed` / `page.pause()`; trước khi commit chạy lại **không headed** ít nhất 2 lần để chắc test ổn định

**Tránh:**

- `page.waitForTimeout(5000)` khi có thể dùng `waitFor` / `expect` với timeout
- CSS class, ID tự sinh làm selector chính
- Test phụ thuộc lẫn nhau
- Dữ liệu test cứng trùng nhau giữa các lần chạy

---

# PHẦN B — QUY TRÌNH LÀM VIỆC VỚI CLAUDE

Đây là quy trình cao nhất: bạn cung cấp **tri thức về màn hình** một lần, sau đó Claude viết test cho bạn nhiều lần. Đầu tư ban đầu ~1-2 giờ mỗi màn hình, thu về mỗi test case chỉ mất vài phút mô tả.

## Sơ đồ tổng quan

```
[Bước 1] Viết instruction màn hình (file .md)
   │        chung.md (header/footer/nav dùng chung)
   │        kho-tai-lieu.md (riêng từng màn hình)
   ▼
[Bước 2] Tạo/cập nhật skill trỏ đến instruction
   │        Claude tự biết đọc file nào khi được gọi
   ▼
[Bước 3] Viết function màn hình (file .function.ts)
   │        taoPhieu(), createFolder(), createFile()...
   │        gói chuỗi thao tác dài thành 1 lệnh gọi
   ▼
[Bước 4] Mô tả test case theo template → Claude viết spec
   │        Claude hỏi lại nếu thiếu thông tin
   ▼
[Kiểm tra] Chạy --headed xem thực tế → chỉnh → chạy ổn định → commit
```

## B.1 — Bước 1: Viết Instruction Màn Hình

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

```
src/screen-instructions/
├── _chung.md              ← thành phần mọi màn hình đều có
├── kho-tai-lieu.md        ← màn hình Kho tài liệu
├── kho-tai-lieu.modal-tao-folder.md  ← tách riêng nếu modal phức tạp
└── quan-ly-nguoi-dung.md
```

Quy ước: file `_chung.md` bắt đầu bằng `_` để luôn đứng đầu danh sách; modal phức tạp tách file riêng theo dạng `<màn-hình>.<tên-modal>.md`.

### Template file `_chung.md` (thành phần dùng chung)

```markdown
# Thành phần chung — mọi màn hình

## Header
- Logo (click → về trang chủ): `[data-test="logo"]`
- Ô tìm kiếm toàn cục: `[data-test="global-search"]`
- Avatar user (click → mở menu tài khoản): `[data-test="user-avatar"]`

## Navigation (sidebar trái)
- Menu item theo tên: dùng `page.getByRole("menuitem", { name: "<Tên menu>" })`
- Menu có thể thu gọn bằng nút: `[data-test="btn-collapse-nav"]`

## Thông báo hệ thống (toast message)
- Thành công: `.ant-message-success` — tự biến mất sau 3s
- Thất bại: `.ant-message-error`
- LƯU Ý: toast biến mất nhanh, phải expect NGAY sau thao tác

## Modal chung
- Nút đóng modal: `.ant-modal-close`
- Nút xác nhận thường có testid dạng `btn-xac-nhan` hoặc `btn-luu`
- Modal có animation ~300ms khi mở — nên chờ element trong modal visible
  trước khi thao tác, không chờ modal container
```

### Template file instruction 1 màn hình (`kho-tai-lieu.md`)

```markdown
# Màn hình: Kho tài liệu

## Thông tin chung
- URL: `${BASE_URL}/kho-tai-lieu`
- Vai trò truy cập được: Admin, Thủ thư (Member chỉ xem)
- Điều kiện tiên quyết: đã đăng nhập (fixture `admin` / `librarian`)
- Dấu hiệu trang load xong: nút "Tạo phiếu" visible — `[data-test="btn-tao-phieu"]`

## Các vùng trên màn hình

### 1. Thanh công cụ (toolbar)
| Thành phần | Selector | Thao tác & hành vi |
| --- | --- | --- |
| Nút "Tạo phiếu" | `[data-test="btn-tao-phieu"]` | Click → mở Modal tạo phiếu |
| Ô tìm kiếm | `[data-test="txt-tim-kiem"]` | Gõ xong nhấn Enter mới tìm (không tự tìm khi gõ) |
| Dropdown lọc trạng thái | `[data-test="sel-trang-thai"]` | Dropdown ant-design, dùng pw.inputDropDownList |

### 2. Bảng danh sách phiếu
- Container: `.ant-table-tbody`
- Mỗi dòng: hover mới hiện nhóm nút thao tác bên phải
- Nút trên dòng (chỉ hiện khi hover):
  | Nút | Selector trong dòng | Hành vi |
  | --- | --- | --- |
  | Sửa | `[data-test="btn-sua"]` | Mở modal sửa, dữ liệu điền sẵn |
  | Xoá | `[data-test="btn-xoa"]` | Mở popup confirm — nút đồng ý: `.ant-popover [data-test="btn-dong-y"]` |
- Phân trang: `.ant-pagination`, mặc định 10 dòng/trang

### 3. Modal "Tạo phiếu"
(Nếu modal đơn giản mô tả tại đây; phức tạp → tách file kho-tai-lieu.modal-tao-phieu.md và ghi rõ: "Chi tiết xem file ...")
| Field | Selector | Loại | Bắt buộc | Ghi chú |
| --- | --- | --- | --- | --- |
| Tên phiếu | `txt-ten-phieu` (testid) | text | ✔ | max 200 ký tự |
| Đơn vị | `tree-sel-don-vi` (testid) | tree dropdown | ✔ | dùng pw.inputTreeDropDown |
| Người phụ trách | `pp-nguoi-phu-trach` (testid) | people picker | ✔ | dùng pw.inputPeoplePicker |
| Mô tả | `txt-mo-ta` (testid) | textarea | ✘ | |
- Nút Lưu: `btn-luu` (testid) → thành công: toast success + modal đóng
- Validate: bỏ trống field bắt buộc + bấm Lưu → hiện chữ đỏ dưới field, selector `.ant-form-item-explain-error`

## Luồng nghiệp vụ chính
1. Tạo phiếu: Tạo phiếu → (trong phiếu) tạo folder → trong folder tạo file
2. Tạo folder: hover dòng phiếu → click nút "Thêm folder" → Modal B mở → điền tên → Lưu
3. ...

## Điểm cần lưu ý khi viết test (quirks)
- Sau khi tạo phiếu, danh sách KHÔNG tự refresh — phải goto lại trang
- Nút Xoá bị disable với phiếu ở trạng thái "Đã duyệt"
- Tìm kiếm có debounce — sau Enter chờ TIMEOUT.DATA_LOADING
```

### Nguyên tắc viết instruction cho Claude hiểu tốt

1. **Selector là vua** — thiếu mô tả văn vẻ không sao, thiếu selector là Claude phải đoán (và đoán sai)
2. **Ghi hành vi, không chỉ ghi tên**: "click → mở modal X", "hover mới hiện", "Enter mới tìm" — chính những chi tiết này quyết định test pass hay fail
3. **Mục "quirks" là mục giá trị nhất** — mọi thứ khiến chính bạn từng vấp phải khi test tay, ghi hết vào đây
4. **Đừng lặp lại nội dung `_chung.md`** — chỉ ghi cái riêng của màn hình. Đầu file riêng nên có dòng: *"Thành phần chung xem `_chung.md`"*
5. Cập nhật file **ngay khi UI đổi** — instruction lỗi thời tệ hơn không có, vì Claude sẽ tự tin viết sai

## B.2 — Bước 2: Tạo Skill Cho Claude

Skill = "công tắc" để Claude tự biết: khi được nhờ viết test cho project này, phải đọc instruction ở đâu, tuân theo quy ước gì.

### Khuyến nghị quan trọng: 1 skill tổng, KHÔNG phải mỗi màn hình 1 skill

Thay vì tạo skill riêng cho từng màn hình (khó trigger đúng, khó bảo trì), tạo **1 skill duy nhất** ví dụ `viet-test-playwright`, bên trong có **bảng chỉ mục** trỏ đến từng file instruction. Claude đọc skill → tra bảng → mở đúng file màn hình cần thiết.

### Cấu trúc skill

```
viet-test-playwright/
└── SKILL.md
```

### Template SKILL.md

```markdown
---
name: viet-test-playwright
description: Viết test case Playwright cho dự án <TÊN DỰ ÁN>. Bắt buộc dùng skill này mỗi khi người dùng yêu cầu viết test, viết spec, tạo test case, automation cho bất kỳ màn hình nào của dự án (Kho tài liệu, Quản lý người dùng, ...), hoặc nhắc đến UC số (UC77, UC12...), hoặc yêu cầu sửa/bổ sung test hiện có.
---

# Viết test Playwright cho dự án <TÊN DỰ ÁN>

## Quy trình bắt buộc
1. Xác định màn hình liên quan → đọc file instruction theo bảng chỉ mục dưới
2. LUÔN đọc kèm `src/screen-instructions/_chung.md`
3. Kiểm tra file function của màn hình (`src/functions/<màn-hình>.function.ts`)
   — nếu đã có hàm phù hợp thì DÙNG LẠI, không viết lại thao tác từ đầu
4. Viết spec theo quy ước ở mục "Quy ước code" dưới đây
5. Nếu thiếu thông tin (selector chưa có trong instruction, hành vi chưa rõ,
   dữ liệu test chưa biết) → HỎI người dùng, không tự đoán

## Bảng chỉ mục màn hình
| Màn hình | Instruction | Function file |
| --- | --- | --- |
| Kho tài liệu | src/screen-instructions/kho-tai-lieu.md | src/functions/kho-tai-lieu.function.ts |
| Quản lý người dùng | src/screen-instructions/quan-ly-nguoi-dung.md | src/functions/quan-ly-nguoi-dung.function.ts |

## Quy ước code
- Import từ fixture: `import { test, expect, ACCOUNT } from "../../src/fixture/base-test"`
- Tên test: `"UC<số> <mã> - <Mô tả> - <Vai>" + ACCOUNT.<VAI>`
- Dùng PW helper cho input/click theo testid; timeout dùng hằng số TIMEOUT
- Mỗi test độc lập; dữ liệu tạo mới kèm `Date.now()`
- Chia bước bằng `test.step()`, mọi expect có `message`
- File spec đặt tại: `tests/uc<nn>/<nhóm>/<mã>.spec.ts`
```

> **Mẹo viết description của skill:** viết hơi "tham" một chút — liệt kê nhiều tình huống trigger (viết test, sửa test, UC số, tên màn hình...). Claude có xu hướng *dưới-kích-hoạt* skill, nên description càng phủ rộng thì càng ít bị bỏ sót.

Nếu bạn dùng **Claude Code**, có thể thay/kèm skill bằng file `CLAUDE.md` ở gốc repo với nội dung tương tự — Claude Code luôn tự đọc file này khi mở project, không cần cơ chế trigger.

## B.3 — Bước 3: Viết Function Màn Hình

Mỗi màn hình một file function gói các chuỗi thao tác dài. Lợi ích kép: **spec ngắn dễ đọc**, và **instruction cũng ngắn lại** (mô tả luồng chỉ cần nói "gọi hàm taoPhieu" thay vì kể 8 bước).

### Cấu trúc

```
src/functions/
├── kho-tai-lieu.function.ts
└── quan-ly-nguoi-dung.function.ts
```

### Template file function

```typescript
// src/functions/kho-tai-lieu.function.ts
import { Page, expect } from "@playwright/test";
import { TIMEOUT } from "../constant/timeout";
import { PW } from "../utils/PW";

const BASE_URL = process.env.BASE_URL!;

/**
 * Mở màn hình Kho tài liệu và chờ trang sẵn sàng.
 * Điều kiện: page đã đăng nhập.
 */
export const moKhoTaiLieu = async (page: Page, pw: PW) => {
  await page.goto(`${BASE_URL}/kho-tai-lieu`);
  await pw.isVisible("btn-tao-phieu", TIMEOUT.PAGE_LOADING);
};

/**
 * Tạo một phiếu mới trong Kho tài liệu.
 * Bao gồm: mở modal → điền form → Lưu → chờ toast success.
 * @param tenPhieu - Tên phiếu (nên kèm Date.now() để không trùng)
 * @param donVi   - Tên đơn vị trong tree dropdown
 * @param nguoiPhuTrach - Tên người trong people picker
 * @returns tenPhieu để test dùng kiểm tra tiếp
 */
export const taoPhieu = async (
  page: Page,
  pw: PW,
  tenPhieu: string,
  donVi: string,
  nguoiPhuTrach: string,
): Promise<string> => {
  await pw.clickButton("btn-tao-phieu");
  await pw.isVisible("txt-ten-phieu", TIMEOUT.CONTROL_LOADING);
  await pw.batchInput([
    { testId: "txt-ten-phieu", value: tenPhieu },
    { testId: "tree-sel-don-vi", value: donVi },
    { testId: "pp-nguoi-phu-trach", value: nguoiPhuTrach },
  ]);
  await pw.clickButton("btn-luu");
  await expect(page.locator(".ant-message-success"), {
    message: `Tạo phiếu '${tenPhieu}' thất bại`,
  }).toBeVisible({ timeout: TIMEOUT.ACTION_LOADING });
  return tenPhieu;
};

/**
 * Tạo folder bên trong một phiếu đã tồn tại.
 * Bao gồm: hover dòng phiếu → click "Thêm folder" → Modal B → điền tên → Lưu.
 */
export const taoFolder = async (
  page: Page,
  pw: PW,
  tenPhieu: string,
  tenFolder: string,
) => {
  const dongPhieu = page.locator(".ant-table-tbody tr").filter({ hasText: tenPhieu });
  await dongPhieu.hover(); // nút chỉ hiện khi hover — xem quirks trong instruction
  await dongPhieu.locator('[data-test="btn-them-folder"]').click();
  await pw.isVisible("txt-ten-folder", TIMEOUT.CONTROL_LOADING);
  await pw.inputText("txt-ten-folder", tenFolder);
  await pw.clickButton("btn-luu-folder");
  await expect(page.locator(".ant-message-success"), {
    message: `Tạo folder '${tenFolder}' thất bại`,
  }).toBeVisible({ timeout: TIMEOUT.ACTION_LOADING });
};
```

### Quy tắc viết function

1. **Mỗi hàm = 1 hành động nghiệp vụ hoàn chỉnh**, kết thúc bằng xác nhận thành công (chờ toast / element). Hàm không tự xác nhận thì spec gọi nó sẽ fail lung tung, khó truy vết
2. **Comment JSDoc (`/** ... */`) đầy đủ bằng tiếng Việt** — đây chính là "tài liệu" Claude và đồng đội đọc để biết cách dùng hàm mà không cần đọc thân hàm
3. **Nhất quán ngôn ngữ đặt tên** — chọn 1: hoặc tiếng Việt không dấu (`taoPhieu`, `taoFolder`, `taoFile`), hoặc tiếng Anh (`createTicket`, `createFolder`). Đừng trộn (`taoPhieu` + `createFolder`) — trộn khiến cả Claude lẫn người mới khó đoán tên hàm đã tồn tại
4. **Hàm nhận tham số, không hard-code dữ liệu** — dữ liệu do test truyền vào
5. **Cập nhật function và instruction cùng lúc khi UI đổi** — chúng là 1 cặp

## B.4 — Bước 4: Mô Tả Test Case Cho Claude

Khi đã có instruction + skill + function, việc còn lại mỗi test case chỉ là mô tả theo template:

### Template mô tả test case

```
Màn hình: Kho tài liệu
Test case: UC77 2.1.3 - Tạo folder thành công trong phiếu vừa tạo - Admin

Tiền điều kiện:
- Đăng nhập Admin (fixture admin)
- Đã có 1 phiếu (tạo mới trong test, dùng hàm taoPhieu)

Các bước:
1. Mở Kho tài liệu
2. Tạo phiếu mới (tên kèm timestamp)
3. Tạo folder trong phiếu đó, tên "FOLDER-" + timestamp
4. Mở phiếu, kiểm tra folder xuất hiện trong danh sách bên trong

Kết quả mong đợi:
- Toast success sau khi tạo folder
- Folder hiện trong danh sách với đúng tên

Ghi chú: nếu thiếu thông tin gì (selector, dữ liệu, hành vi), hãy hỏi tôi
trước khi viết.
```

### Vì sao mô tả theo template thay vì "văn xuôi tự do"?

- Claude ít phải hỏi lại → tiết kiệm vòng lặp
- Chính bạn cũng không bỏ sót tiền điều kiện / kết quả mong đợi
- Template này gần với format test case của PM/BA → copy-chỉnh nhanh

### Vòng kiểm tra sau khi Claude viết xong (bắt buộc)

1. **Đọc lướt spec Claude sinh ra** — kiểm tra: có dùng function sẵn có không? tên test đúng format? có expect kèm message?
2. Chạy `--headed` xem thao tác thực tế
3. Fail → đưa nguyên văn lỗi (Expected/Received + trace nếu có) cho Claude sửa
4. Pass → chạy **không headed 2 lần** kiểm tra ổn định
5. Commit

> **Nguyên tắc số 1 của toàn quy trình: bạn chịu trách nhiệm về test, không phải Claude.**
> Claude là người viết nháp tốc độ cao; nghiệm thu là việc của bạn.

## B.5 — Cải Tiến Đề Xuất Cho Quy Trình

Những nâng cấp đáng cân nhắc khi quy trình đã chạy trơn:

1. **Để Claude tự chạy và tự sửa (nếu dùng Claude Code):** thay vì bạn chạy test rồi dán lỗi lại, Claude Code chạy được `npx playwright test` trực tiếp, đọc lỗi, tự sửa, chạy lại — vòng lặp nhanh hơn nhiều. Ghi vào CLAUDE.md: *"Sau khi viết spec, tự chạy test đó với --workers=1 và sửa cho đến khi pass."*
2. **Sinh instruction bán tự động:** dán HTML của màn hình (copy outerHTML từ F12) hoặc ảnh chụp màn hình cho Claude và yêu cầu *"điền vào template instruction này"* — bạn chỉ việc kiểm tra lại selector và bổ sung quirks, nhanh hơn viết tay từ đầu đáng kể.
3. **Kiểm tra instruction "sống":** mỗi khi có test fail do UI đổi, quy ước sửa **instruction + function trước**, spec sau. Giữ được kỷ luật này thì tri thức của team luôn nằm ở 1 chỗ đúng.
4. **File `du-lieu-test.md`:** mô tả dữ liệu test chuẩn của môi trường (đơn vị nào tồn tại, user picker có ai...) — giảm hẳn số câu Claude phải hỏi "đơn vị 'Chi nhánh HCM' có tồn tại trên môi trường test không?"
5. **Review chéo instruction:** người thứ hai đọc instruction và thử nhờ Claude viết 1 case — nếu Claude viết đúng mà không cần hỏi, instruction đạt chuẩn.

---

## Tổng kết Cấp độ 3

Tick đủ 6 ô checklist đầu tài liệu. Đến đây bạn không chỉ viết được test — bạn đang **vận hành một hệ thống sản xuất test**: tri thức màn hình được tài liệu hoá, thao tác được đóng gói, và AI làm phần việc lặp lại. Vai trò của bạn chuyển từ "người gõ code" sang "người thiết kế test và nghiệm thu chất lượng" — đó chính là giá trị cao nhất của một automation tester.

---

_Phiên bản 2.0 — hợp nhất quy ước project với quy trình Claude 4 bước, kèm template và cải tiến._
