# NỀN TẢNG WEB: HTML — CSS — JAVASCRIPT (Cho Automation Tester)

> **Đây KHÔNG phải khóa học web dev.** Tài liệu này chỉ dạy đúng phần HTML/CSS/JS
> mà một automation tester cần để: đọc được cấu trúc trang qua F12, hiểu vì sao
> chọn selector này mà không chọn selector khác, và đọc/viết được script test.
> Không có phần dựng trang, không có phần thiết kế giao diện.
>
> **Vị trí trong lộ trình:** đọc tài liệu này sau khi xong **Cấp độ 1** (đã chạy
> được test có sẵn) và trước/song song với **Cấp độ 2** (tự viết test). Không cần
> học hết một lượt — mục nào thấy đang cần thì đọc mục đó.

## Mục lục

1. [HTML tối thiểu — đọc hiểu F12](#1-html)
2. [CSS tối thiểu — hiểu selector](#2-css)
3. [JavaScript tối thiểu — đọc và viết script](#3-js)

---

## 1. HTML — Đọc Hiểu F12 <a name="1-html"></a>

### 1.1 HTML là gì

Mọi trang web đều là một cây các "thẻ" (tag) lồng nhau. Trình duyệt đọc cây này và vẽ ra thành giao diện bạn thấy. F12 (DevTools) cho bạn xem thẳng cái cây đó dưới dạng chữ.

```html
<div class="login-form">
  <input data-test="username" type="text" placeholder="Username" />
  <input data-test="password" type="password" placeholder="Password" />
  <button data-test="login-button">Login</button>
</div>
```

Đọc cây này: có 1 khối `div` (khung chứa) tên class là `login-form`, bên trong có 2 ô nhập (`input`) và 1 nút (`button`). Đây chính xác là những gì bạn nhìn thấy khi F12 vào trang login.

### 1.2 Cấu tạo một thẻ

```html
<input data-test="username" type="text" placeholder="Username" /> └─┬─┘
└──────────┬───────┘ └────┬────┘ └───────┬───────┘ tên attribute attribute
attribute
```

- **Tên thẻ** (`input`, `button`, `div`, `a`...) — loại phần tử
- **Attribute** — cặp `tên="giá trị"` mô tả thêm về thẻ, đây chính là nguồn của selector

### 1.3 Các thẻ hay gặp và ý nghĩa cho việc test

| Thẻ                       | Là gì                                   | Thao tác Playwright thường dùng                  |
| ------------------------- | --------------------------------------- | ------------------------------------------------ |
| `<input>`                 | Ô nhập liệu                             | `.fill()`, `.clear()`                            |
| `<button>`                | Nút bấm                                 | `.click()`                                       |
| `<a>`                     | Đường link                              | `.click()`                                       |
| `<select>`                | Dropdown kiểu HTML chuẩn                | `.selectOption()`                                |
| `<div>`, `<span>`         | Khung/khối chứa, không có hành vi riêng | dùng làm container để tìm phần tử con            |
| `<table>`, `<tr>`, `<td>` | Bảng, dòng, ô                           | dùng để định vị hàng trong danh sách             |
| `<form>`                  | Khối chứa cả 1 form                     | ít khi thao tác trực tiếp, chỉ để nhận diện vùng |

### 1.4 Attribute quan trọng nhất với tester: `data-test` / `data-testid`

Đây là attribute **dev cố tình đặt riêng cho việc test**, không phục vụ hiển thị. Vì cố tình đặt cho test nên nó **ổn định nhất** — dev đổi giao diện, đổi màu, đổi vị trí, nhưng hiếm khi đổi `data-test`. Đây là lý do nó luôn đứng đầu bảng ưu tiên chọn selector.

```html
<button data-test="btn-luu" class="ant-btn ant-btn-primary" style="color: blue">
  Lưu
</button>
```

3 attribute trên cùng 1 thẻ, nhưng chỉ `data-test="btn-luu"` đáng tin cậy để làm selector — `class` và `style` phục vụ hiển thị, đổi theme là đổi ngay.

### 1.5 Attribute khác cũng hữu ích khi không có `data-test`

| Attribute     | Ví dụ                      | Ghi chú                                                                          |
| ------------- | -------------------------- | -------------------------------------------------------------------------------- |
| `id`          | `id="username"`            | Khá ổn định nếu dev có quy ước, nhưng không đảm bảo                              |
| `name`        | `name="email"`             | Thường dùng cho `<input>` trong form truyền thống                                |
| `placeholder` | `placeholder="Nhập email"` | Đổi theo ngôn ngữ hiển thị — rủi ro nếu app đa ngôn ngữ                          |
| `role`        | `role="button"`            | Attribute hỗ trợ người khiếm thị — Playwright có hẳn `getByRole` dựa vào cái này |
| `aria-label`  | `aria-label="Đóng"`        | Tương tự — mô tả cho công cụ đọc màn hình, rất bền vì ít khi đổi                 |

### 1.6 Cây lồng nhau — vì sao phải "đi xuống" từ cha đến con

```html
<tr class="row">
  <td>Phiếu 001</td>
  <td>
    <button data-test="btn-sua">Sửa</button>
    <button data-test="btn-xoa">Xoá</button>
  </td>
</tr>
<tr class="row">
  <td>Phiếu 002</td>
  <td>
    <button data-test="btn-sua">Sửa</button>
    <button data-test="btn-xoa">Xoá</button>
  </td>
</tr>
```

Có **2 nút** cùng `data-test="btn-sua"` — nếu chọn thẳng `[data-test="btn-sua"]`, Playwright báo lỗi "khớp nhiều hơn 1 phần tử" (strict mode violation). Phải đi từ dòng cha trước:

```typescript
const dong = page.locator("tr").filter({ hasText: "Phiếu 001" });
await dong.locator('[data-test="btn-sua"]').click(); // giờ chỉ khớp đúng 1
```

Đây là tình huống rất phổ biến với bảng danh sách — hiểu cây lồng nhau là hiểu vì sao code phải viết 2 dòng như vậy.

---

## 2. CSS — Hiểu Selector <a name="2-css"></a>

> CSS thật ra dùng để tạo màu mè giao diện — nhưng **cú pháp chọn phần tử** của nó
> (CSS Selector) được Playwright mượn lại để làm địa chỉ tìm phần tử. Bạn không cần
> học CSS để tạo giao diện, chỉ cần học phần cú pháp chọn phần tử này.

### 2.1 Bốn loại selector cơ bản

| Ký hiệu         | Chọn theo | Ví dụ                    | Khớp với                      |
| --------------- | --------- | ------------------------ | ----------------------------- |
| (không ký hiệu) | Tên thẻ   | `button`                 | mọi thẻ `<button>`            |
| `.`             | Class     | `.login-btn`             | thẻ có `class="login-btn"`    |
| `#`             | Id        | `#username`              | thẻ có `id="username"`        |
| `[ ]`           | Attribute | `[data-test="username"]` | thẻ có `data-test="username"` |

```html
<button id="submit-btn" class="btn btn-primary" data-test="btn-submit">
  Gửi
</button>
```

Phần tử này chọn được bằng cả 4 cách: `button`, `.btn-primary`, `#submit-btn`, `[data-test="btn-submit"]` — nhưng chỉ cách cuối là bền, 3 cách đầu dễ vỡ khi UI đổi (nhiều nút cùng là `button`, class đổi theo theme, id có thể trùng hoặc đổi).

### 2.2 Kết hợp selector — thu hẹp phạm vi tìm

```css
div.login-form input[data-test="username"]
```

Đọc từ trái sang phải: _tìm thẻ `input` có `data-test="username"`, nằm bên trong thẻ `div` có class `login-form`_. Khoảng trắng giữa 2 phần nghĩa là "nằm bên trong" (con, cháu, chắt — không nhất thiết là con trực tiếp).

```css
.inventory_list .inventory_item:first-child
```

_Phần tử đầu tiên (`:first-child`) trong số các `.inventory_item`, nằm trong `.inventory_list`._

### 2.3 Bảng ký hiệu hay gặp khi đọc code có sẵn

| Ký hiệu                        | Ý nghĩa                                     | Ví dụ                   |
| ------------------------------ | ------------------------------------------- | ----------------------- |
| (khoảng trắng)                 | con/cháu (nằm bên trong, mọi cấp)           | `.form input`           |
| `>`                            | con trực tiếp (chỉ 1 cấp)                   | `.form > input`         |
| `,`                            | hoặc — khớp 1 trong nhiều selector          | `.btn-a, .btn-b`        |
| `:first-child` / `:last-child` | phần tử đầu/cuối trong nhóm anh em          | `li:first-child`        |
| `:not(...)`                    | loại trừ                                    | `button:not(.disabled)` |
| `*=` trong `[ ]`               | attribute chứa chuỗi (không cần khớp y hệt) | `[class*="error"]`      |

### 2.4 Playwright còn có "selector riêng" mạnh hơn CSS thuần

Đây là điểm khác biệt quan trọng: Playwright **mở rộng thêm** cú pháp ngoài CSS chuẩn, những thứ này **không dùng được** trong `document.querySelectorAll()` ở Console (vì đó là CSS thuần của trình duyệt):

```typescript
page.locator('button:has-text("Lưu")'); // chỉ Playwright hiểu
page.locator(".item", { hasText: "Tên A" }); // chỉ Playwright hiểu
page.getByRole("button", { name: "Lưu" }); // API riêng, không phải CSS
page.getByText("Đăng nhập"); // API riêng, không phải CSS
```

→ Nếu bạn test 1 selector trong Console (F12) mà báo lỗi cú pháp, trước khi nghĩ selector sai, kiểm tra xem nó có phải cú pháp riêng của Playwright không — cú pháp đó phải test trong Playwright thật (dùng `page.pause()` như Cấp độ 2 đã học), không test được qua Console.

### 2.5 Thực hành — tự kiểm tra selector qua Console

```javascript
// Đếm số phần tử khớp — 1 là chuẩn, 0 là sai, >1 là chưa đủ cụ thể
document.querySelectorAll('[data-test="username"]').length;

// Xem phần tử đầu tiên khớp là gì
document.querySelector(".inventory_item");
```

---

## 3. JAVASCRIPT — Đọc Và Viết Script <a name="3-js"></a>

> Đây là JS **tối thiểu**. Không có phần DOM, không có class/OOP, không có
> closure. Chỉ đủ để đọc code Playwright có sẵn và tự viết được cái mới.

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

```typescript
const URL = "https://www.saucedemo.com"; // const = không đổi giá trị sau khi tạo
let soLanThu = 0; // let = giá trị có thể đổi
```

Dùng `const` gần như mọi lúc. Chỉ dùng `let` khi biết chắc giá trị sẽ đổi (ví dụ bộ đếm trong vòng lặp).

### 3.2 Các kiểu giá trị cơ bản

```typescript
"chuỗi ký tự"; // string — nằm trong dấu nháy
42; // number — số
true / false; // boolean — đúng/sai
null; // "rỗng, cố tình để trống"
undefined; // "chưa được gán giá trị gì"
```

### 3.3 Chuỗi (string) và nội suy (template literal)

```typescript
const ten = "Nguyễn Văn A";
const loi = `Không tìm thấy user ${ten}`; // dấu NHÁY HUYỀN — nhét biến vào giữa bằng ${...}
// kết quả: "Không tìm thấy user Nguyễn Văn A"

const maCase = `AT-TEST-${Date.now()}`; // Date.now() = con số thời gian hiện tại, luôn khác nhau
```

### 3.4 Object — hộp lớn chứa nhiều ngăn có tên

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

SEL.username; // lấy giá trị bằng dấu chấm → '[data-test="username"]'
SEL["username"]; // cách khác, ít dùng hơn nhưng tương đương
```

### 3.5 Mảng (array) — danh sách các giá trị, có thứ tự

```typescript
const taiKhoan = ["standard_user", "locked_out_user", "problem_user"];

taiKhoan[0]; // lấy phần tử đầu tiên → "standard_user" (đếm từ 0, KHÔNG từ 1)
taiKhoan.length; // số lượng phần tử → 3
```

Mảng chứa object — dạng hay gặp nhất khi làm data-driven test:

```typescript
const cases = [
  { ma: "1.2.1", input: "", expected: "Bắt buộc nhập" },
  { ma: "1.2.2", input: "a".repeat(201), expected: "Tối đa 200 ký tự" },
];

cases[0].ma; // "1.2.1"
cases[1].expected; // "Tối đa 200 ký tự"
```

### 3.6 Vòng lặp `for...of` — lặp qua từng phần tử trong mảng

```typescript
for (const c of cases) {
  console.log(c.ma, c.expected);
}
// In ra: 1.2.1 Bắt buộc nhập
//        1.2.2 Tối đa 200 ký tự
```

Đọc là: _với mỗi phần tử trong mảng `cases`, gọi nó là `c`, rồi làm gì đó với `c`_. Đây chính là cơ chế đứng sau data-driven test:

```typescript
for (const c of cases) {
  test(`Validate ${c.ma}`, async ({ page }) => {
    await page.locator(SEL.ten).fill(c.input);
    await expect(page.locator(SEL.error)).toContainText(c.expected);
  });
}
// 1 đoạn code → tự sinh ra N test case, mỗi phần tử trong `cases` là 1 case
```

### 3.7 `if / else` — rẽ nhánh theo điều kiện

```typescript
if (vaiTro === "Admin") {
  await pw.clickButton("btn-xoa");
} else {
  await expect(page.locator("btn-xoa")).toBeDisabled();
}
```

Đọc là: _nếu vai trò là Admin thì click nút xoá, không thì kiểm tra nút xoá bị disable_.

So sánh hay dùng: `===` (bằng), `!==` (khác), `>`, `<`. Luôn dùng `===` chứ không dùng `==` (JS có 2 loại so sánh, `===` an toàn hơn — không cần hiểu sâu tại sao, chỉ cần nhớ dùng `===`).

### 3.8 Hàm (function) và arrow function

```typescript
// Cách viết arrow function — cách viết chuẩn trong code Playwright
const dangNhap = async (page, username, password) => {
  await page.locator(SEL.username).fill(username);
  await page.locator(SEL.password).fill(password);
  await page.locator(SEL.loginButton).click();
};

// Gọi hàm — điền giá trị thật vào chỗ username/password
await dangNhap(page, "standard_user", "secret_sauce");
```

`async (page, username, password) => { ... }` đọc là: _một hàm nhận 3 tham số, cho phép dùng `await` bên trong_.

### 3.9 Destructuring — "tách hộp" lấy đúng phần cần

Đây là cú pháp bạn **đã dùng từ Cấp độ 2** mà chưa biết tên gọi:

```typescript
test("Login thành công", async ({ page }) => { ... });
//                              └───┬───┘
//                     "tách" object fixture, chỉ lấy ra `page`

test("Login với Admin", async ({ admin }) => { ... });
//                             └───┬───┘
//                     lấy ra `admin` — 1 page đã đăng nhập sẵn (fixture riêng của project)
```

Object đầy đủ có thể chứa nhiều hơn (`page`, `admin`, `librarian`, `context`...) — destructuring cho phép bạn chỉ lấy đúng cái cần dùng, viết ngắn gọn thay vì gọi `fixtures.page` dài dòng mỗi lần.

Cũng dùng được với object thường:

```typescript
const nguoiDung = { ten: "A", tuoi: 20, email: "a@x.com" };
const { ten, email } = nguoiDung; // lấy ra 2 biến `ten` và `email`, bỏ qua `tuoi`
```

### 3.10 `.filter()` và `.map()` — xử lý mảng không cần viết vòng lặp

```typescript
const cases = [
  { ma: "1.1", nhom: "login", expected: "..." },
  { ma: "1.2", nhom: "validate", expected: "..." },
  { ma: "1.3", nhom: "login", expected: "..." },
];

// filter — GIỮ LẠI phần tử thoả điều kiện, bỏ phần tử không thoả
const caseLogin = cases.filter((c) => c.nhom === "login");
// → chỉ còn case 1.1 và 1.3

// map — BIẾN ĐỔI mỗi phần tử thành 1 giá trị khác, giữ nguyên số lượng
const danhSachMa = cases.map((c) => c.ma);
// → ["1.1", "1.2", "1.3"]
```

Đây chính là công cụ dùng khi đọc dữ liệu case từ Excel/JSON và cần lọc ra đúng nhóm màn hình cần xử lý — thứ đã nhắc ở phần "cắt lô theo màn hình".

Playwright locator cũng có `.filter()` — cùng ý tưởng "giữ lại phần tử thoả điều kiện", chỉ khác là áp dụng trên phần tử trang web thay vì mảng dữ liệu thường:

```typescript
page.locator("tr").filter({ hasText: "Phiếu 001" }); // giữ lại dòng chứa chữ "Phiếu 001"
```

### 3.11 `try / catch` — bắt lỗi, không để cả script sập

```typescript
try {
  await page.locator(SEL.tooltip).click({ timeout: 2000 });
} catch (error) {
  console.log("Không có tooltip, bỏ qua bước này");
}
```

Đọc là: _thử làm việc trong `try`; nếu nó lỗi (ví dụ tooltip không xuất hiện), nhảy vào `catch` thay vì làm cả test dừng đột ngột_. Dùng khi có bước **có thể có hoặc không có**, không phải lúc nào cũng cần — dùng quá nhiều `try/catch` để "che" lỗi thật cũng là dấu hiệu xấu, chỉ nên dùng cho trường hợp thật sự không chắc chắn.

### 3.12 JSON — định dạng dữ liệu dùng khắp nơi

JSON chỉ là cách viết **object và mảng ra thành chữ** để lưu vào file hoặc gửi qua mạng. Cú pháp giống hệt object/mảng JS, chỉ khác: key luôn phải có nháy kép, và không có comment.

```json
{
  "id": "ecm01",
  "username": "user01@example.com",
  "password": "pass123"
}
```

Đây chính là nội dung file `account.json` bạn đã cấu hình ở Cấp độ 3 — giờ nhìn lại, bạn đã đọc hiểu được cấu trúc này.

```typescript
// Đọc JSON trong code (ví dụ khi cần đọc dữ liệu test từ file)
import data from "./account.json";
console.log(data[0].username); // → "user01@example.com"
```

---

## Bài tập tổng hợp — áp dụng ngay vào project

1. Mở lại file `login.spec.ts` bạn viết ở Cấp độ 2 — với mỗi dòng code, thử tự giải thích bằng lời (không nhìn đáp án) dùng đúng khái niệm ở mục 3
2. Gộp case 4–7 (bạn tự viết ở Cấp độ 2) thành **1 vòng lặp `for...of` với 1 mảng `cases`** — đúng kỹ thuật ở mục 3.6
3. Vào trang saucedemo, F12, dùng Console kiểm tra: selector `.inventory_item` khớp bao nhiêu phần tử? Dùng `.filter()` trên kết quả `document.querySelectorAll(...)` được không? (gợi ý: `querySelectorAll` trả về NodeList, cần `Array.from(...)` chuyển thành mảng thường trước mới `.filter()` được — thử và quan sát lỗi nếu bỏ qua bước này)

Làm được cả 3 bài trên mà không cần mở tài liệu ra xem lại — nghĩa là nền tảng đã đủ vững để quay lại tiếp tục Cấp độ 3.

---

_Tài liệu độc lập — không đánh số theo Cấp độ 1/2/3 Playwright, dùng làm tài liệu tra cứu/bổ trợ song song._
