# HƯỚNG DẪN CÀI ĐẶT & TẠO TEST CASE TỰ ĐỘNG VỚI PLAYWRIGHT

_(Tài liệu dành cho team QA/Tester)_

**Mục tiêu:** Giúp bạn tự tay cài đặt môi trường, cấu hình lưu phiên đăng nhập cho nhiều tài khoản, và tạo được một kịch bản luồng test xuyên suốt qua nhiều người dùng (Ví dụ: Người A tạo -> Người B duyệt) bằng cách ghép nối tính năng Record.

## PHẦN 1: CHUẨN BỊ MÔI TRƯỜNG

Trước khi bắt đầu, máy tính của bạn cần có sẵn 2 phần mềm sau. Nếu đã có, bạn có thể bỏ qua phần này.

1. **Node.js (Môi trường để chạy hệ thống):**
   - Truy cập trang chủ: [nodejs.org](https://nodejs.org)
   - Tải và cài đặt bản **LTS** (Bản ổn định nhất). Cứ bấm `Next` liên tục như cài game/phần mềm bình thường.

2. **Visual Studio Code (VS Code - Trình soạn thảo):**
   - Truy cập: [code.visualstudio.com](https://code.visualstudio.com)
   - Tải và cài đặt. Đây là nơi chúng ta sẽ làm việc chính.

## PHẦN 2: CÀI ĐẶT PLAYWRIGHT VÀO DỰ ÁN

- **Bước 1:** Tạo một thư mục trống trên máy tính để chứa code test (Ví dụ: `D:\Automation_Test_Project`).
- **Bước 2:** Mở VS Code lên. Chọn mục **File > Open Folder** và trỏ tới thư mục vừa tạo.
- **Bước 3:** Mở Terminal (Cửa sổ dòng lệnh) trong VS Code bằng cách: Chọn menu **Terminal > New Terminal** (hoặc dùng phím tắt `Ctrl + ` `).
- **Bước 4:** Copy dòng lệnh dưới đây, dán vào Terminal và ấn Enter:

```bash
npm init playwright@latest
npm install -D monocart-reporter
```

- **Bước 5:** Playwright sẽ hỏi bạn một vài câu hỏi thiết lập. Bạn cứ ấn Enter để chọn các giá trị mặc định:
  - `Do you want to use TypeScript or JavaScript?` -> Chọn **TypeScript**.
  - `Where to put your end-to-end tests?` -> Chọn **tests**.
  - `Add a GitHub Actions workflow?` -> Chọn **false** (nhấn `n`).
  - `Install Playwright browsers?` -> Chọn **true**.

> **Lưu ý:** Quá trình này sẽ mất khoảng 1-3 phút để tải các trình duyệt. Hãy kiên nhẫn đợi đến khi báo `Success!`.
> mở file playwright.config.ts lên, tìm đến dòng có chữ reporter: (thường nó nằm ở phần nửa trên của file) và sửa lại như sau:

```typescript
import { defineConfig } from "@playwright/test";

export default defineConfig({
  // ... (các cấu hình khác của bạn giữ nguyên)

  // Thay thế cấu hình reporter cũ bằng đoạn này:
  reporter: [
    ["list"], // Dòng này giúp terminal vẫn in ra chữ xanh/đỏ lúc đang chạy cho dễ nhìn
    [
      "monocart-reporter",
      {
        name: "Báo Cáo Automation", // Tên báo cáo hiển thị siêu to khổng lồ
        outputFile: "./monocart-report/index.html", // Đường dẫn lưu file báo cáo
      },
    ],
  ],

  // ... (các cấu hình bên dưới giữ nguyên)
});
```

## PHẦN 3: CẤU HÌNH LƯU PHIÊN ĐĂNG NHẬP (GLOBAL SETUP)

_(Mục đích: Đăng nhập 1 lần duy nhất cho mỗi tài khoản, lưu lại "Thẻ bài" để không bao giờ phải gõ User/Pass ở các lần test sau)._

- **Bước 1:** Tạo ở thư mục gốc 1 file có tên là `account.json` theo mẫu bên dưới, cần sử dụng bao nhiêu tài khoản thì setup bấy nhiêu

```typescript
[
  {
    id: "adminm", //id chỉ cần khác nhau là được
    email: "adminm@fxp.vn",
    password: "12312312312",
  },
  {
    id: "admin1",
    email: "admin1@fxp.vn",
    password: "1231231312312",
  },
];
```

- **Bước 2:** Trong thư mục `tests`, tạo một file tên là `auth.setup.ts`.
- **Bước 3:** Copy đoạn code sau vào file vừa tạo: (URL thay tương ứng với site cần test)

```typescript
import { test as setup } from "@playwright/test";
import accounts from "../account.json";
import fs from "fs";

const URL =
  "https://1work.sharepoint.com/sites/afotech/rfa.aspx#/rfa/dashboard";

for (const account of accounts) {
  setup(`Đăng nhập qua Microsoft ${account.email}`, async ({ page }) => {
    // Kiểm tra xem phiên đăng nhập trước đó còn hạn không
    const authFile = `playwright/.auth/${account.id}.json`;

    if (fs.existsSync(authFile)) {
      const stats = fs.statSync(authFile);
      const now = new Date().getTime();
      const mtime = new Date(stats.mtime).getTime();
      const diffMinutes = (now - mtime) / (1000 * 60);

      if (diffMinutes < 50) {
        console.log(
          `Token ${account.email} còn hạn (${diffMinutes.toFixed(1)} phút). Bỏ qua đăng nhập.`,
        );
        return;
      }

      console.log(
        `Token ${account.email} đã cũ (${diffMinutes.toFixed(1)} phút). Đăng nhập lại...`,
      );
    }

    // 1. Mở trang web của công ty (Nó sẽ tự redirect sang Microsoft)
    await page.goto(URL);

    // 2. Điền Email Microsoft và bấm Next
    await page.locator('input[type="email"]').fill(account.email);
    await page.locator('input[type="submit"][value="Next"]').click();

    // 3. Điền Password và bấm Sign In
    await page.locator('input[type="password"]').fill(account.password);
    await page.locator('input[type="submit"][value="Sign in"]').click();

    // 4. Xử lý màn hình "Stay signed in?" (Duy trì đăng nhập) của Microsoft
    try {
      // Microsoft thường đổi class/type, nhưng ID của nút Yes thường cố định là idSIButton9
      // Hoặc dùng nút có value="Yes" như bạn đã tìm ra
      const staySignedInBtn = page.locator('#idSIButton9, input[value="Yes"]');

      // Bắt Playwright phải ĐỢI TỐI ĐA 5 giây xem nút này có xuất hiện không
      await staySignedInBtn.waitFor({ state: "visible", timeout: 5000 });

      // Nếu trong 5s nút xuất hiện, tiến hành click
      await staySignedInBtn.click();
      console.log("Đã tự động click nút Duy trì đăng nhập!");
    } catch (error) {
      // Nếu đợi hết 5 giây mà không thấy nút này (do Microsoft không hỏi),
      // Playwright sẽ nhảy vào đây. Bỏ qua lỗi và đi tiếp bình thường.
      console.log("Không gặp màn hình Stay signed in, tiếp tục luồng...");
    }

    // 5. QUAN TRỌNG: Chờ web chuyển hướng về lại trang chủ công ty thành công
    // Cần đảm bảo UI trang chủ đã hiện ra thì cookies mới được ghi đầy đủ
    // (Thay 'text=Trang chủ' bằng một element nào đó luôn có mặt sau khi login xong)

    // 6. Lưu toàn bộ trạng thái vào file JSON
    await page
      .context()
      .storageState({ path: `playwright/.auth/${account.id}.json` });
    console.log(`Đã tạo xong thẻ bài cho: ${account.email}`);
  });
}
```

- **Bước 4:** Cập nhật file `playwright.config.ts` ở ngoài cùng dự án để Playwright biết cần chạy file Setup này trước tiên:

```typescript
// Tìm khối projects: [ ... ] và sửa thành:
projects: [
  {
      name: "setup",
      testMatch: /.*\.setup\.ts/,
    },
    {
      name: "chromium",
      use: {
        ...devices["Desktop Chrome"],
        // 1. Tự động chụp ảnh màn hình NGAY LÚC LỖI
        screenshot: "only-on-failure",

        // 2. Tự động quay video toàn bộ quá trình, nhưng chỉ LƯU LẠI KHI LỖI (để tiết kiệm dung lượng)
        video: "retain-on-failure",

        // 3. BẬT TRACE VIEWER: Ghi lại toàn bộ DOM, Network API, Console log... (Rất quan trọng)
        trace: "retain-on-failure",
      },
      dependencies: ["setup"],
    },

],
```

- **Bước 5:** Chạy lệnh `npx playwright test tests/auth.setup.ts --headed` để Playwright chạy đăng nhập và sinh ra 2 file thẻ bài `Adminm.json` và `Admin1.json` trong thư mục `.auth`.

## PHẦN 4: SỬ DỤNG RECORD (CODEGEN) CHO TỪNG TÀI KHOẢN

Tạo sẵn 1 file trong thư mục tests với tên luồng dễ nhận biết ví dụ `rfa-luong-tuyen-dung.spec.ts`
Nội dung tạo sẵn như bên dưới.

```typescript
import { test, expect } from "@playwright/test";
// tền luồng đặt sao cho dễ nhận biết (nếu nhiều luồng liên quan nên đặt tên giống nhau, sau này có thể chạy cùng lúc)
test("Luồng E2E: User Adminm Tạo RFA -> User Admin1 Phê duyệt", async ({
  browser,
}) => {
  // ==========================================
  // Các code tí nữa sẽ copy vào đây
});
```

### 1. Record các thao tác của Tài khoản Adminm (Người Tạo):

Mở Terminal, chạy lệnh sau để bật trình duyệt đã được đăng nhập sẵn bằng Tài khoản Adminm (hoặc tài khoản bất kỳ khác), url nhập đúng URL cần chạy:

```bash
npx playwright codegen --load-storage="playwright/.auth/Adminm.json" https://1work.sharepoint.com/sites/afotech/rfa.aspx#/rfa/dashboard
```

- Thao tác tạo một Request mới trên trình duyệt.
- Xong việc, code sẽ sinh ra ở Playwright Inspector, giống như bên dưới:

```typescript
import { test, expect } from "@playwright/test";

test.use({
  storageState: "playwright/.auth/Adminm.json",
});

test("test", async ({ page }) => {
  //======== Phần này sẽ copy file đã tạo trước đó là rfa-luong-tuyen-dung.spec.ts ==================
  // tức là chỉ copy phần nội dung bên trong hàm test()
  await page.goto(
    "https://1work.sharepoint.com/sites/afotech/rfa.aspx#/rfa/dashboard",
  );
  await page.locator("div").filter({ hasText: "Tạo mới" }).nth(5).click();
  await page
    .locator("div")
    .filter({ hasText: /^Chọn nhóm tờ trình\/yêu cầu$/ })
    .nth(4)
    .click();
  await page.getByText("Đề xuất tuyển dụng").click();
  await page.getByRole("combobox", { name: "Loại yêu cầu:" }).click();
  await page
    .getByText("Châu Á - Đề xuất tuyển dụng - Văn phòng Hà Nội")
    .click();
  await page
    .locator("div")
    .filter({ hasText: /^Chọn lý do tuyển dụng$/ })
    .nth(5)
    .click();
  await page.getByText("Tuyển dụng thay thế hoặc").click();
  await page.locator("#rc_select_11").click();
  await page.getByText("Các vị trí còn lại").click();
  await page
    .getByRole("textbox", { name: "Nhập chức danh tuyển dụng" })
    .click();
  await page
    .getByRole("textbox", { name: "Nhập chức danh tuyển dụng" })
    .fill("Giáo viên");
  await page.getByRole("spinbutton", { name: "Nhập số lượng đề nghị" }).click();
  await page
    .getByRole("spinbutton", { name: "Nhập số lượng đề nghị" })
    .fill("2");
  await page.locator("#rc_select_12").click();
  await page.getByText("Trên Đại học").click();
  await page.getByRole("textbox", { name: "Nhập chuyên ngành" }).click();
  await page
    .getByRole("textbox", { name: "Nhập chuyên ngành" })
    .fill("Ngôn Ngữ Anh");
  await page.locator(".ant-select-selection-overflow").first().click();
  await page.getByText("Nam").click();
  await page.locator("#rc_select_14").click();
  await page.getByText("Cần", { exact: true }).click();
  await page
    .locator(
      ".ant-col.ant-form-item-control.ant-col-xs-24.ant-col-lg-20 > .ant-form-item-control-input > .ant-form-item-control-input-content > .flex > .min-w-0 > .ant-select > .ant-select-selector > .ant-select-selection-overflow",
    )
    .click();
  await page.getByText("Không cần").click();
  await page.locator("#tiny-react_85124615551773110871020").click();
  await page.locator("#tiny-react_85124615551773110871020").fill("TOEIC 700");
  await page.getByRole("textbox", { name: "Trình độ tiếng Anh" }).click();
  await page.locator("#tiny-react_59468268961773110871020").click();
  await page.locator("#tiny-react_59468268961773110871020").fill("Giảng dạy");
  await page.getByRole("button", { name: "Lưu" }).click();
  await page.getByRole("button", { name: "Bắt đầu quy trình" }).click();
  //=============================================================================
});
```

Copy đoạn trên xong thì bổ sung đoạn bên dưới lên trên cùng, để định nghĩa đây là thao tác của Adminm
thay chữ page. thành pageAdminm.

```typescript
const contextAdminm = await browser.newContext({
  storageState: "playwright/.auth/Adminm.json",
});
const pageAdminm = await contextAdminm.newPage();
```

thêm dòng này vào cuối

```typescript
await pageAdminm.close(); // User pageAdminm xong việc, đóng tab
```

Code lúc này trong file `rfa-luong-tuyen-dung.spec.ts` sẽ như sau:

```typescript
import { test, expect } from "@playwright/test";

// Dùng { browser } thay vì { page } để mở nhiều ngữ cảnh độc lập
test("Luồng E2E: User Adminm Tạo RFA -> User Admin1 Phê duyệt", async ({
  browser,
}) => {
  const contextAdminm = await browser.newContext({
    storageState: "playwright/.auth/Adminm.json",
  });
  const pageAdminm = await contextAdminm.newPage();
  await pageAdminm.goto(
    "https://1work.sharepoint.com/sites/afotech/rfa.aspx#/rfa/dashboard",
  );
  await pageAdminm.locator("div").filter({ hasText: "Tạo mới" }).nth(5).click();
  await pageAdminm
    .locator("div")
    .filter({ hasText: /^Chọn nhóm tờ trình\/yêu cầu$/ })
    .nth(4)
    .click();
  await pageAdminm.getByText("Đề xuất tuyển dụng").click();
  await pageAdminm.getByRole("combobox", { name: "Loại yêu cầu:" }).click();
  await pageAdminm
    .getByText("Châu Á - Đề xuất tuyển dụng - Văn phòng Hà Nội")
    .click();
  await pageAdminm
    .getByRole("textbox", { name: "Nhập tiêu đề tờ trình - phiếu" })
    .click();
  await pageAdminm
    .getByRole("textbox", { name: "Nhập tiêu đề tờ trình - phiếu" })
    .fill("Đề xuất tuyển dụng 123123");
  await pageAdminm
    .locator("div")
    .filter({ hasText: /^Chọn lý do tuyển dụng$/ })
    .nth(5)
    .click();
  await pageAdminm.getByText("Tuyển dụng thay thế hoặc").click();
  await pageAdminm.locator("#rc_select_11").click();
  await pageAdminm.getByText("Các vị trí còn lại").click();
  await pageAdminm
    .getByRole("textbox", { name: "Nhập chức danh tuyển dụng" })
    .click();
  await pageAdminm
    .getByRole("textbox", { name: "Nhập chức danh tuyển dụng" })
    .fill("Giáo viên");
  await pageAdminm
    .getByRole("spinbutton", { name: "Nhập số lượng đề nghị" })
    .click();
  await pageAdminm
    .getByRole("spinbutton", { name: "Nhập số lượng đề nghị" })
    .fill("2");
  await pageAdminm.locator("#rc_select_12").click();
  await pageAdminm.getByText("Trên Đại học").click();
  await pageAdminm.getByRole("textbox", { name: "Nhập chuyên ngành" }).click();
  await pageAdminm
    .getByRole("textbox", { name: "Nhập chuyên ngành" })
    .fill("Ngôn Ngữ Anh");
  await pageAdminm.locator(".ant-select-selection-overflow").first().click();
  await pageAdminm.getByText("Nam").click();
  await pageAdminm.locator("#rc_select_14").click();
  await pageAdminm.getByText("Cần", { exact: true }).click();
  await pageAdminm
    .locator(
      ".ant-col.ant-form-item-control.ant-col-xs-24.ant-col-lg-20 > .ant-form-item-control-input > .ant-form-item-control-input-content > .flex > .min-w-0 > .ant-select > .ant-select-selector > .ant-select-selection-overflow",
    )
    .click();
  await pageAdminm.getByText("Không cần").click();
  await pageAdminm.locator("#tiny-react_85124615551773110871020").click();
  await pageAdminm
    .locator("#tiny-react_85124615551773110871020")
    .fill("TOEIC 700");
  await pageAdminm.getByRole("textbox", { name: "Trình độ tiếng Anh" }).click();
  await pageAdminm.locator("#tiny-react_59468268961773110871020").click();
  await pageAdminm
    .locator("#tiny-react_59468268961773110871020")
    .fill("Giảng dạy");
  await pageAdminm.getByRole("button", { name: "Lưu" }).click();
  await pageAdminm.getByRole("button", { name: "Bắt đầu quy trình" }).click();
});
```

### 2. Record các thao tác của Tài khoản Admin1 (Người Duyệt):

Ở bước 1 chúng ta đã tạo 1 phiếu với text dễ tìm kiếm ở tiêu đề là `Đề xuất tuyển dụng 123123`, đoạn `123123` này sẽ là text tìm kiếm vì nó không trùng với gì.
Tiếp tục chạy lệnh với Tài khoản Admin1:

```bash
npx playwright codegen --load-storage="playwright/.auth/Admin1.json" https://1work.sharepoint.com/sites/afotech/rfa.aspx#/rfa/dashboard
```

Sau khi ghép tương tự như bước 1, chúng ta sẽ có file tổng hợp sau đây

```typescript
import { test, expect } from "@playwright/test";

// Dùng { browser } thay vì { page } để mở nhiều ngữ cảnh độc lập
test("Luồng E2E: User Adminm Tạo RFA -> User Admin1 Phê duyệt", async ({
  browser,
}) => {
  const contextAdminm = await browser.newContext({
    storageState: "playwright/.auth/Adminm.json",
  });
  const pageAdminm = await contextAdminm.newPage();
  await pageAdminm.goto(
    "https://1work.sharepoint.com/sites/afotech/rfa.aspx#/rfa/dashboard",
  );
  await pageAdminm.locator("div").filter({ hasText: "Tạo mới" }).nth(5).click();
  await pageAdminm
    .locator("div")
    .filter({ hasText: /^Chọn nhóm tờ trình\/yêu cầu$/ })
    .nth(4)
    .click();
  await pageAdminm.getByText("Đề xuất tuyển dụng").click();
  await pageAdminm.getByRole("combobox", { name: "Loại yêu cầu:" }).click();
  await pageAdminm
    .getByText("Châu Á - Đề xuất tuyển dụng - Văn phòng Hà Nội")
    .click();
  await pageAdminm
    .getByRole("textbox", { name: "Nhập tiêu đề tờ trình - phiếu" })
    .click();
  await pageAdminm
    .getByRole("textbox", { name: "Nhập tiêu đề tờ trình - phiếu" })
    .fill("Đề xuất tuyển dụng 123123");
  await pageAdminm
    .locator("div")
    .filter({ hasText: /^Chọn lý do tuyển dụng$/ })
    .nth(5)
    .click();
  await pageAdminm.getByText("Tuyển dụng thay thế hoặc").click();
  await pageAdminm.locator("#rc_select_11").click();
  await pageAdminm.getByText("Các vị trí còn lại").click();
  await pageAdminm
    .getByRole("textbox", { name: "Nhập chức danh tuyển dụng" })
    .click();
  await pageAdminm
    .getByRole("textbox", { name: "Nhập chức danh tuyển dụng" })
    .fill("Giáo viên");
  await pageAdminm
    .getByRole("spinbutton", { name: "Nhập số lượng đề nghị" })
    .click();
  await pageAdminm
    .getByRole("spinbutton", { name: "Nhập số lượng đề nghị" })
    .fill("2");
  await pageAdminm.locator("#rc_select_12").click();
  await pageAdminm.getByText("Trên Đại học").click();
  await pageAdminm.getByRole("textbox", { name: "Nhập chuyên ngành" }).click();
  await pageAdminm
    .getByRole("textbox", { name: "Nhập chuyên ngành" })
    .fill("Ngôn Ngữ Anh");
  await pageAdminm.locator(".ant-select-selection-overflow").first().click();
  await pageAdminm.getByText("Nam").click();
  await pageAdminm.locator("#rc_select_14").click();
  await pageAdminm.getByText("Cần", { exact: true }).click();
  await pageAdminm
    .locator(
      ".ant-col.ant-form-item-control.ant-col-xs-24.ant-col-lg-20 > .ant-form-item-control-input > .ant-form-item-control-input-content > .flex > .min-w-0 > .ant-select > .ant-select-selector > .ant-select-selection-overflow",
    )
    .click();
  await pageAdminm.getByText("Không cần").click();
  await pageAdminm.locator("#tiny-react_85124615551773110871020").click();
  await pageAdminm
    .locator("#tiny-react_85124615551773110871020")
    .fill("TOEIC 700");
  await pageAdminm.getByRole("textbox", { name: "Trình độ tiếng Anh" }).click();
  await pageAdminm.locator("#tiny-react_59468268961773110871020").click();
  await pageAdminm
    .locator("#tiny-react_59468268961773110871020")
    .fill("Giảng dạy");
  await pageAdminm.getByRole("button", { name: "Lưu" }).click();
  await pageAdminm.getByRole("button", { name: "Bắt đầu quy trình" }).click();
  await pageAdminm.close();
  //============================ Thao tác của Admin1 ============================
  const contextAdmin1 = await browser.newContext({
    storageState: "playwright/.auth/Admin1.json",
  });
  const pageAdmin1 = await contextAdmin1.newPage();
  await pageAdmin1.goto(
    "https://1work.sharepoint.com/sites/afotech/rfa.aspx#/rfa/dashboard",
  );
  await pageAdmin1
    .getByRole("textbox", { name: "Nhập mã, từ khóa để tìm tờ tr" })
    .click();
  await pageAdmin1
    .getByRole("textbox", { name: "Nhập mã, từ khóa để tìm tờ tr" })
    .fill("123123");
  await pageAdmin1
    .getByRole("textbox", { name: "Nhập mã, từ khóa để tìm tờ tr" })
    .press("Enter");
  await pageAdmin1
    .getByRole("button", { name: "Đề xuất tuyển dụng 123123" })
    .click();
  await pageAdmin1.getByRole("button", { name: "Đồng ý" }).click();
  await pageAdmin1
    .locator(
      ".ant-select.w-full.\\!flex.peoplePickerPopup.xs500\\:w-\\[90\\%\\] > .ant-select-selector",
    )
    .click();
  await pageAdmin1.locator("._focusable_link_qjfl8_1.\\!flex").click();
  await pageAdmin1
    .getByRole("textbox", { name: "Nhập tên, email người dùng/" })
    .click();
  await pageAdmin1
    .getByRole("textbox", { name: "Nhập tên, email người dùng/" })
    .fill("adminm");
  await pageAdmin1
    .getByRole("row", { name: "Mai Chung Anh Nhân viên kiểm" })
    .getByLabel("", { exact: true })
    .check();
  await pageAdmin1
    .getByLabel("Chọn người dùng/nhóm người dù")
    .getByRole("button", { name: "Thêm" })
    .click();
  await pageAdmin1
    .getByRole("dialog", { name: "Đồng ý", exact: true })
    .getByRole("textbox")
    .click();
  await pageAdmin1
    .getByRole("dialog", { name: "Đồng ý", exact: true })
    .getByRole("textbox")
    .fill("Oke");
  await pageAdmin1.getByRole("button", { name: "Gửi" }).click();
  await pageAdmin1.close();
});
```

## PHẦN 5: CHẠY TEST VÀ XEM BÁO CÁO (REPORT)

copy 2 lệnh này vào mục `scripts` trong `package.json`

```typescript
 "test": "npx playwright test",
 "report": "npx playwright show-report monocart-report"
```

Để kiểm tra xem hệ thống tự động chuyền tay nhau thế nào, bạn hãy chạy lệnh:

```bash
npx playwright test --headed
```

còn nếu không cần hiển thị giao diện, cho autotest chạy ngầm thì bỏ `--headed` đi, hoặc chạy `npm run test`

**Cách đọc Báo Cáo:**
Khi chạy xong (đặc biệt là nếu test bị lỗi/Failed), bạn dùng lệnh sau để mở HTML Report:

```bash
npx playwright show-report monocart-report
```

hoặc đơn giản là gõ `npm run report` nếu đã config `scripts`

- Báo cáo sẽ hiển thị rõ lỗi ở dòng code nào.
- Cuộn xuống dưới cùng của Report, bạn sẽ thấy tính năng **Trace Viewer**. Bấm vào đó, bạn có thể xem lại video ghi hình toàn bộ quá trình test, soi chi tiết các API đã gọi (Network) và lỗi Javascript (Console) nếu có, giúp Developer sửa lỗi cực nhanh.

# TIPS.

Ở trên chúng ta có tạo ra 1 phiếu với tên cố định là `Đề xuất tuyển dụng 123123`.
tuy nhiên nếu chạy test case này lần thứ 2, thì sẽ lại tạo ra 1 phiếu có tên như thế nữa, dẫn đến khó xác định và bấm được vào item.
nên chúng ta cần thay đổi số 123123 sau mỗi lần chạy, bằng cách sử dụng thời gian tính đến mili giây hiện tại.

ở đây chúng ta sẽ tạo ra 1 biến chung là title và gắn thời gian kèm với tiêu đề chúng ta muốn.

```typescript
const title = `Đề xuất tuyển dụng ${new Date().getTime()}`;

//thay thế vào như sau
//thay
await pageAdminm
  .getByRole("textbox", { name: "Nhập tiêu đề tờ trình - phiếu" })
  .fill("Đề xuất tuyển dụng 123123");
//thành
await pageAdminm
  .getByRole("textbox", { name: "Nhập tiêu đề tờ trình - phiếu" })
  .fill(title);
// và đoạn tìm kiếm
await pageAdmin1
  .getByRole("textbox", { name: "Nhập mã, từ khóa để tìm tờ tr" })
  .click();
await pageAdmin1
  .getByRole("textbox", { name: "Nhập mã, từ khóa để tìm tờ tr" })
  .fill(title);
await pageAdmin1
  .getByRole("textbox", { name: "Nhập mã, từ khóa để tìm tờ tr" })
  .press("Enter");
await pageAdmin1.getByRole("button", { name: title }).click();
```

Đây là các bước cơ bản để tạo ra 1 bộ test với Playwright.

Các lỗi có thể gặp:
Test timeout of 30000ms exceeded: do trang web load quá chậm, trong 60s không thể load được 1 nội dung nào đó, cần tăng thời gian chờ lên.
truy cập file playwright.config.ts, tăng 60 lên cao hơn ở dòng như bên dưới

```typescript
export default defineConfig({
  //  (mặc định là 30s)
  timeout: 30 * 1000,
  ...
})
```

Error: locator.click: Test ended.
Call log: - waiting for locator('#tiny-react_85124615551773110871020')

Lỗi này do khi chúng ta ghi lại thao tác, thì Playwright đã lấy Id của ô để ghi lại, tuy nhiên Id này lại thay đổi mỗi lần load ở số cuối 85124615551773110871020

để sửa chúng ta sẽ thay thế id thành place holder của ô

```typescript
await pageAdminm.locator("#tiny-react_85124615551773110871020").click();
await pageAdminm
  .locator("#tiny-react_85124615551773110871020")
  .fill("TOEIC 700");
await pageAdminm.getByRole("textbox", { name: "Trình độ tiếng Anh" }).click();
await pageAdminm.locator("#tiny-react_59468268961773110871020").click();
await pageAdminm
  .locator("#tiny-react_59468268961773110871020")
  .fill("Giảng dạy");
```

Trường hợp với rich textbox tạm thời cần Dev can thiệp để xử lý sau
