# sun-ecm — Hướng dẫn chung cho test tự động (Playwright)

> Tài liệu **dùng chung cho cả dự án** sun-ecm: fixtures/accounts, TIMEOUT, helper `PW`,
> import chuẩn và các pattern khai báo test. Mỗi UC có file README riêng chỉ mô tả
> phần đặc thù (trường, button, modal của màn hình đó) và link về tài liệu này.
>
> README riêng theo UC: [uc29/UC29.md](uc29/UC29.md) (các UC khác bổ sung tương tự).

---

## 1. Import chuẩn cho mọi spec

```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";
// dữ liệu mặc định của từng UC, vd:
import { uc29DefaultData, uc29DefaultUrl } from "../uc29.default.data";

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

> Số cấp `../` tuỳ độ sâu thư mục spec. Với `tests/uc29/1.1.2/*.spec.ts` thì `src` ở `../../../src`.

---

## 2. Tài khoản (fixtures)

Khai báo trong [src/fixture/base-test.ts](../src/fixture/base-test.ts). Mỗi fixture là một `Page` **đã đăng nhập sẵn** (auth context + record video; video tự xoá nếu test pass, đính kèm report nếu fail).

| Fixture           | Account ID | Vai trò                                                             | Hằng `ACCOUNT`      |
| ----------------- | ---------- | ------------------------------------------------------------------- | ------------------- |
| `librarian`       | `ecm01`    | Thủ thư                                                             | `ACCOUNT.LIBRARIAN` |
| `admin`           | `ecm09`    | Admin                                                               | `ACCOUNT.ADMIN`     |
| `end_user`        | `ecm04`    | End user                                                            | `ACCOUNT.END_USER`  |
| `ecm01` … `ecm09` | tương ứng  | inject trực tiếp khi cần vai cụ thể (vd `ecm05` hay dùng làm Owner) | —                   |

Pattern khai báo test (thường chạy 2 vai Thủ thư + Admin):

```ts
const runTest = async (page: Page) => {
  /* ... các test.step ... */
};

test(
  "UC<nn> <code> - <mô tả> - Thủ thư" + ACCOUNT.LIBRARIAN,
  async ({ librarian }) => {
    await runTest(librarian);
  },
);
test("UC<nn> <code> - <mô tả> - Admin" + ACCOUNT.ADMIN, async ({ admin }) => {
  await runTest(admin);
});
```

Cần thêm vai phụ (vd kiểm tra quyền Owner): `async ({ librarian, ecm05 }) => runTest(librarian, ecm05)`.

---

## 3. TIMEOUT (ms) — [src/constant/timeout.ts](../src/constant/timeout.ts)

| Hằng               | Giá trị | Dùng cho                                 |
| ------------------ | ------- | ---------------------------------------- |
| `PAGE_LOADING`     | 60.000  | Chờ trang load / nút primary của màn     |
| `ELEMENT`          | 5.000   | Element thường                           |
| `CONTROL_LOADING`  | 5.000   | Chờ control hiện/ẩn (vd nút trong modal) |
| `ACTION_LOADING`   | 60.000  | Chờ sau action (mở modal, toast success) |
| `VALIDATE_WAITING` | 5.000   | Chờ validate giá trị input               |
| `HARD_WAITING`     | 30.000  | Hard wait sau khi vào màn (chờ JS init)  |
| `LOGIN_NAVIGATE`   | 120.000 | Điều hướng login                         |
| `DATA_LOADING`     | 30.000  | Chờ load dữ liệu (search, bảng)          |

> Khi search trong modal, nhiều chỗ dùng hard wait `page.waitForTimeout(10000)` sau `press("Enter")` vì kết quả load chậm.

---

## 4. Helper `PW` — [src/utils/PW.ts](../src/utils/PW.ts)

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

### 4a. Method theo nhóm

| Method                                                    | Tác dụng                                                                           |
| --------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `isVisible(testId, timeout?, msg?)`                       | assert element visible                                                             |
| `isEmpty(testId)`                                         | assert control rỗng (tự nhận loại theo prefix)                                     |
| `wait(ms)`                                                | `page.waitForTimeout`                                                              |
| `clickButton(testId)`                                     | click theo testId                                                                  |
| `inputText(testId, text)` / `inputTextArea(testId, text)` | điền text / textarea                                                               |
| `inputDropDownList(testId, optionText?)`                  | chọn dropdown; bỏ trống → chọn item đầu tiên                                       |
| `inputTreeDropDown(testId, optionText?, extraLocator?)`   | chọn tree dropdown                                                                 |
| `inputDropDownTagsList(testId, tags[])`                   | nhập nhiều tag                                                                     |
| `inputDatetime(testId, value)`                            | điền ngày                                                                          |
| `inputPeoplePicker(testId, value)`                        | nhập people picker, `value` dạng `"a,b"`                                           |
| `batchInput(data[], checkBranch?)`                        | điền cả form theo prefix testId (xem 4b)                                           |
| `valueShouldBe / valueShouldContain(testId, v)`           | assert giá trị control                                                             |
| `clearValue(testId)`                                      | xoá giá trị select/text                                                            |
| `getValue(testId)`                                        | lấy text của select                                                                |
| `checkListValueDropdownList(testId, values[])`            | assert đúng danh sách option                                                       |
| `checkListValueDropdownListTags(testId, values[])`        | assert tags đã chọn                                                                |
| `checkSearchDropdownHasOption(testId, keyword)`           | assert option search chứa keyword (cả không dấu)                                   |
| `checkSearchTreeDropdownHasOption(testId, keyword)`       | tương tự cho tree dropdown                                                         |
| `inputRelatedECM(testId, order?)`                         | mở pop-up liên quan, chọn dòng đầu, bấm `btn-add-related` (đặc thù UC có liên kết) |

### 4b. Quy ước prefix testId → control (dùng bởi `batchInput`)

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

`batchInput(data, checkBranch)`:

- `data`: mảng `{ testId, value, extraLocator? }` (thường là `<uc>DefaultData`).
- `checkBranch=true`: sau khi chọn `sel-loaiBoHoSo`, nếu là loại "dự án" sẽ hiện và tự điền thêm `sel-duAn`.
- Bỏ qua field không muốn điền bằng `.filter(...)` rồi xử lý riêng (vd people picker hay flaky → tách ra gọi `inputPeoplePicker`).

---

## 5. Selectors AntD dùng chung

Dự án dùng **Ant Design**, nên các selector sau lặp lại ở nhiều màn:

| Mục đích                  | Locator                                                                                                                                    |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| Modal đang mở             | `page.locator(".ant-modal-content:visible").last()`                                                                                        |
| Dòng bảng (level 0)       | `.ant-table-row.ant-table-row-level-0`                                                                                                     |
| Header cột bảng           | `.ant-table-thead th`                                                                                                                      |
| Checkbox chọn dòng        | `.ant-table-row .ant-checkbox-wrapper` (hover dòng trước khi click)                                                                        |
| Toast thành công          | `.ant-message-success`                                                                                                                     |
| Phân trang                | `.ant-pagination`; `li[title="Previous Page"]`, `li[title="Next Page"]`, `.ant-pagination-item-<n>`, active = `ant-pagination-item-active` |
| Nút theo nhãn             | `page.getByRole("button", { name: "Hủy" })`                                                                                                |
| Selection item của select | `.ant-select-selection-item` / placeholder rỗng = `.ant-select-selection-placeholder`                                                      |
| Nút "..." trên title modal | `getByTestId("btn-more")` — hover để mở dropdown menu                                                                                     |
| Mục "Kiểm tra phân quyền" | `getByTestId("btn-kiem-tra-phan-quyen")` — chỉ visible sau khi hover `btn-more`                                                            |

### Pattern kiểm tra button "Kiểm tra phân quyền" theo vai quyền

```ts
const trigger = userPage.getByTestId("btn-more");

// Vai CÓ quyền: trigger phải visible → hover → assert nút visible
await expect(trigger).toBeVisible({ timeout: TIMEOUT.CONTROL_LOADING });
await trigger.hover();
await expect(userPage.getByTestId("btn-kiem-tra-phan-quyen")).toBeVisible({
  timeout: TIMEOUT.CONTROL_LOADING,
});

// Vai KHÔNG có quyền:
// - trigger ẩn → thỏa mãn ngay (không có menu)
// - trigger hiện → hover rồi assert nút KHÔNG visible
const triggerVisible = await trigger.isVisible();
if (triggerVisible) {
  await trigger.hover();
  await expect(userPage.getByTestId("btn-kiem-tra-phan-quyen")).not.toBeVisible({
    timeout: TIMEOUT.CONTROL_LOADING,
  });
}
```

---

## 6. Lưu ý chung khi viết case

1. Vào màn xong, `pw.wait(TIMEOUT.HARD_WAITING)` trước khi thao tác nút đầu tiên (chờ JS init).
2. Sau `press("Enter")` để search trong modal, chờ `waitForTimeout(10000)` vì kết quả load chậm.
3. Hover dòng bảng trước khi click checkbox (checkbox chỉ hiện khi hover).
4. Mỗi case khai báo tối thiểu 2 test: Thủ thư (`librarian`) + Admin (`admin`).
5. Tên dữ liệu unique: `` `AT-...-${Date.now()}` `` để vừa tạo vừa tìm lại không trùng.
6. Người dùng thật khi cần (people picker) — value dạng `"a,b"`, cân nhắc tách khỏi `batchInput`.
