# Màn hình "Kho tài liệu" — Hồ sơ (Managed Records)

> Tài liệu kỹ thuật **đầy đủ** cho màn hình `/managed-records`.
> Dùng chung cho mọi UC thao tác trên màn này (UC29, UC77, …).
> Phần fixture/account/TIMEOUT/PW method dùng chung toàn dự án xem `tests/README.md`.

> 📌 **Phạm vi tài liệu — chỉ mô tả màn hình**: field, nút, testId, cấu trúc modal/bảng, thông báo,
> luồng thao tác. **Không** ghi logic test / kỳ vọng của case / quy tắc nghiệp vụ vào đây — chỗ của
> chúng là spec (`tests/<mục>/<mục>.<mã case>.spec.ts`), file bước dùng chung
> (`tests/<mục>/<mục>.steps.ts`) và file tiền điều kiện của mục (`tests/<mục>/<mục>.md`).
> Viết case mới **không** kéo theo việc cập nhật file này, trừ khi thông tin **màn hình** bị sai/thiếu.

---

## 0. File function đi kèm — `KHO_TAI_LIEU.function.ts`

**Mọi thao tác dài của màn này đã được đóng gói thành hàm** trong `src/screen-instructions/KHO_TAI_LIEU.function.ts`.
Khi viết spec: **import hàm từ file đó, KHÔNG copy code dài vào spec.**

```ts
import {
  createHoSo,
  addFolder,
  addDocument,
  FOLDER_PICKER,
  DOC_PERM,
  openPhanQuyenModal,
  switchToUserTab,
  fillUserPicker,
  searchInUserTab,
  selectFilterOption,
  getFilterDropdown,
  expandFolderInModal,
  checkItemVisible,
  checkTableCell,
  checkInheritanceColumn,
  checkUniquePermColumn,
  checkPermColumn,
  clickItemAndVerify,
  verifyPhanQuyenButtonForRole,
  getPhanQuyenModal,
  getPhanQuyenTableBody,
  getUserPicker,
  getUserTabSearchInput,
} from "../../../src/screen-instructions/KHO_TAI_LIEU.function";
```

| Hàm                                                                                                             | Mục tài liệu | Công dụng                                                                                           |
| --------------------------------------------------------------------------------------------------------------- | :----------: | --------------------------------------------------------------------------------------------------- |
| `createHoSo(page, pw, name, peoplePickers, { readFields })` → `{ recordUrl, values }`                           |      5       | Overload: đọc lại giá trị field (vd option đầu tiên tự chọn) NGAY TRONG FORM trước khi Lưu          |
| `chuyenHoatDong(page, pw, recordUrl?)`                                                                          |      5       | Chuyển hồ sơ "Khai báo" → "Hoạt động" (bắt buộc để end_user tìm thấy ở màn tra cứu)                 |
| `addFolder(page, pw, recordUrl, name, perm?)`                                                                   |      7a      | Tạo thư mục (tự goto reset DOM); `perm={account, pickerIndex}` dùng `FOLDER_PICKER`                 |
| `addDocument(page, pw, recordUrl, name, opts?)`                                                                 |      7b      | Tạo tài liệu; `opts={folderName?, perm?: {account, permTestId, clearInherited?}}` dùng `DOC_PERM`   |
| `openPhanQuyenModal(page, pw, recordUrl?)`                                                                      |    6a/6b     | Hover `btn-more` → click "Kiểm tra phân quyền"                                                      |
| `switchToUserTab(page)`                                                                                         |      6g      | Chuyển sang tab "Theo người dùng"                                                                   |
| `fillUserPicker(page, account, clearFirst?)`                                                                    |      6g      | Điền people picker "Người dùng/Nhóm" (mặc định xóa user hiện tại trước)                             |
| `searchInUserTab(page, keyword)`                                                                                |      6g      | Search "Tìm hồ sơ, tài liệu" trong tab người dùng                                                   |
| `selectFilterOption(page, "Quyền"\|"Phân loại", option)`                                                        |      6g      | Tick 1 option dropdown lọc rồi đóng dropdown                                                        |
| `expandFolderInModal(page, folderName)`                                                                         |      6d      | Expand thư mục trong bảng tree                                                                      |
| `checkItemVisible(page, itemName, shouldBeVisible, label)`                                                      |      6c      | Assert item hiện/ẩn trong bảng                                                                      |
| `checkTableCell(page, itemName, colIndex, expected, label)`                                                     |      6f      | Assert 1 ô bảng theo tên item + index cột                                                           |
| `checkInheritanceColumn` / `checkUniquePermColumn`                                                              |      6f      | Assert td[1] = "Kế thừa" / "Quyền riêng tư"                                                         |
| `checkPermColumn(page, itemName, perm, label)`                                                                  |      6g      | Assert td[2] tab người dùng = VIEW/EDIT/ADD/DOWNLOAD/OWNER                                          |
| `checkMultiPermColumn(page, itemName, perms[], label)`                                                          |      6g      | Assert td[2] chứa ĐỦ nhiều quyền hiệu lực & không có giá trị ngoài tập hợp lệ (`VALID_PERM_VALUES`) |
| `clickItemAndVerify(page, itemName, expectSuccess, label)`                                                      |      6e      | Click item, verify modal con hoặc toast lỗi                                                         |
| `verifyPhanQuyenButtonForRole(page, recordUrl, opts)`                                                           |    6a+6b     | Kiểm tra nút + tuỳ chọn theo vai (ROLE_CHECKS)                                                      |
| `getPhanQuyenModal` / `getPhanQuyenTableBody` / `getUserPicker` / `getUserTabSearchInput` / `getFilterDropdown` |    6c/6g     | Locator getter khi cần assert tùy biến                                                              |

> 📌 **Quy tắc bổ sung hàm mới**: khi phát hiện thao tác dài (> ~10 dòng) cần dùng ở ≥ 2 spec,
> **viết thành hàm vào `KHO_TAI_LIEU.function.ts`** rồi cập nhật bảng trên — không copy code dài vào từng spec.
> Các đoạn code chi tiết bên dưới (mục 5–7) là **tài liệu giải thích cơ chế bên trong hàm** — chỉ dùng khi cần viết thao tác biến thể.

---

## 1. Thông tin màn hình

| Mục          | Giá trị                                           |
| ------------ | ------------------------------------------------- |
| URL          | `/managed-records`                                |
| Hằng URL     | `uc29DefaultUrl` (export từ `uc29.default.data`)  |
| Data mẫu     | `uc29DefaultData` (export từ `uc29.default.data`) |
| Re-export UC | `uc77DefaultData/Url` ← `uc29.default.data`       |

---

## 2. Form "Tạo mới / Cập nhật Hồ sơ" — danh sách field

`batchInput()` tự route theo prefix testId (xem `tests/README.md` mục 4b).

| #   | testId                        | Loại          | required | Giá trị mẫu                       | Ghi chú                                                                       |
| --- | ----------------------------- | ------------- | :------: | --------------------------------- | ----------------------------------------------------------------------------- |
| 1   | `txt-tenHoSo`                 | text          |    ✔     | `"Hồ sơ tự động " + timestamp`    | Override bằng tên unique khi cần                                              |
| 2   | `tree-sel-mucLuuTru`          | tree-select   |    ✔     | (đầu tiên)                        | Mục lưu trữ                                                                   |
| 3   | `sel-companyInvestor`         | select        |    ✔     | (đầu tiên)                        | Công ty / Chủ đầu tư                                                          |
| 4   | `tree-sel-owner-department`   | tree-select   |    ✔     | (đầu tiên)                        | Đơn vị sở hữu                                                                 |
| 5   | `sel-loaiBoHoSo`              | select        |    ✔     | (đầu tiên)                        | Loại hồ sơ. Nếu loại "dự án" → hiện thêm `sel-duAn` (dùng `checkBranch=true`) |
| 6   | `sel-cauTrucHoSo`             | select        |          | (đầu tiên)                        | Cấu trúc hồ sơ                                                                |
| 7   | `sel-securityLevel`           | select        |          | (đầu tiên)                        | Độ mật                                                                        |
| 8   | `txt-soHieuHoSo`              | text          |          | `"123456"`                        | Số hiệu hồ sơ                                                                 |
| 9   | `sel-trangThai`               | select        |          | (đầu tiên)                        | Trạng thái hiển thị. `"Private"` → KHÔNG xuất hiện trong search liên quan     |
| 10  | `sel-storageTerm`             | select        |          | (đầu tiên)                        | Thời hạn lưu trữ                                                              |
| 11  | `sel-hardCopyStatus`          | select        |    ✔     | (đầu tiên)                        | Trạng thái bản cứng                                                           |
| 12  | `sel-tags-tuKhoa`             | tags (multi)  |          | `"từ khóa 1,từ khóa 2,từ khóa 3"` | Từ khóa                                                                       |
| 13  | `txa-ghiChu`                  | textarea      |          | `"Ghi chú tự động"`               | Ghi chú                                                                       |
| 14  | `pp-multi-usersRightOwner`    | people-picker |          | `"ecm05"`                         | Quyền Owner — **tách khỏi batchInput**, gọi riêng `inputPeoplePicker`         |
| 15  | `pp-multi-usersRightAdd`      | people-picker |          | `"ecm06"`                         | Quyền Thêm                                                                    |
| 16  | `pp-multi-usersRightEdit`     | people-picker |          | `"ecm07"`                         | Quyền Sửa                                                                     |
| 17  | `pp-multi-usersRightDownload` | people-picker |          | `"ecm08"`                         | Quyền Tải                                                                     |
| 18  | `pp-multi-usersRightViewers`  | people-picker |          | `"ecm09"`                         | Quyền Xem                                                                     |
| 19  | `btn-add-related-ecm`         | nút liên quan |          | —                                 | Xem mục 4. **Lọc khỏi batchInput** nếu không muốn thêm liên quan              |

---

## 3. Button / element của màn hình

| testId / locator                               | Mô tả                                                                 |
| ---------------------------------------------- | --------------------------------------------------------------------- |
| `btn-create-hstl`                              | Nút **Tạo mới** trên danh sách                                        |
| `btn-save`                                     | Nút **Lưu** trong form tạo/cập nhật                                   |
| `btn-close-modal`                              | Nút đóng modal form                                                   |
| `lbl-tab-thongTin`                             | Tab "Thông tin" ở màn chi tiết / cập nhật                             |
| `lbl-tenHoSo`                                  | Label tên hồ sơ trên màn chi tiết                                     |
| `btn-add-related-ecm`                          | Nút **+ Thêm thông tin liên quan** (dropdown: Hồ sơ / Tài liệu)       |
| `btn-add-related`                              | Nút **Thêm** trong pop-up liên quan (chỉ visible khi đã chọn ≥1 dòng) |
| `related-item-table`                           | Bảng HS/TL liên quan                                                  |
| `.cssMaHSTLChild` (trong `related-item-table`) | Cell mã HS/TL của từng dòng đã liên kết                               |
| `btn-more`                                     | Nút "..." (dropdown-trigger) trên title modal — hover để mở menu      |
| `btn-kiem-tra-phan-quyen`                      | Mục **Kiểm tra phân quyền** trong dropdown `btn-more`                 |

---

## 4. Hai pop-up "Thông tin liên quan"

### 4a. Pop-up "Tìm hồ sơ liên quan" — mở bằng click trực tiếp

```ts
await pw.clickButton("btn-add-related-ecm");
await page
  .getByText("Tìm hồ sơ liên quan")
  .waitFor({ timeout: TIMEOUT.ACTION_LOADING });
```

- Search placeholder: `Nhập tên hồ sơ, số hồ sơ`
- Cột bảng: `Mã hồ sơ`, `Tên hồ sơ`, `Độ mật`, `Thư mục lưu trữ`, `Dự án`

### 4b. Pop-up "Tìm tài liệu liên quan" — mở bằng hover dropdown → click

```ts
const parent = page
  .locator('[data-testid="btn-add-related-ecm"]')
  .locator("..");
await parent.locator(".ant-dropdown-trigger").hover();
await page.getByTestId("btn-options-related-file").click();
await expect(page.getByText("Tìm tài liệu liên quan")).toBeVisible({
  timeout: TIMEOUT.DATA_LOADING,
});
```

- Search placeholder: `Nhập tên tài liệu, mã tài liệu`
- Cột bảng: `Mã tài liệu`, `Tên tài liệu`, `Độ mật`, `Loại tài liệu`, `Đơn vị sở hữu`

### Pattern chọn dòng đầu trong pop-up (dùng chung)

```ts
const modalContent = page.locator(".ant-modal-content:visible").last();
await modalContent
  .locator(".ant-table-row.ant-table-row-level-0")
  .first()
  .hover();
await modalContent
  .locator(".ant-table-row.ant-table-row-level-0 .ant-checkbox-wrapper")
  .first()
  .click();
await expect(page.getByTestId("btn-add-related")).toBeVisible({
  timeout: TIMEOUT.CONTROL_LOADING,
});
await pw.clickButton("btn-add-related");
await pw.isVisible("related-item-table");
```

### Pattern chọn 1 hồ sơ/tài liệu CỤ THỂ theo tên

> ⚠️ `inputRelatedECM` và `btn-add-related-ecm` qua `batchInput` **chỉ tick dòng đầu tiên**.
> Muốn link tới phiếu xác định → lọc `btn-add-related-ecm` khỏi `batchInput` và search thủ công:

```ts
await pw.clickButton("btn-add-related-ecm");
await page
  .getByText("Tìm hồ sơ liên quan")
  .waitFor({ timeout: TIMEOUT.ACTION_LOADING });

const modalContent = page.locator(".ant-modal-content:visible").last();
const searchInput = modalContent.locator(
  'input[placeholder="Nhập tên hồ sơ, số hồ sơ"]',
);
await searchInput.fill(targetName);
await searchInput.press("Enter");
await page.waitForTimeout(10000); // kết quả load chậm

const firstRow = modalContent
  .locator(".ant-table-row.ant-table-row-level-0")
  .first();
await expect(firstRow).toContainText(targetName, {
  timeout: TIMEOUT.DATA_LOADING,
});
await firstRow.hover();
await modalContent
  .locator(".ant-table-row.ant-table-row-level-0 .ant-checkbox-wrapper")
  .first()
  .click();
await expect(page.getByTestId("btn-add-related")).toBeVisible({
  timeout: TIMEOUT.CONTROL_LOADING,
});
await pw.clickButton("btn-add-related");
await pw.isVisible("related-item-table");
```

### Pattern đọc / đếm dòng trong `related-item-table`

```ts
const relatedRows = page
  .getByTestId("related-item-table")
  .locator(".cssMaHSTLChild");
await expect(relatedRows).toHaveCount(1); // số dòng = số lần đã thêm
const code = ((await relatedRows.first().textContent()) || "").trim();
```

### Cấu trúc dòng trong `related-item-table` (màn chi tiết)

Các `<tr class="ant-table-row ant-table-row-level-1">` là dòng HS/TL đã liên kết:

- `td` thứ 2 (index 1): nút mở tab mới
- `td` thứ 3 (index 2): tên HS/TL

```ts
const rows = page
  .getByTestId("related-item-table")
  .locator("tr.ant-table-row.ant-table-row-level-1");
const row = rows.nth(i);
const name = ((await row.locator("td").nth(2).textContent()) || "").trim();
const [newTab] = await Promise.all([
  page.context().waitForEvent("page"),
  row.locator("td").nth(1).locator("button").click(),
]);
await newTab.waitForLoadState("domcontentloaded");
await expect(newTab.getByTestId("lbl-tenHoSo")).toContainText(name, {
  timeout: TIMEOUT.PAGE_LOADING,
});
await newTab.close();
```

---

## 5. Tạo mới 1 bộ hồ sơ hoàn chỉnh

> ✅ Dùng hàm **`createHoSo`** (KHO_TAI_LIEU.function.ts) — trả về `recordUrl`:

```ts
const ts = Date.now();

// Không gán quyền:
const recordUrl = await createHoSo(admin, pw, `AT-UC77-xxx-${ts}`);

// Gán quyền ngay khi tạo (peoplePickers):
const recordUrl2 = await createHoSo(admin, pw, `AT-UC77-yyy-${ts}`, [
  { testId: DOC_PERM.OWNER, account: "ecm05" },
]);

// Đọc lại giá trị field đã chọn (vd "Loại hồ sơ" bỏ trống -> hệ thống tự chọn option đầu tiên)
// NGAY TRONG FORM trước khi Lưu -> dùng để tái sử dụng giá trị đó (vd lọc lại đúng ở màn search):
const { recordUrl: recordUrl3, values } = await createHoSo(
  admin,
  pw,
  `AT-UC59-${ts}`,
  undefined,
  { readFields: ["sel-loaiBoHoSo"] },
);
const loaiBoHoSo = values["sel-loaiBoHoSo"];
```

Bên trong hàm: goto danh sách → `btn-create-hstl` → `batchInput` (lọc `pp-multi-*`, `txt-tenHoSo`, `btn-add-related-ecm`, `sel-cauTrucHoSo`; `checkBranch=true`) → điền tên → people pickers (nếu có) → `btn-save` → assert `.ant-message-success` → trả `page.url()`.

> ⚠️ `sel-cauTrucHoSo` bị lọc khỏi `batchInput` — testId này trùng với 1 field khác trên trang nền
> `/managed-records` (panel filter danh sách) nên `page.getByTestId("sel-cauTrucHoSo")` (`PW.getElement`,
> không scope theo modal) luôn bị strict-mode violation (khớp 2 phần tử) khi modal Tạo mới còn mở.
> Field này không bắt buộc nên bỏ qua an toàn — nếu cần điền, phải tự scope riêng vào
> `.ant-modal-content.last()` thay vì gọi `pwUser.inputDropDownList` trực tiếp.

### Lưu URL & mở lại phiếu

```ts
await pw.wait(TIMEOUT.ACTION_LOADING);
const recordUrl = page.url(); // form đã chuyển sang màn Cập nhật
await pw.clickButton("btn-close-modal");
await expect(page.getByTestId("btn-close-modal")).not.toBeVisible({
  timeout: TIMEOUT.ACTION_LOADING,
});

// Mở lại (cùng hoặc khác tài khoản):
await page.goto(recordUrl);
await pw.wait(TIMEOUT.HARD_WAITING);
await pw.clickButton("lbl-tab-thongTin"); // vào tab Thông tin trước khi đọc related-item-table
```

### Chuyển Hoạt động (có confirm modal)

```ts
await page.getByTestId("btn-chuyen-hoat-dong").click();
await page
  .locator(".ant-modal-confirm-btns")
  .getByRole("button", { name: "Chuyển hoạt động" })
  .click();
await expect(page.locator(".ant-message-notice")).toContainText("Thành công", {
  timeout: TIMEOUT.ACTION_LOADING,
});
```

---

## 6. Kiểm tra button "Kiểm tra phân quyền" theo vai

### 6a. Hiển thị nút theo vai

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

// Vai CÓ quyền (Owner): 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
// - 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,
    },
  );
}
```

### 6b. Click "Kiểm tra phân quyền" và kiểm tra tuỳ chọn hiển thị

Sau khi `btn-kiem-tra-phan-quyen` visible (tức đã hover `btn-more`), click vào nó rồi kiểm tra các tuỳ chọn xuất hiện:

- **Owner**: thấy cả 2 text `"Theo phân quyền"` **và** `"Theo người dùng"`
- **Vai khác** (Add/Edit/Download/Viewers): **chỉ** thấy `"Theo phân quyền"`, **không** thấy `"Theo người dùng"`

```ts
await userPage.getByTestId("btn-kiem-tra-phan-quyen").click();
await userPage.waitForTimeout(TIMEOUT.DATA_LOADING);

if (isOwner) {
  await expect(userPage.getByText("Theo phân quyền")).toBeVisible({
    timeout: TIMEOUT.CONTROL_LOADING,
  });
  await expect(userPage.getByText("Theo người dùng")).toBeVisible({
    timeout: TIMEOUT.CONTROL_LOADING,
  });
} else {
  await expect(userPage.getByText("Theo phân quyền")).toBeVisible({
    timeout: TIMEOUT.CONTROL_LOADING,
  });
  await expect(userPage.getByText("Theo người dùng")).not.toBeVisible({
    timeout: TIMEOUT.CONTROL_LOADING,
  });
}
```

### Pattern ROLE_CHECKS (nhiều vai song song, gộp cả 6a + 6b)

> ✅ Toàn bộ logic 6a + 6b đã gói trong hàm **`verifyPhanQuyenButtonForRole`** (KHO_TAI_LIEU.function.ts).
> `shouldSee` kiểm soát phần 6a; `isOwner` kiểm soát phần 6b.

```ts
const ROLE_CHECKS = [
  {
    userPage: ecm05Page,
    label: "ecm05 Owner",
    shouldSee: true,
    isOwner: true,
    closeAfter: false,
  },
  {
    userPage: ecm06Page,
    label: "ecm06 Viewer",
    shouldSee: false,
    isOwner: false,
    closeAfter: true,
  },
  {
    userPage: ecm04Page,
    label: "ecm04 Download",
    shouldSee: false,
    isOwner: false,
    closeAfter: true,
  },
  // thêm/bớt vai tại đây
];

await Promise.all(
  ROLE_CHECKS.map(({ userPage, label, shouldSee, isOwner, closeAfter }) =>
    test.step(`Kiểm tra [${label}]`, async () => {
      await verifyPhanQuyenButtonForRole(userPage, recordUrl, {
        shouldSee,
        isOwner,
        label,
      });
      if (closeAfter) await userPage.close();
    }),
  ),
);
```

### 6c. Cấu trúc bên trong modal "Phân quyền nâng cao"

Sau khi click `btn-kiem-tra-phan-quyen`, modal mở với:

| Element               | Locator                                    | Ghi chú                                                                       |
| --------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- |
| Title modal           | `getByTestId("lbl-modal-title")`           | Text = `"Phân quyền nâng cao"`                                                |
| Nút "Theo phân quyền" | `getByText("Theo phân quyền")`             | Luôn hiện với vai có quyền mở modal                                           |
| Nút "Theo người dùng" | `getByText("Theo người dùng")`             | Chỉ hiện với Owner BHS                                                        |
| Ô tìm kiếm            | `input[placeholder="Tìm hồ sơ, tài liệu"]` | Scope vào `.ant-modal-content.last()`; mặc định rỗng khi modal vừa mở lần đầu |
| Bảng tree             | `.ant-table-wrapper` → `.ant-table-tbody`  | Cây BHS / Thư mục / Tài liệu                                                  |

Các cột bảng (lấy từ `.ant-table-thead th`):
`"Tên hồ sơ/tài liệu"`, `"Kế thừa"`, `"Quyền Owner"`, `"Quyền tạo mới"`, `"Quyền cập nhật"`, `"Quyền tải file"`, `"Quyền xem"`.

**Cấu trúc DOM mỗi dòng `tr.ant-table-row`:**

```
Thư mục:
<td>
  <span/>                    ← bỏ qua (indent)
  <span/>                    ← trigger expand / collapse
  <button>tên thư mục</button>
</td>

Tài liệu:
<td>
  <span/>                    ← bỏ qua (indent)
  <button>tên tài liệu</button>
</td>
```

> ⚠️ Tài liệu bên trong thư mục **không hiển thị** cho đến khi thư mục được expand.
> Phải expand thư mục cha trước khi thao tác với tài liệu con.

### 6d. Expand thư mục trong bảng

> ✅ Dùng hàm **`expandFolderInModal(userPage, folderName)`** (KHO_TAI_LIEU.function.ts).

Cơ chế: click span thứ 2 (index 1) trong `td` đầu tiên của dòng thư mục — `span.nth(0)` là indent, `span.nth(1)` là trigger expand/collapse.

### 6e. Click vào tên BHS / Thư mục / Tài liệu trong bảng

Khi click vào `<button>` chứa tên item trong bảng, kết quả phụ thuộc vào quyền:

| Điều kiện                  | Kết quả                                                                                                                                  |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| User là **Owner** của item | Modal con xuất hiện: `lbl-modal-title` = `"Phân quyền cấu trúc bộ hồ sơ"` → đóng bằng `btn-close-modal`. Cấu trúc chi tiết xem mục 6e-1. |
| User **không phải Owner**  | Toast `.ant-message-notice-content` = `"Bạn không được phân quyền truy cập thư mục này"`                                                 |

> ⚠️ **Assert toast ngay sau `click()`** — không được `waitForTimeout` trước khi kiểm tra toast vì toast xuất hiện rồi biến mất rất nhanh. `waitForTimeout` chỉ đặt SAU khi đóng modal con (case thành công).

**Bảng quyền click theo vai điển hình:**

| Vai                            | Thấy                     | Click TM                                 | Click TL                              |
| ------------------------------ | ------------------------ | ---------------------------------------- | ------------------------------------- |
| Admin / Librarian / Owner BHS  | Tất cả TM, TL            | Thành công                               | Thành công                            |
| Owner TM                       | TM đó + TL bên trong     | TM mình owns → thành công; TM khác → lỗi | Thành công nếu owns TL, lỗi nếu không |
| Owner TL                       | TM cha (visible) + TL đó | Lỗi (không owns TM)                      | Thành công                            |
| Quyền không phải Owner trên TL | TM cha (visible) + TL đó | Lỗi                                      | Lỗi                                   |

> ✅ Dùng hàm **`clickItemAndVerify(userPage, itemName, expectSuccess, label)`** (KHO_TAI_LIEU.function.ts).

**Pattern mở modal + expand + click (đầy đủ):**

```ts
await openPhanQuyenModal(userPage, pwUser, recordUrl); // 1. Mở modal Phân quyền nâng cao
await expandFolderInModal(userPage, folderName); // 2. Expand thư mục cha trước khi click tài liệu con
await clickItemAndVerify(userPage, docName, expectSuccess, label); // 3. Click item và kiểm tra
```

### 6e-1. Cấu trúc modal con "Phân quyền cấu trúc bộ hồ sơ" (khảo sát 2026-07-20, bổ sung 2026-07-21)

> Khảo sát bằng Admin (luôn thành công khi click): tạo BHS → 1 Thư mục root (kế thừa, chưa phân quyền riêng) → 1 Tài liệu **trong** thư mục đó (kế thừa) → 1 Tài liệu **ngoài** thư mục/root (kế thừa) → lần lượt click cả 3 và dump toàn bộ `[data-testid]` + text bên trong modal con.

Modal này **rất đơn giản** — không có tab, không có bảng dữ liệu, không có field chỉnh sửa nào khác:

| Element            | Locator                                                                    | Ghi chú                                                   |
| ------------------ | -------------------------------------------------------------------------- | --------------------------------------------------------- |
| Title              | `getByTestId("lbl-modal-title")`                                           | Luôn = `"Phân quyền cấu trúc bộ hồ sơ"` cho mọi loại item |
| Nút đóng           | `getByTestId("btn-close-modal")`                                           | Icon X góc phải                                           |
| Nút "Ngắt kế thừa" | **Không có testId** — dùng `getByRole("button", { name: "Ngắt kế thừa" })` | Icon kéo màu đỏ; xem điều kiện xuất hiện bên dưới         |

**Khác biệt theo loại item và trạng thái kế thừa** (khảo sát bổ sung 2026-07-21 — tạo thêm 1 Thư mục và 1 Tài liệu đã bật sẵn "Phân quyền riêng" lúc tạo, cùng 1 Tài liệu kế thừa để test hành vi bấm nút):

| Loại item                                            | Trạng thái          | Nội dung modal                                                                                                                                                                                                         |
| ---------------------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Thư mục**                                          | Kế thừa             | Chỉ có title + nút đóng — không có action nào (thân modal trống)                                                                                                                                                       |
| **Thư mục**                                          | Đã phân quyền riêng | **Giống hệt** — vẫn chỉ có title + nút đóng, không có action nào. Thư mục **không bao giờ** có action trong modal này bất kể trạng thái (muốn ngắt/khôi phục kế thừa cho thư mục phải làm ở form Sửa thư mục — mục 7a) |
| **Tài liệu** (trong hoặc ngoài thư mục — giống nhau) | Kế thừa             | Title + nút **"Ngắt kế thừa"**                                                                                                                                                                                         |
| **Tài liệu**                                         | Đã phân quyền riêng | Title + 2 nút: **"Kế thừa quyền"** (khôi phục kế thừa — thay thế vị trí nút "Ngắt kế thừa") và **"Thêm"** (chưa khảo sát nút này mở ra gì — nghi là thêm người vào 1 trong 5 quyền trực tiếp từ đây)                   |

**Click nút "Ngắt kế thừa"** (trên tài liệu đang kế thừa) → mở **confirm dialog** (dạng `.ant-modal-confirm`, không có testid, chồng lên trên modal con):

| Element      | Locator                                     | Giá trị                                                                                       |
| ------------ | ------------------------------------------- | --------------------------------------------------------------------------------------------- |
| Title        | text                                        | `"Ngắt kế thừa quyền"`                                                                        |
| Nội dung     | text                                        | `"Thao tác này sẽ ngắt kế thừa quyền của thư mục/ tài liệu hiện tại. Bạn có muốn thực hiện?"` |
| Nút hủy      | `getByRole("button", { name: "Hủy bỏ" })`   | —                                                                                             |
| Nút xác nhận | `getByRole("button", { name: "Xác nhận" })` | —                                                                                             |

```ts
const modal = userPage.locator(".ant-modal-content").last();
await modal.getByRole("button", { name: "Ngắt kế thừa" }).click();
// Confirm dialog chồng lên trên — không waitForTimeout trước, bấm luôn:
await userPage.getByRole("button", { name: "Xác nhận" }).last().click();
```

> ⚠️ **Còn 1 điểm chưa quan sát trực tiếp được** (2 lần khảo sát đều gặp sự cố môi trường sitdev — lần đứng ở bước dọn dẹp do chính script debug lỗi, lần sau global auth setup bị kẹt SSO — không phải lỗi của cơ chế này): trạng thái **NGAY SAU** khi bấm "Xác nhận" trong confirm dialog — modal con "Phân quyền cấu trúc bộ hồ sơ" có tự đóng theo không, hay ở lại và đổi nút thành "Kế thừa quyền"/"Thêm" giống tài liệu đã phân quyền riêng; và cột "Kế thừa" (td[1]) ở bảng cha có đổi từ `"Kế thừa"` sang `"Quyền riêng tư"` ngay hay cần đóng/mở lại modal cha mới thấy cập nhật. Dựa trên việc tài liệu-đã-phân-quyền-riêng cho thấy đúng bộ nút "Kế thừa quyền"/"Thêm", nhiều khả năng hành vi sau xác nhận sẽ khớp — nhưng cần chạy lại khảo sát khi sitdev ổn định để xác nhận trực tiếp trước khi viết case dựa vào chi tiết này.

### 6e-2. Modal "Thêm" (mở từ nút "Thêm" — chỉ có khi Tài liệu đã phân quyền riêng)

> Mô tả do người dùng cung cấp trực tiếp (chưa tự khảo sát lại bằng script) — click nút "Thêm" trong modal con "Phân quyền cấu trúc bộ hồ sơ" (mục 6e-1, chỉ xuất hiện khi item là **Tài liệu đã phân quyền riêng**) mở ra 1 modal mới chồng lên trên.

| Element        | Locator                                                                                     | Ghi chú                                                                     |
| --------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| Người nhận     | people-picker (chưa rõ testId)                                                              | Chọn người nhận                                                             |
| Quyền           | select, testId dạng `sel-...` (chưa xác định chính xác)                                     | Placeholder = `"Chọn quyền"` (`.ant-select-selection-placeholder`). Click mở dropdown → 5 option: `"VIEW"`, `"EDIT"`, `"ADD"`, `"DOWNLOAD"`, `"OWNER"` |
| Nội dung        | `textarea[placeholder="Nhập nội dung"]`                                                      | —                                                                            |
| Nút "Gửi"       | `getByRole("button", { name: "Gửi" })`                                                        | Thực hiện thao tác                                                          |
| Nút "Đóng"      | `getByRole("button", { name: "Đóng" })`                                                       | Đóng modal, không thực hiện gì                                              |

Gửi thành công → toast `.ant-message-notice` chứa `"Thành công"`.

> ⚠️ **Chưa tự khảo sát — cần xác nhận trước khi viết case dùng modal này:**
> - testId chính xác của field "Quyền" (`sel-...`) và của people-picker "Người nhận"
> - Đây là **gửi yêu cầu/thông báo** cho người nhận (dựa theo tên field "Người nhận" + "Nội dung" giống 1 form gửi thông báo) hay **gán quyền trực tiếp** ngay lập tức cho người đó trên item — cần kiểm tra lại cột Quyền (mục 6f/6g) của người nhận sau khi Gửi để biết chắc.
> - Có validate field nào bắt buộc không (vd không chọn Quyền mà bấm Gửi thì sao)
> - Nút "Gửi" có testId riêng hay chỉ bắt được bằng text/role

### 6f. Đọc các cột trong bảng theo index `td`

Mỗi `tr.ant-table-row` trong bảng có 7 cột theo thứ tự:

| `td` index | Tên cột              | Giá trị điển hình                     |
| ---------- | -------------------- | ------------------------------------- |
| 0          | Tên hồ sơ / tài liệu | `<button>tên item</button>` bên trong |
| 1          | Kế thừa              | `"Kế thừa"` hoặc `"Quyền riêng tư"`   |
| 2          | Quyền Owner          | tên user hoặc rỗng                    |
| 3          | Quyền tạo mới        | tên user hoặc rỗng                    |
| 4          | Quyền cập nhật       | tên user hoặc rỗng                    |
| 5          | Quyền tải file       | tên user hoặc rỗng                    |
| 6          | Quyền xem            | tên user hoặc rỗng                    |

**Helper kiểm tra 1 ô cụ thể theo tên item và index cột:**

> ✅ Dùng hàm **`checkTableCell`** hoặc 2 wrapper **`checkInheritanceColumn`** / **`checkUniquePermColumn`** (KHO_TAI_LIEU.function.ts).

```ts
await checkTableCell(admin, docName, 2, "ecm05", "Tài liệu 1 Owner"); // ô bất kỳ theo index cột
await checkInheritanceColumn(admin, folderName, "Thư mục 1"); // td[1] = "Kế thừa"
await checkUniquePermColumn(admin, folderName2, "Thư mục 2"); // td[1] = "Quyền riêng tư"
```

> ⚠️ Với tài liệu **bên trong thư mục**, phải gọi `expandFolderInModal` trước — tài liệu ẩn cho đến khi expand.

### 6g. Tab "Theo người dùng" trong modal Phân quyền nâng cao

> Tất cả nội dung từ 6c–6f mô tả tab **"Theo phân quyền"** (tab mặc định khi mở modal).
> Mục này mô tả tab **"Theo người dùng"** — chỉ hiện với Owner BHS (xem 6b).

#### Cách mở tab

> ✅ Dùng hàm **`switchToUserTab(userPage)`** (KHO_TAI_LIEU.function.ts) — assert tuỳ chọn hiển thị rồi click `div#user`.

#### Các element bên trong tab

| Element                         | Locator                                                    | Ghi chú                           |
| ------------------------------- | ---------------------------------------------------------- | --------------------------------- |
| People picker "Người dùng/Nhóm" | xem pattern bên dưới                                       | Không có `data-testid`            |
| Ô search                        | `input[placeholder="Tìm hồ sơ, tài liệu"]` — `.last()`     | Có 2 cái trên trang, lấy cái cuối |
| Dropdown lọc "Quyền"            | `div.cssDropdownSelectField` filter `hasText: "Quyền"`     | Click mở → 5 checkbox             |
| Dropdown lọc "Phân loại"        | `div.cssDropdownSelectField` filter `hasText: "Phân loại"` | Click mở → 2 label checkbox       |

#### People picker "Người dùng/Nhóm"

> ✅ Dùng hàm **`fillUserPicker(userPage, account, clearFirst?)`** — mặc định `clearFirst=true` xóa user hiện tại (2 lần Backspace) trước khi điền.
> Locator getter: **`getUserPicker(userPage)`**.

Cơ chế locator: `span` text `"Người dùng/Nhóm"` và `div.people-picker` là **anh em cùng cấp** (siblings) — truy cập qua parent `.locator("..")`. Sau `fill` phải chờ ~5s cho kết quả search rồi mới `Enter`.

```ts
await fillUserPicker(admin, "ecm05"); // xóa user mặc định rồi điền ecm05
await fillUserPicker(admin, "ecm06", false); // điền thêm, không xóa
```

#### Ô search (lấy cái cuối)

> ✅ Dùng hàm **`searchInUserTab(userPage, keyword)`** — fill + Enter + chờ DATA_LOADING.
> Locator getter: **`getUserTabSearchInput(userPage)`** (có 2 input cùng placeholder trên trang — hàm lấy `.last()`).

```ts
await searchInUserTab(admin, docName1);
// Assert ô rỗng khi mở lần đầu:
await expect(getUserTabSearchInput(admin)).toHaveValue("");
```

#### Dropdown lọc "Quyền" (5 option) và "Phân loại" (2 option)

> ✅ Dùng hàm **`selectFilterOption(userPage, "Quyền" | "Phân loại", optionLabel)`** — mở dropdown, tick option, Escape đóng, chờ bảng load. Gọi lại lần nữa với cùng option → bỏ tick.
> Locator getter: **`getFilterDropdown(userPage, label)`** — dùng khi chỉ cần assert các option hiển thị.

- Option của "Quyền": `"VIEW" | "EDIT" | "ADD" | "DOWNLOAD" | "OWNER"`
- Option của "Phân loại": `"Kế thừa" | "Quyền riêng tư"`
- Mỗi option là 1 `label.ant-checkbox-wrapper` — cho phép chọn nhiều

```ts
await selectFilterOption(admin, "Quyền", "VIEW");
await selectFilterOption(admin, "Phân loại", "Kế thừa");

// Chỉ mở dropdown để assert option (không tick):
await getFilterDropdown(admin, "Phân loại").click();
await expect(
  admin.locator("label.ant-checkbox-wrapper").filter({ hasText: "Kế thừa" }),
).toBeVisible();
```

> ⚠️ Hai dropdown lọc dùng cùng class `cssDropdownSelectField` — luôn filter thêm `hasText` để tránh nhầm. Sau khi chọn xong, click vào vùng khác (hoặc nhấn Escape) để đóng dropdown trước khi assert bảng kết quả.

#### Cấu trúc cột bảng kết quả trong tab "Theo người dùng"

Bảng có 3 cột — cột 1 và 2 giống hệt tab "Theo phân quyền" (xem mục 6f), cột 3 là mới:

| `td` index | Tên cột              | Giá trị điển hình                                                                           |
| ---------- | -------------------- | ------------------------------------------------------------------------------------------- |
| 0          | Tên hồ sơ / tài liệu | `<button>tên item</button>` bên trong                                                       |
| 1          | Kế thừa              | `"Kế thừa"` hoặc `"Quyền riêng tư"`                                                         |
| 2          | Quyền                | Quyền của user được chọn trên item đó: `"VIEW"`, `"EDIT"`, `"ADD"`, `"DOWNLOAD"`, `"OWNER"` |

Giá trị cột `td[2]` tương ứng với các option trong dropdown lọc **"Quyền"**. Ví dụ: nếu user được gán `pp-multi-usersRightViewers` trên tài liệu thì cột Quyền hiển thị `"VIEW"`.

**Helper kiểm tra cột Quyền (td[2]) theo tên item:**

> ✅ Dùng hàm **`checkPermColumn(userPage, itemName, expectedPerm, label)`** (KHO_TAI_LIEU.function.ts).

```ts
await checkPermColumn(admin, docName, "VIEW", "Tài liệu 1"); // ecm05 có quyền Xem
await checkPermColumn(admin, folderName, "OWNER", "Thư mục 1"); // ecm05 là Owner
```

**User có NHIỀU quyền hiệu lực trên 1 item** (ví dụ được điền vào 2 people picker):

> ✅ Dùng hàm **`checkMultiPermColumn(userPage, itemName, expectedPerms[], label)`** — assert td[2] chứa đủ mọi quyền trong mảng VÀ không chứa token nào ngoài tập hợp lệ `VALID_PERM_VALUES` (OWNER/ADD/EDIT/DOWNLOAD/VIEW).

```ts
await checkMultiPermColumn(admin, docName, ["EDIT", "DOWNLOAD"], "Tài liệu 1"); // ecm06 có cả Sửa + Tải
```

---

## 7. Tab "Cấu trúc hồ sơ" — Tạo thư mục & tài liệu

Truy cập tab bằng `getByTestId("lbl-tab-cauTrucHoSo").click()` trong khi đang mở modal hồ sơ.

> ⚠️ **Bắt buộc goto lại `recordUrl` trước mỗi lần tạo thư mục hoặc tài liệu** để reset trang, xóa các DOM element thừa còn sót từ thao tác trước. Nếu bỏ qua, các `.ant-modal-content.last()` hoặc dropdown trigger có thể khớp nhầm element cũ.
> Hai hàm `addFolder` / `addDocument` **đã tự goto bên trong** — chỉ cần truyền `recordUrl`.

### 7a. Tạo thư mục mới

> ✅ Dùng hàm **`addFolder(userPage, pwUser, recordUrl, folderName, perm?)`** (KHO_TAI_LIEU.function.ts).

```ts
// Không phân quyền riêng (kế thừa từ BHS):
await addFolder(admin, pw, recordUrl, folderName1);

// Phân quyền riêng — pickerIndex dùng hằng FOLDER_PICKER (OWNER=0, ADD=1, EDIT=2, DOWNLOAD=3, VIEW=4):
await addFolder(admin, pw, recordUrl, folderName2, {
  account: "ecm05",
  pickerIndex: FOLDER_PICKER.OWNER,
});
await addFolder(admin, pw, recordUrl, folderName3, {
  account: "ecm05",
  pickerIndex: FOLDER_PICKER.VIEW,
});

// clearInherited: true — xóa hết chip đã tự điền sẵn do kế thừa quyền từ BHS TRƯỚC khi thêm
// account trên picker đó. Bật "Phân quyền riêng" chỉ tách thư mục khỏi kế thừa cho các thao tác
// SAU đó — picker vẫn giữ nguyên các account đã có quyền qua kế thừa tại thời điểm bật, nếu không
// xóa thì các account đó vẫn còn quyền trên thư mục dù đã bật phân quyền riêng.
await addFolder(admin, pw, recordUrl, folderName4, {
  account: "ecm05",
  pickerIndex: FOLDER_PICKER.VIEW,
  clearInherited: true,
});
```

Điểm cần biết về cơ chế bên trong (khi cần viết biến thể):

- Nút "Tạo mới": khớp `button span` filter `/^Tạo mới$/` trong `.ant-modal-content.last()`
- Fields scope vào tabpanel active: `div[role="tabpanel"][aria-hidden="false"]` — tự cập nhật khi switch tab
- Bật "Phân quyền riêng" có **2 dạng UI**: checkbox `input#uniquePermission` HOẶC tab "Phân quyền" → nút "Đặt quyền độc lập" (`.ant-alert-action`)
- People picker theo index `.people-picker.nth(i)` — chưa có data-testid
- ⚠️ Dropdown suggestion có thể che nút Xác nhận hoặc picker kế tiếp sau Enter — hàm tự nhấn Escape sau mỗi lần gán (kể cả khi gán nhiều account vào cùng 1 picker, vd nhiều Owner)
- Nút Xác nhận nằm ở footer modal (ngoài tabpanel) → scope `folderModal`

### 7b. Tạo tài liệu mới

> ✅ Dùng hàm **`addDocument(userPage, pwUser, recordUrl, docName, opts?)`** (KHO_TAI_LIEU.function.ts).

```ts
// Tài liệu ở root BHS, không phân quyền riêng:
await addDocument(admin, pw, recordUrl, docName1);

// Bên trong 1 thư mục:
await addDocument(admin, pw, recordUrl, docName2, { folderName: folderName1 });

// Phân quyền riêng — permTestId dùng hằng DOC_PERM (OWNER/ADD/EDIT/DOWNLOAD/VIEW):
await addDocument(admin, pw, recordUrl, docName3, {
  folderName: folderName2,
  perm: { account: "ecm05", permTestId: DOC_PERM.VIEW },
});

// clearInherited: true — xóa hết chip đã tự điền sẵn do kế thừa quyền từ BHS TRƯỚC khi thêm
// account. Bật "Phân quyền riêng" chỉ tách tài liệu khỏi kế thừa cho các thao tác SAU đó — picker
// vẫn giữ nguyên các account đã có quyền qua kế thừa tại thời điểm bật, nếu không xóa thì các
// account đó vẫn còn quyền trên tài liệu dù đã bật phân quyền riêng.
await addDocument(admin, pw, recordUrl, docName4, {
  perm: { account: "ecm05", permTestId: DOC_PERM.VIEW, clearInherited: true },
});
```

Điểm cần biết về cơ chế bên trong:

- Nút "Tạo mới" ở tab Cấu trúc hồ sơ là **split button**: click thẳng → tạo thư mục; hover `.ant-dropdown-trigger` → dropdown → chọn "Tài liệu"
- ⚠️ Tab pane active có **2** phần tử `.ant-dropdown-trigger` — phải scope `.ant-tabs-tabpane-active` và dùng `.first()`, nếu không → strict mode violation hoặc hover nhầm element ẩn
- Modal tạo tài liệu load chậm → chờ 10s sau khi click "Tài liệu"
- Chọn thư mục chứa qua `sel-folder-storage` (tree-select, `matchMode="contains"` vì option có prefix) — hàm tự kiểm tra `isVisible()` trước khi điền
- `opts.perm` nhận 1 quyền hoặc mảng nhiều quyền — sau mỗi `inputPeoplePicker` hàm tự nhấn Escape để đóng gợi ý còn sót lại (nếu không, gợi ý có thể che nút Lưu hoặc field kế tiếp)
- Sau toast "Thành công" hàm tự đóng modal bằng `btn-close-modal`

| Element                              | Scope                | Locator                                                                                                                                                          |
| ------------------------------------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Tab Cấu trúc hồ sơ                   | page                 | `getByTestId("lbl-tab-cauTrucHoSo")`                                                                                                                             |
| Nút Tạo mới (click thẳng → folder)   | folderModal          | `locator("button span").filter({ hasText: /^Tạo mới$/ })`                                                                                                        |
| **Active tabpanel (scope fields)**   | folderModal          | `locator('div[role="tabpanel"][aria-hidden="false"]')` — tự cập nhật khi switch tab                                                                              |
| Dropdown trigger → Tài liệu          | activeTabPane (page) | `.ant-tabs-tabpane-active` → `.ant-dropdown-trigger.first()` → hover                                                                                             |
| Item "Tài liệu" trong dropdown       | page                 | `li.ant-dropdown-menu-item` filter `hasText: "Tài liệu"`                                                                                                         |
| Tên thư mục                          | activeTabPane        | `input[type="text"][placeholder="Nhập tên"]`                                                                                                                     |
| Đơn vị nộp (tuỳ chọn)                | activeTabPane        | `tree-sel-submissionUnit` — kiểm tra `isVisible()` trước khi điền                                                                                                |
| Phân quyền riêng — dạng 1 (checkbox) | activeTabPane        | `input#uniquePermission`                                                                                                                                         |
| Phân quyền riêng — dạng 2 (tab nav)  | folderModal          | `.ant-tabs-nav-list .ant-tabs-tab` filter `hasText: "Phân quyền"` → click (tab nav nằm ngoài tabpanel)                                                           |
| Phân quyền riêng — dạng 2 (button)   | folderModal          | `.ant-alert-action` → `getByRole("button", { name: "Đặt quyền độc lập" })`                                                                                       |
| People picker Owner (folder)         | activeTabPane        | `.people-picker.nth(0)` — chưa có data-testid                                                                                                                    |
| People picker Tạo mới/Add (folder)   | activeTabPane        | `.people-picker.nth(1)` — chưa có data-testid                                                                                                                    |
| People picker Cập nhật (folder)      | activeTabPane        | `.people-picker.nth(2)` — chưa có data-testid                                                                                                                    |
| People picker Download (folder)      | activeTabPane        | `.people-picker.nth(3)` — chưa có data-testid                                                                                                                    |
| People picker Xem/View (folder)      | activeTabPane        | `.people-picker.nth(4)` — chưa có data-testid                                                                                                                    |
| Nút Xác nhận                         | folderModal          | `getByRole("button", { name: /Xác nhận/ })` (footer modal, ngoài tabpanel)                                                                                       |
| Tên tài liệu                         | docModal             | `[data-testid="txt-ten-tai-lieu"]`                                                                                                                               |
| Loại tài liệu                        | page                 | `sel-loai-tai-lieu` (dùng `pwUser.inputDropDownList`)                                                                                                            |
| Checkbox phân quyền riêng (doc)      | docModal             | `input#basic_hasUniquePermission`                                                                                                                                |
| Owner tài liệu                       | page                 | `pp-multi-usersRightOwner` (dùng `pwUser.inputPeoplePicker`)                                                                                                     |
| Thư mục chứa tài liệu (tuỳ chọn)     | page                 | `sel-folder-storage` (dùng `pwUser.inputTreeDropDown`) — tree-select; dùng `matchMode="contains"` vì option hiển thị có thể chứa prefix/suffix ngoài tên thư mục |
| Đóng modal tạo tài liệu              | page                 | `btn-close-modal` — click sau khi toast "Thành công" xuất hiện                                                                                                   |

---

## 8. Thông báo lỗi phân quyền

### Không có quyền truy cập phiếu

Khi user truy cập URL phiếu mà **không có quyền xem**, hệ thống hiển thị notification:

```ts
await expect(page.locator(".ant-notification-notice-message"), {
  message: "Lỗi: Thông báo 'Không có quyền truy cập' không hiển thị",
}).toContainText("Không có quyền truy cập", { timeout: TIMEOUT.PAGE_LOADING });
```

| Element                            | Mô tả                                                     |
| ---------------------------------- | --------------------------------------------------------- |
| `.ant-notification-notice-message` | Tiêu đề của notification (khác với `.ant-message-notice`) |

> ⚠️ Phân biệt với `.ant-message-notice` (toast ở giữa màn) — notification phân quyền dùng `.ant-notification-notice-message` (góc màn, dạng card).

---

## 9. Lưu ý quan trọng

1. `pw.wait(TIMEOUT.HARD_WAITING)` sau khi vào màn trước khi thao tác nút đầu tiên.
2. `pp-multi-usersRightOwner` **luôn tách** khỏi `batchInput`, gọi riêng `inputPeoplePicker("pp-multi-usersRightOwner", "ecm05")`.
3. `btn-add-related-ecm` **lọc khỏi batchInput** nếu muốn phiếu không có liên quan, hoặc muốn tự chọn phiếu cụ thể.
4. Hồ sơ trạng thái `Private` không xuất hiện trong kết quả tìm liên quan.
5. Sau `press("Enter")` trong pop-up search: `page.waitForTimeout(10000)` — kết quả load chậm.
6. `inputRelatedECM` chỉ chọn dòng đầu tiên — dùng khi không cần chọn phiếu cụ thể.
7. Khi mở phiếu (goto URL) và muốn **chỉnh sửa** bất kỳ field nào, phải click `lbl-tab-thongTin` trước — tab này kích hoạt form cập nhật. Thiếu bước này các field sẽ không thể input.
8. **Goto `recordUrl` trước mỗi lần tạo thư mục / tài liệu** — reset DOM, tránh nhầm `.ant-modal-content.last()` hoặc dropdown trigger với element cũ còn sót trên trang. Hai hàm `addFolder` / `addDocument` đã tự goto bên trong (xem mục 7).
9. **Thao tác dài mới → bổ sung vào `KHO_TAI_LIEU.function.ts`** (xem mục 0) — không copy code dài vào spec.

```ts
await page.goto(recordUrl);
await pw.wait(TIMEOUT.HARD_WAITING);
await pw.clickButton("lbl-tab-thongTin"); // bắt buộc trước khi chỉnh sửa
await pw.inputText("txt-tenHoSo", newName);
```
