# Quy Ước `data-testid` — Tài Liệu Dành Cho Dev

> **Đối tượng đọc:** Developer frontend.
> **Mục đích:** Thống nhất cách đặt `data-testid` để đội QA automation (Playwright) tìm element **ổn định** — không vỡ test khi đổi text hiển thị, đổi class CSS, hay đổi layout.
>
> **Ký hiệu trong tài liệu:**
> - Dòng bắt đầu bằng 🆕 = **QA đề xuất thêm, cần dev/lead review** — chưa phải quy ước chốt.
> - Dòng có ⚠️ = điểm chưa thống nhất, cần chốt 1 phương án.

---

## 1. Quy tắc chung

1. **Định dạng tên:** `<tiền-tố><ten-y-nghia>`, viết **kebab-case, không dấu** (vd `btn-luu-nhap`, `txt-ten-phieu`).
   ⚠️ Codebase hiện tại đang lẫn 2 kiểu: kebab-case (`btn-create-hstl`, `sel-loai-tai-lieu`) và camelCase (`txt-tenHoSo`, `sel-loaiBoHoSo`). Đề xuất: **màn hình mới dùng kebab-case**, màn cũ giữ nguyên (đổi tên = vỡ test đang chạy).
2. **Đặt theo Ý NGHĨA, không theo text hiển thị.** Nút "Lưu" đổi text thành "Ghi lại" thì `btn-save` vẫn giữ nguyên.
3. **Unique trong phạm vi hiển thị cùng lúc.** Hai element cùng testid cùng lúc trên màn hình = QA phải dùng `.first()/.last()` — chính là thứ quy ước này sinh ra để tránh.
4. **Không sinh testid từ dữ liệu động thuần** (index, uuid ngẫu nhiên). Với danh sách, dùng id nghiệp vụ ổn định: `row-phieu-<ma-phieu>`.
5. **Gắn vào element thao tác được** (input thật, button thật) — không gắn vào wrapper div trang trí, trừ các testid loại *container* (mục 3c).

---

## 2. Bảng tiền tố theo loại element

### 2a. Nhóm INPUT (điều khiển nhập liệu trong form)

| Loại Element | Tiền tố | Ví dụ thực tế | Lời khuyên |
| --- | --- | --- | --- |
| **Text Input** (Ô nhập chữ ngắn) | `txt-` | `txt-ten-phieu`, `txt-email` | Dùng cho thẻ `<input type="text">` |
| **Text Area** (Ô nhập chữ dài) | `txa-` | `txa-ghi-chu`, `txa-mo-ta` | Dùng cho thẻ `<textarea>` hoặc TinyMCE |
| **Number Input** (Ô nhập số) | `num-` | `num-so-luong`, `num-gia-tien` | |
| **Select / Dropdown** (Danh sách thả xuống) | `sel-` | `sel-loai-ho-so`, `sel-du-an` | Dùng cho Component Select của Ant Design |
| **Date/Time Picker** (Chọn ngày tháng giờ) | ⚠️ `dtp-` | `dtp-ngay-tao`, `dtp-han-chot` | ⚠️ Bảng gốc ghi tiền tố `datetime-` nhưng ví dụ lại là `dtp-` — đề xuất chốt **`dtp-`** (ngắn, khớp ví dụ) |
| **Checkbox** (Ô tick nhiều lựa chọn) | `chk-` | `chk-dong-y`, `chk-luu-mat-khau` | Gắn vào `<input type="checkbox">`, không gắn vào label |
| **Radio** (Ô tick 1 lựa chọn) | `rdo-` | `rdo-gioi-tinh-nam` | |
| **File Upload** (Nút tải file lên) | `file-` | `file-tai-lieu-dinh-kem` | Gắn trực tiếp vào thẻ `<input type="file">` ẩn |
| **Tree select** (Dropdown dạng cây) | `tree-sel-` | `tree-sel-muc-luu-tru`, `tree-sel-don-vi` | |
| **Select dạng tags** (chọn/nhập nhiều giá trị) | `sel-tags-` | `sel-tags-tu-khoa` | |
| **People picker multi** (Chọn người dùng/nhóm) | `pp-multi-` | `pp-multi-usersRightOwner` | Nếu chỉ chọn 1 người: 🆕 đề xuất thêm `pp-single-` |
| 🆕 **Switch / Toggle** (Công tắc bật tắt) | `swt-` | `swt-kich-hoat`, `swt-phan-quyen-rieng` | Ant Design Switch — khác checkbox về UI nhưng test giống nhau |
| 🆕 **Search Input** (Ô tìm kiếm của danh sách/bảng) | `txt-search-` | `txt-search-ho-so`, `txt-search-tai-lieu` | Tách riêng khỏi `txt-` vì màn hay có nhiều ô search giống placeholder (xem mục 4.4) |
| 🆕 **Slider** (Thanh kéo giá trị) | `sld-` | `sld-muc-do` | Ít gặp — chỉ thêm khi có |

### 2b. Nhóm HÀNH ĐỘNG (element click được)

| Loại Element | Tiền tố | Ví dụ thực tế | Lời khuyên |
| --- | --- | --- | --- |
| **Button** (Nút bấm thông thường) | `btn-` | `btn-khoi-tao`, `btn-luu-nhap` | Gắn vào các nút xác nhận, hủy, submit |
| **Link / Anchor** (Đường dẫn, chữ bấm được) | `lnk-` | `lnk-quen-mat-khau`, `lnk-chi-tiet` | |
| **Tab** (Chuyển tab) | `tab-` | `tab-thong-tin-chung`, `tab-lich-su` | ⚠️ Codebase hiện dùng `lbl-tab-` (vd `lbl-tab-thongTin`) — màn mới đề xuất dùng `tab-` |
| 🆕 **Nút hành động trong dòng bảng** (sửa/xóa/xem trên từng row) | `btn-<hanh-dong>-row` | `btn-edit-row`, `btn-delete-row` | Kết hợp với `row-<id>` của dòng: QA tìm row trước rồi tìm nút trong row — nút không cần unique toàn trang |
| 🆕 **Item trong dropdown menu** (menu xổ ra khi hover/click) | `mni-` | `mni-tao-tai-lieu`, `mni-xuat-excel` | Menu item của split button, menu "..." — hiện QA phải filter theo text hiển thị |
| 🆕 **Nút expand/collapse** (mở rộng dòng cây, section) | `btn-expand-` | `btn-expand-row`, `btn-expand-filter` | Hiện QA phải click `span` thứ 2 trong `td` đầu — cực kỳ dễ vỡ (xem mục 4.3) |
| 🆕 **Phân trang** | `pag-` | `pag-danh-sach-phieu` | Gắn vào container pagination; nút trong đó dùng role sẵn của Ant |

### 2c. Nhóm HIỂN THỊ & CONTAINER

| Loại Element | Tiền tố | Ví dụ thực tế | Lời khuyên |
| --- | --- | --- | --- |
| **Table** (Cả cái bảng) | `tbl-` | `tbl-danh-sach-phieu` | |
| **Table Row** (Một dòng trong bảng) | `row-` | `row-phieu-12345` | Cực kỳ quan trọng để Playwright tìm đúng dòng |
| **Message / Toast** (Thông báo popup góc màn hình) | `msg-` | `msg-tao-thanh-cong` | Dùng để expect sau khi bấm nút |
| **Modal / Dialog** (Cửa sổ bật lên giữa màn hình) | `mdl-` | `mdl-xac-nhan-xoa` | Gắn vào container modal — chi tiết ở mục 4.1 |
| **Label / Text** (Chữ tĩnh hiển thị trạng thái) | `lbl-` | `lbl-trang-thai-duyet` | Dùng để đọc giá trị hiển thị |
| 🆕 **Notification** (Thông báo dạng card góc màn — khác toast) | `ntf-` | `ntf-khong-co-quyen` | Ant Design tách `message` (giữa màn) và `notification` (góc màn) — test cũng phải phân biệt 2 loại này |
| 🆕 **Form container** (Khối form trong trang/modal) | `frm-` | `frm-tao-ho-so`, `frm-phan-quyen` | Khi 1 modal có nhiều form/khu vực nhập liệu |
| 🆕 **Section / Card** (Khối nội dung có tiêu đề trong trang) | `sec-` | `sec-thong-tin-chung`, `sec-lien-quan` | Giúp QA scope tìm kiếm trong đúng khu vực |
| 🆕 **Drawer** (Panel trượt từ cạnh màn hình) | `drw-` | `drw-bo-loc`, `drw-chi-tiet` | Ant Design Drawer — hành xử như modal nhưng DOM khác |
| 🆕 **Validation error** (Dòng lỗi đỏ dưới field) | `err-` | `err-ten-phieu`, `err-email` | Rất cần cho test validate form — hiện phải bắt text chung chung |
| 🆕 **Tag / Badge** (Nhãn trạng thái, đếm số) | `tag-` | `tag-trang-thai`, `tag-so-luong-moi` | |
| 🆕 **Empty state** (Vùng "Không có dữ liệu") | `empty-` | `empty-danh-sach-phieu` | Để assert danh sách rỗng thay vì đếm 0 row |
| 🆕 **Loading / Spinner** | `loading-` | `loading-danh-sach` | Để QA chờ "loading biến mất" thay vì hard-wait |
| 🆕 **Step / Wizard** (Thanh các bước) | `step-` | `step-2-phan-quyen` | Chỉ khi có flow nhiều bước |
| 🆕 **Breadcrumb** | `brc-` | `brc-kho-tai-lieu` | ⚠️ Codebase hiện dùng `lbl-breadcrumb` — màn cũ giữ nguyên |

---

## 3. Trạng thái hiện tại & Yêu cầu bổ sung chi tiết

**Đã yêu cầu và đang có (giữ nguyên, tiếp tục áp dụng):**

- ✅ Các **button** hành động (`btn-save`, `btn-close-modal`, `btn-create-hstl`…)
- ✅ Một số **label quan trọng** (`lbl-modal-title`, `lbl-tenHoSo`, `lbl-tab-thongTin`…)
- ✅ Các **input của form** (`txt-`, `sel-`, `tree-sel-`, `pp-multi-`…)

**Phần dưới đây là yêu cầu BỔ SUNG** — sắp theo độ ưu tiên, mỗi mục kèm lý do lấy từ chính code test đang chạy.

---

### 4.1 🔴 P1 — Container của MODAL (`mdl-`)

**Yêu cầu:** mỗi modal gắn `data-testid="mdl-<ten-modal>"` vào element container (cùng cấp hoặc trên `.ant-modal-content`).

```
mdl-tao-ho-so          → modal Tạo mới / Cập nhật hồ sơ
mdl-phan-quyen-nang-cao → modal Phân quyền nâng cao
mdl-tao-thu-muc        → modal Tạo thư mục
mdl-tao-tai-lieu       → modal Tạo tài liệu
mdl-xac-nhan           → modal confirm chung
```

**Lý do (điểm đau thực tế):** hệ thống thường mở **modal chồng modal** (mở hồ sơ → mở Phân quyền → click item → modal con). Test hiện tại phải viết:

```ts
const phanQuyenModal = page.locator(".ant-modal-content").last(); // "cái modal trên cùng"
```

`.last()` chỉ đúng khi số modal trên DOM đúng như dự đoán — chỉ cần 1 modal cũ chưa bị unmount là test click nhầm chỗ. Đây hiện là **nguồn flaky lớn nhất** của bộ test. Có `mdl-<ten>` thì QA viết `page.getByTestId("mdl-phan-quyen-nang-cao")` — trúng tuyệt đối.

> Kèm theo: nút đóng của mỗi modal nên nằm **trong** container đó (đang có `btn-close-modal` — tốt, nhưng khi 2 modal cùng mở thì 2 nút trùng testid → cần scope theo `mdl-` là giải quyết được, không cần đổi tên nút).

### 4.2 🔴 P1 — Container của BẢNG (`tbl-`) + định danh dòng (`row-`)

**Yêu cầu:**
- Mỗi bảng gắn `tbl-<ten-bang>` (màn có thể có 2+ bảng: bảng chính + bảng trong modal).
- Mỗi dòng gắn `row-<id-nghiep-vu>` (mã phiếu, id tài liệu…) — **không dùng index**.

**Lý do:** test hiện tại tìm dòng bằng cách filter text tên item trong `.ant-table-tbody` — khi 2 item tên gần giống nhau (`Tài liệu 1` / `Tài liệu 10`) là dính strict mode violation hoặc match nhầm.

### 4.3 🔴 P1 — Nút expand của bảng dạng CÂY (`btn-expand-row`)

**Yêu cầu:** trong bảng tree (cấu trúc hồ sơ, phân quyền), nút mở rộng/thu gọn của mỗi dòng gắn `btn-expand-row`.

**Lý do:** test hiện tại phải click "span thứ 2 trong td đầu tiên" — hoàn toàn dựa vào cấu trúc DOM ngầm:

```ts
await folderRow.locator("td").first().locator("span").nth(1).click(); // span.nth(0) là indent!
```

Dev chỉ cần thêm 1 span/div indent là toàn bộ test expand vỡ. Trong dòng đã có `row-<id>` thì `btn-expand-row` không cần unique toàn trang.

### 4.4 🟠 P2 — Ô SEARCH có phân biệt (`txt-search-<pham-vi>`)

**Yêu cầu:** các ô tìm kiếm đặt testid theo phạm vi: `txt-search-ho-so`, `txt-search-phan-quyen`…

**Lý do:** modal Phân quyền nâng cao hiện có **2 ô input cùng placeholder** `"Tìm hồ sơ, tài liệu"` — test phải lấy `.last()` và ghi chú "có 2 cái, lấy cái cuối". Thêm tab mới là sai ngay.

### 4.5 🟠 P2 — People picker trong form THƯ MỤC (`pp-multi-`)

**Yêu cầu:** 5 people picker phân quyền thư mục (Owner / Tạo mới / Cập nhật / Tải file / Xem) hiện **chưa có testid** — bổ sung như form tài liệu đã có (`pp-multi-usersRightOwner`…).

**Lý do:** test hiện phải đếm theo vị trí:

```ts
activeTabPane.locator(".people-picker").nth(4); // 4 = "Quyền xem" — đổi thứ tự field là sai
```

### 4.6 🟠 P2 — Dropdown LỌC trong modal phân quyền

**Yêu cầu:** 2 dropdown lọc "Quyền" và "Phân loại" (class `cssDropdownSelectField`) gắn `sel-loc-quyen`, `sel-loc-phan-loai`; các option checkbox bên trong gắn `chk-<gia-tri>` (vd `chk-view`, `chk-ke-thua`).

**Lý do:** hiện phải filter theo text hiển thị (`hasText: "Quyền"`) — đổi wording là vỡ; và "Quyền riêng tư" chứa chữ "Quyền" nên nguy cơ match chéo giữa 2 dropdown.

### 4.7 🟡 P3 — Thông báo & trạng thái

- `msg-` cho toast (hiện bắt bằng class `.ant-message-notice` chung chung — 2 toast liên tiếp là phải `.last()`)
- `ntf-` cho notification góc màn (vd `ntf-khong-co-quyen`)
- `err-<ten-field>` cho dòng lỗi validate dưới mỗi field
- `loading-<khu-vuc>` cho spinner, `empty-<khu-vuc>` cho vùng không dữ liệu

### 4.8 🟡 P3 — Container khu vực

- `sec-<ten-khu-vuc>` cho các khối lớn trong trang/modal (thông tin chung, liên quan, cấu trúc hồ sơ…)
- `drw-<ten>` nếu dùng Drawer
- `frm-<ten>` khi 1 modal chứa nhiều form

---

## 5. Checklist cho Dev khi làm màn hình mới

Trước khi bàn giao màn hình cho QA, tự kiểm:

- [ ] Mọi input trong form có testid đúng tiền tố (mục 2a)
- [ ] Mọi nút bấm được có `btn-` / `lnk-` / `tab-` / `mni-` (mục 2b)
- [ ] **Mỗi modal có `mdl-<ten>` ở container** — kể cả modal confirm nhỏ
- [ ] Bảng có `tbl-`, dòng có `row-<id nghiệp vụ>`, bảng cây có `btn-expand-row`
- [ ] Ô search có `txt-search-<pham-vi>` — không trùng placeholder mà không phân biệt được
- [ ] Toast/notification/lỗi validate có testid để QA assert kết quả
- [ ] Không có 2 element **cùng hiển thị** mang cùng testid (F12 → Console: `document.querySelectorAll('[data-testid="..."]').length` phải = 1)
- [ ] Đổi text hiển thị KHÔNG làm đổi testid

---

_Phiên bản 0.1 — bản nháp để review. Các dòng 🆕 và ⚠️ cần lead FE + QA chốt trước khi áp dụng chính thức._
