# CẤP ĐỘ 2 — Tự Viết Test Đầu Tiên

> **Điều kiện:** Đã hoàn thành Cấp độ 1 (chạy được test có sẵn) và Cấp độ 1.5 (dùng Codegen, đọc hiểu được code sinh ra).
> **Mục tiêu của cấp độ này:** Hiểu lượng lập trình **tối thiểu** cần cho tester, tự tìm được selector bằng F12, và **tự tay viết + chạy pass 7 test case** cho màn hình login của trang demo saucedemo.com.

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

- [ ] Giải thích được cho người khác: biến là gì, chuỗi là gì, `await` để làm gì
- [ ] Dùng F12 tìm được `data-test` của 4 phần tử trên trang saucedemo
- [ ] Kiểm tra được selector đúng/sai bằng Console (`document.querySelectorAll`)
- [ ] Tự viết file `login.spec.ts` với đủ 7 case và chạy **pass toàn bộ**
- [ ] Khi test fail, tự debug được theo quy trình 5 bước (mục 5.10)
- [ ] Tự viết lại được bằng tay (không dùng Codegen) đoạn code login mà Cấp độ 1.5 bạn mới chỉ đọc

Tick đủ → chuyển sang **Cấp độ 3**.

---

## Phần 0 — Lập Trình Tối Thiểu Cho Tester (30 phút)

Bạn không cần "học lập trình". Bạn chỉ cần hiểu **6 khái niệm** dưới đây — đủ để đọc và viết test.

### 0.1 Biến (variable) — hộp đựng giá trị có dán nhãn

```typescript
const URL = "https://www.saucedemo.com";
```

Đọc là: *tạo một cái hộp tên `URL`, bỏ vào đó dòng chữ địa chỉ web*. Từ đây về sau, chỗ nào viết `URL` nghĩa là đang dùng giá trị trong hộp. Đổi 1 chỗ → cả file đổi theo.

- `const` = hộp dán keo, bỏ vào rồi không đổi giá trị khác được (dùng gần như mọi lúc)
- `let` = hộp mở, thay được giá trị (hiếm khi cần)

### 0.2 Chuỗi (string) — đoạn chữ nằm trong dấu nháy

```typescript
"standard_user"        // nháy kép
'secret_sauce'         // nháy đơn — như nhau
`AT-TEST-${Date.now()}` // nháy huyền — CHO PHÉP nhét biến vào giữa bằng ${...}
```

Dòng thứ ba đọc là: *chữ "AT-TEST-" nối với con số thời gian hiện tại* → mỗi lần chạy ra tên khác nhau, ví dụ `AT-TEST-1720444800123`. Rất hay dùng để tránh trùng dữ liệu.

### 0.3 Object — hộp lớn chứa nhiều hộp nhỏ có tên

```typescript
const SEL = {
  username: '[data-test="username"]',
  password: '[data-test="password"]',
};
```

Đọc là: *hộp `SEL` có 2 ngăn tên `username` và `password`*. Lấy ra bằng dấu chấm: `SEL.username`.

### 0.4 Hàm (function) — chuỗi thao tác gói lại có tên, gọi là chạy

```typescript
const dangNhap = async (page) => {
  await page.goto(URL);
  await page.locator(SEL.username).fill("standard_user");
};
```

Đọc là: *gói 2 thao tác (mở trang, điền username) vào cái tên `dangNhap`*. Sau đó chỗ nào cần thì viết `await dangNhap(page);` — khỏi chép lại 2 dòng. Cấp độ 3 bạn sẽ dùng kỹ thuật này rất nhiều.

### 0.5 `async` / `await` — "làm xong bước này rồi hãy làm bước sau"

Thao tác trên web **cần thời gian** (trang phải load, nút phải hiện). `await` nghĩa là: **đợi thao tác này xong hẳn rồi mới chạy dòng tiếp theo**.

```typescript
await page.goto(URL);              // đợi trang mở xong...
await page.locator(SEL.username).fill("abc"); // ...rồi mới điền
```

Quy tắc bỏ túi:

- Hầu như **mọi dòng thao tác Playwright đều bắt đầu bằng `await`**
- Hàm nào bên trong có `await` thì phải khai báo `async` ở đầu
- **Quên `await` là lỗi số 1 của người mới** — test chạy loạn thứ tự, fail khó hiểu. Thấy fail kỳ lạ → kiểm tra thiếu `await` trước tiên

### 0.6 Comment — ghi chú cho người đọc, máy bỏ qua

```typescript
// dòng này máy không chạy, chỉ để giải thích
await page.goto(URL); // có thể ghi chú cuối dòng
```

**Hết rồi.** 6 khái niệm này + copy mẫu có sẵn = đủ viết test. Những thứ khác (vòng lặp, if/else...) học dần khi gặp.

---

## Phần 1 — Các Thao Tác Playwright Thường Gặp

File tham khảo đầy đủ trong project: **`tests/demo/demo-actions.spec.ts`** (mở ra đọc, có comment giải thích từng dòng). Dưới đây là tóm tắt để tra cứu.

### 1.1 Điều hướng

```typescript
await page.goto("https://example.com"); // mở URL
await page.reload();                    // tải lại trang
await page.goBack();                    // nút Back
await page.waitForURL("**/inventory.html"); // đợi URL đổi sang trang mới
```

### 1.2 Tìm element (Locator)

Trước khi click/điền vào đâu, phải "chỉ" cho Playwright biết element đó:

```typescript
// Theo data-test / data-testid (ưu tiên số 1)
page.locator('[data-test="username"]');
page.getByTestId("username"); // nếu project đã config testIdAttribute

// Theo vai trò + tên hiển thị (ưu tiên số 2 — rất bền)
page.getByRole("button", { name: "Login" });

// Theo chữ hiển thị
page.getByText("Đăng nhập");
page.getByPlaceholder("Nhập mật khẩu");
page.getByLabel("Tên đăng nhập");

// Theo CSS (kém bền — chỉ dùng khi không còn cách khác)
page.locator(".class-name");
page.locator("#element-id");

// Lọc và chọn trong danh sách
page.locator("li").filter({ hasText: "Mục cần tìm" });
page.locator(".item").first();  // phần tử đầu
page.locator(".item").last();   // phần tử cuối
page.locator(".item").nth(2);   // phần tử thứ 3 (đếm từ 0)
```

### 1.3 Thao tác chuột

```typescript
await locator.click();
await locator.dblclick();               // double click
await locator.click({ button: "right" }); // chuột phải
await locator.hover();                  // rê chuột vào (không click)
await locator.scrollIntoViewIfNeeded(); // cuộn đến element
```

### 1.4 Điền / xoá input

```typescript
await locator.fill("nội dung");  // XOÁ SẠCH rồi điền — dùng 95% thời gian
await locator.clear();           // xoá không điền
await locator.pressSequentially("abc", { delay: 50 }); // gõ từng ký tự (cho ô có autocomplete)

// Dropdown <select> chuẩn HTML
await locator.selectOption("value");
await locator.selectOption({ label: "Nhãn hiển thị" });

// Checkbox / Radio
await locator.check();
await locator.uncheck();
```

### 1.5 Phím đặc biệt

```typescript
await locator.press("Enter");     // xác nhận / submit
await locator.press("Tab");       // sang ô kế tiếp
await locator.press("Escape");    // đóng modal / dropdown
await locator.press("ArrowDown"); // mũi tên xuống (chọn trong dropdown)
await locator.press("Control+a"); // Ctrl+A chọn tất cả
```

### 1.6 Chờ đợi

```typescript
await locator.waitFor({ state: "visible" });  // đợi element hiện
await locator.waitFor({ state: "hidden" });   // đợi element biến mất
await page.waitForURL("**/dashboard");        // đợi URL đổi
await page.waitForLoadState("domcontentloaded"); // đợi trang load

// Hard wait — đợi "chay" theo thời gian. TRÁNH dùng nếu có cách trên
await page.waitForTimeout(1000); // đứng im 1 giây
```

### 1.7 Assertion — trái tim của test

`expect()` = câu hỏi "kết quả có đúng như mong đợi không?". Đúng → pass, sai → fail. **Test không có expect thì không phải là test** — chỉ là robot bấm lung tung.

```typescript
// Hiển thị
await expect(locator).toBeVisible();
await expect(locator).not.toBeVisible();

// Số lượng
await expect(locator).toHaveCount(6);

// Chữ
await expect(locator).toHaveText("Đăng nhập thành công"); // đúng 100%
await expect(locator).toContainText("thành công");        // chỉ cần chứa

// Giá trị ô nhập
await expect(locator).toHaveValue("nội dung đã điền");

// URL và tiêu đề trang
await expect(page).toHaveURL(/inventory/); // /.../ = chỉ cần URL chứa chữ này
await expect(page).toHaveTitle("Swag Labs");

// Trạng thái
await expect(locator).toBeChecked();
await expect(locator).toBeEnabled();
await expect(locator).toBeDisabled();

// Kèm message tự đặt — khi fail sẽ hiện dòng chữ này, dễ hiểu hơn nhiều
await expect(locator, {
  message: "Lỗi: không chuyển sang dashboard sau khi đăng nhập",
}).toBeVisible({ timeout: 10000 });
```

---

## Phần 2 — Tìm Selector Bằng F12

**Selector** = "địa chỉ" để Playwright tìm đúng element. Kỹ năng quan trọng nhất của cấp độ này.

### 2.1 Mở DevTools

- Nhấn **F12** trên bất kỳ trang web nào
- Hoặc chuột phải vào phần tử → **Inspect** (Kiểm tra)

### 2.2 Element Picker — chỉ đâu biết đó

1. Nhấn **Ctrl+Shift+C** (Windows) / **⌘+Shift+C** (Mac) — hoặc click biểu tượng mũi tên góc trên trái DevTools
2. Rê chuột lên phần tử muốn tìm — nó được tô sáng
3. Click → DevTools nhảy đến đúng dòng HTML của phần tử đó

### 2.3 Đọc `data-test` từ HTML

Nhìn dòng HTML được tô sáng, tìm attribute:

```html
<input data-test="username" class="input_error form_input" />
```

→ Selector là `[data-test="username"]`

**Thứ tự ưu tiên khi chọn selector:**

| Ưu tiên | Cách | Ví dụ | Vì sao |
| --- | --- | --- | --- |
| 1 | `data-test` / `data-testid` | `[data-test="username"]` | Dev đặt riêng cho test, gần như không bao giờ đổi |
| 2 | `getByRole` | `getByRole("button", { name: "Login" })` | Bám theo ngữ nghĩa, bền |
| 3 | `getByPlaceholder` / `getByLabel` | `getByPlaceholder("Password")` | Khá bền |
| 4 | `getByText` | `getByText("Đăng nhập")` | Vỡ khi đổi wording |
| 5 | CSS class / id | `.login-btn`, `#submit` | Vỡ khi đổi giao diện — hạn chế tối đa |

### 2.4 Kiểm tra selector bằng Console

Tab **Console** trong DevTools:

```javascript
document.querySelectorAll('[data-test="username"]');
// NodeList(1) → tìm thấy đúng 1 element ✓
// NodeList(0) → selector sai, tìm lại
// NodeList(5) → khớp tận 5 cái — cần selector cụ thể hơn
```

### 2.5 Codegen — máy ghi lại thao tác và tự sinh code (ôn lại Cấp độ 1.5)

```bash
npx playwright codegen https://www.saucedemo.com
```

Bạn đã dùng thành thạo ở Cấp độ 1.5. Từ giờ vai trò của nó: lên nháp nhanh và tra selector (nút Pick locator) khi F12 bất tiện.

> **Lưu ý:** code Codegen chỉ là **điểm khởi đầu** — thường dài dòng và chọn selector chưa tối ưu. Luôn xem lại và chỉnh theo bảng ưu tiên ở mục 2.3.

---

## Phần 3 — Thực Hành: Viết 7 Test Case Login

### 3.1 Trang demo: saucedemo.com

**https://www.saucedemo.com** — trang của Sauce Labs làm riêng để học automation. Tài khoản ghi sẵn ngay trên trang:

| Tài khoản | Mật khẩu | Kết quả |
| --- | --- | --- |
| `standard_user` | `secret_sauce` | Đăng nhập thành công |
| `locked_out_user` | `secret_sauce` | Tài khoản bị khoá |
| `problem_user` | `secret_sauce` | Vào được nhưng UI lỗi |
| `performance_glitch_user` | `secret_sauce` | Vào được nhưng chậm |

### 3.2 Bước 1 — Khám phá màn hình bằng F12

Tự làm, đừng nhìn đáp án vội:

1. Mở https://www.saucedemo.com, nhấn **F12**
2. Dùng Element Picker click vào ô **Username** → ghi lại `data-test`
3. Tương tự với ô **Password**, nút **Login**
4. Đăng nhập **sai** một lần để lỗi hiện ra → tìm `data-test` của khung lỗi

Kết quả cần có:

| Phần tử | Selector |
| --- | --- |
| Ô username | `[data-test="username"]` |
| Ô password | `[data-test="password"]` |
| Nút Login | `[data-test="login-button"]` |
| Thông báo lỗi | `[data-test="error"]` |
| Danh sách sản phẩm (sau khi vào) | `.inventory_list` |

### 3.3 Bước 2 — Tạo file spec

1. Trong VSCode, chuột phải thư mục `tests/saucedemo/` → **New File**
2. Đặt tên: `login.spec.ts` (nếu file đã tồn tại trong project, tạo `login-cua-toi.spec.ts` để tự luyện)

### 3.4 Bước 3 — Khung xương của spec file

```typescript
// 1. Import — "mượn" công cụ test và expect từ thư viện Playwright
import { test, expect } from "@playwright/test";

// 2. Khai báo URL và selector ở ĐẦU FILE — sau này đổi 1 chỗ là xong
const URL = "https://www.saucedemo.com";
const SEL = {
  username: '[data-test="username"]',
  password: '[data-test="password"]',
  loginButton: '[data-test="login-button"]',
  errorMessage: '[data-test="error"]',
};

// 3. Mỗi test case là 1 khối test("tên", async ({ page }) => { ... })
test("Tên mô tả test case", async ({ page }) => {
  // các bước thao tác
  // assertion kiểm tra kết quả
});
```

### 3.5 Case 1 — Đăng nhập thành công

```typescript
test("Login — Đăng nhập thành công với tài khoản hợp lệ", async ({ page }) => {
  // Bước 1: Mở trang
  await page.goto(URL);

  // Bước 2: Điền thông tin
  await page.locator(SEL.username).fill("standard_user");
  await page.locator(SEL.password).fill("secret_sauce");

  // Bước 3: Click Login
  await page.locator(SEL.loginButton).click();

  // Bước 4: Kiểm tra — cả 3 dấu hiệu của "thành công"
  await expect(page).toHaveURL(/inventory/);                     // URL đã đổi
  await expect(page.locator(".inventory_list")).toBeVisible();   // danh sách hiện
  await expect(page.locator(SEL.errorMessage)).not.toBeVisible(); // không có lỗi
});
```

Chạy thử ngay:

```bash
npx playwright test tests/saucedemo/login.spec.ts --headed -g "thành công"
```

Browser bật lên → tự điền → tự click → vào trang Products → terminal báo **passed** ✓. Chúc mừng — **bạn vừa viết test automation đầu tiên trong đời.**

### 3.6 Case 2 — Sai mật khẩu

```typescript
test("Login — Đăng nhập thất bại khi sai mật khẩu", async ({ page }) => {
  await page.goto(URL);
  await page.locator(SEL.username).fill("standard_user");
  await page.locator(SEL.password).fill("sai_mat_khau");
  await page.locator(SEL.loginButton).click();

  // Vẫn ở trang login
  await expect(page).toHaveURL(URL + "/");

  // Có thông báo lỗi đúng nội dung
  const errorMsg = page.locator(SEL.errorMessage);
  await expect(errorMsg).toBeVisible();
  await expect(errorMsg).toContainText(
    "Username and password do not match any user in this service",
  );
});
```

### 3.7 Case 3 — Để trống username

```typescript
test("Login — Đăng nhập thất bại khi để trống username", async ({ page }) => {
  await page.goto(URL);
  // Cố tình KHÔNG điền username
  await page.locator(SEL.password).fill("secret_sauce");
  await page.locator(SEL.loginButton).click();

  await expect(page.locator(SEL.errorMessage)).toBeVisible();
  await expect(page.locator(SEL.errorMessage)).toContainText(
    "Username is required",
  );
});
```

### 3.8 Case 4–7 — Tự viết (đây là bài kiểm tra của bạn)

Dựa theo mẫu 3 case trên, tự viết tiếp:

| # | Case | Kịch bản | Kết quả mong đợi |
| --- | --- | --- | --- |
| 4 | Trống password | điền username, bỏ trống password | Lỗi "Password is required" |
| 5 | Tài khoản khoá | `locked_out_user` + đúng mật khẩu | Lỗi chứa chữ "locked out" |
| 6 | Trống cả hai | click Login ngay khi vào trang | Lỗi "Username is required" |
| 7 | Đóng thông báo lỗi | tạo lỗi, rồi click nút X trên khung lỗi | Khung lỗi biến mất (`not.toBeVisible`) |

> Gợi ý case 7: dùng F12 tìm selector của nút X trong khung lỗi trước.
> Bí quá mới mở file mẫu `tests/saucedemo/login.spec.ts` của project ra so.

### 3.9 Chạy toàn bộ và kiểm tra

```bash
npx playwright test tests/saucedemo/login.spec.ts --headed
npx playwright show-report
```

Mục tiêu:

```
Running 7 tests using 1 worker
  ✓  1 ... ✓  2 ... ✓  3 ... ✓  4 ... ✓  5 ... ✓  6 ... ✓  7 ...
  7 passed (9.1s)
```

---

## Phần 4 — Khi Test Fail: Quy Trình Debug 5 Bước

Làm **theo đúng thứ tự**, đừng nhảy cóc:

1. **Đọc message lỗi** trong terminal — cặp "Expected / Received" nói lên 80%
2. **Chạy lại với `--headed`** — nhìn browser làm gì, dừng ở đâu
3. **Chèn `await page.pause();`** vào ngay trước dòng nghi ngờ rồi chạy `--headed` — test **dừng lại tại đó**, mở cửa sổ Playwright Inspector cho bạn xem trang, thử selector, rồi bấm tiếp từng bước. Đây là công cụ debug mạnh nhất. (Nhớ xoá dòng này khi xong!)
4. **Dùng UI Mode** (`npx playwright test --ui`) — xem timeline từng bước
5. **Mở HTML report → trace** — xem screenshot tại chính xác lúc fail

Bảng lỗi thường gặp của người mới:

| Lỗi | Nguyên nhân | Cách sửa |
| --- | --- | --- |
| `Timeout exceeded` | Element không xuất hiện trong thời gian chờ | Kiểm tra selector đúng chưa (F12 + Console); nếu đúng thì tăng timeout |
| `strict mode violation` | Selector khớp NHIỀU HƠN 1 element | Thêm `.filter()` / `.first()` / `.nth()` để chỉ đúng 1 cái |
| `locator.fill: Target closed` | Trang bị đóng / chuyển hướng trước khi kịp điền | Thêm `await page.waitForLoadState()` sau bước điều hướng |
| `Expected "..." Received ""` | Element có nhưng chữ chưa kịp load | Chờ bằng `waitFor` hoặc tăng timeout của `expect` |
| Test lúc pass lúc fail | Thường do thiếu `await` hoặc chờ chưa đủ | Soát từng dòng thao tác — dòng nào thiếu `await`? |

---

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

Quay lại checklist đầu tài liệu, tick đủ 6 ô. Nếu 7 case đều pass và bạn tự debug được ít nhất 1 lần fail — bạn đã có nền tảng thật sự, không phải chép máy móc.

**Tiếp theo:** tài liệu **Cấp độ 3 — Project Thật & Quy Trình Làm Việc Với Claude**: cấu trúc project nội bộ, quy ước của team, và quy trình dùng AI để tăng tốc viết test.

---

_Phiên bản 2.0 — tách từ tài liệu gốc, bổ sung phần lập trình tối thiểu và page.pause()._
