# 🎭 Checklist Học Playwright - 4 Tuần Cho Tester Fresher

> **Mục tiêu:** Từ zero đến có thể viết automation test chuyên nghiệp với Playwright
> **Yêu cầu:** Biết cơ bản HTML/CSS, có máy tính cài sẵn VS Code

---

## 📋 Tổng Quan Lộ Trình

| Tuần | Chủ đề | Mức độ |
|------|--------|--------|
| Tuần 1 | Setup & Test đầu tiên | 🟢 Beginner |
| Tuần 2 | Locators & Assertions | 🟡 Elementary |
| Tuần 3 | POM & API Testing | 🟠 Intermediate |
| Tuần 4 | CI/CD & Reporting | 🔴 Advanced |

---

## 🗓️ TUẦN 1: Môi Trường & Test Đầu Tiên

> **Mục tiêu tuần:** Cài đặt xong môi trường, chạy được test đầu tiên, hiểu cấu trúc project

---

### 📚 Kiến Thức Cần Học

#### 1. Node.js & npm
- [ ] **Cài Node.js** (phiên bản LTS khuyến nghị)
  > 💡 Node.js là nền tảng chạy JavaScript bên ngoài trình duyệt. npm là trình quản lý package đi kèm.

- [ ] Kiểm tra cài đặt thành công:
  ```bash
  node --version   # Kết quả mong đợi: v18.x.x hoặc cao hơn
  npm --version    # Kết quả mong đợi: 9.x.x hoặc cao hơn
  ```

- [ ] Hiểu `package.json` là gì
  > 💡 File này giống như "hồ sơ" của project - ghi lại tên project, version, và danh sách các thư viện đang dùng.

#### 2. Khởi Tạo Playwright Project
- [ ] Tạo project mới:
  ```bash
  mkdir my-playwright-project
  cd my-playwright-project
  npm init playwright@latest
  ```

- [ ] Hiểu các câu hỏi khi init:
  - [ ] Chọn **TypeScript** hay JavaScript (khuyến nghị TypeScript cho fresher vì có gợi ý code)
  - [ ] Thư mục chứa tests: `tests/`
  - [ ] Có thêm GitHub Actions không: **Yes**
  - [ ] Cài Playwright browsers: **Yes**

- [ ] Cấu trúc project sau khi init:
  ```
  my-playwright-project/
  ├── tests/                  ← Viết test file ở đây
  │   └── example.spec.ts     ← File test mẫu
  ├── playwright.config.ts    ← File cấu hình chính
  ├── package.json            ← Thông tin project & dependencies
  └── node_modules/           ← Thư viện (đừng chỉnh tay!)
  ```

#### 3. Cấu Trúc File Test
- [ ] Hiểu cú pháp cơ bản:
  ```typescript
  import { test, expect } from '@playwright/test';

  // test() = một test case
  // 'Tên test' = mô tả test làm gì
  test('Tên test mô tả rõ ràng', async ({ page }) => {
    // page = đối tượng đại diện cho tab trình duyệt
    await page.goto('https://example.com');
    // await = chờ hành động hoàn thành (quan trọng!)
  });
  ```

- [ ] Hiểu `async/await`:
  > 💡 Playwright thao tác với trình duyệt mất thời gian. `await` nói với code "hãy chờ xong rồi mới làm bước tiếp theo". Quên `await` là bug phổ biến nhất của người mới!

- [ ] Hiểu `test.describe()`:
  ```typescript
  // Nhóm các test liên quan lại với nhau
  test.describe('Chức năng Login', () => {
    test('Login thành công', async ({ page }) => { ... });
    test('Login sai password', async ({ page }) => { ... });
  });
  ```

#### 4. Chạy Test
- [ ] Các lệnh cơ bản:
  ```bash
  npx playwright test                    # Chạy tất cả tests
  npx playwright test example.spec.ts    # Chạy 1 file cụ thể
  npx playwright test --headed           # Chạy có hiện trình duyệt
  npx playwright test --ui               # Mở UI mode (siêu tiện!)
  ```

- [ ] Hiểu kết quả test:
  > 💡 **PASSED** ✅ = Test chạy đúng | **FAILED** ❌ = Test phát hiện lỗi hoặc bản thân test bị lỗi | **SKIPPED** ⏭️ = Test bị bỏ qua

#### 5. Codegen - Tự Động Tạo Code
- [ ] Chạy Codegen:
  ```bash
  npx playwright codegen https://www.google.com
  ```

- [ ] Hiểu Codegen làm gì:
  > 💡 Codegen mở trình duyệt, bạn click/type gì thì nó tự viết code Playwright tương ứng. Rất hữu ích khi mới học để xem "Playwright viết thế nào?"

- [ ] Thực hành với Codegen:
  - [ ] Record lại thao tác tìm kiếm trên Google
  - [ ] Copy code được tạo ra vào file test
  - [ ] Chạy thử xem có pass không

---

### 🛠️ Bài Tập Thực Hành Tuần 1

**Website thực hành:** `https://www.saucedemo.com` (trang demo login miễn phí)

- [ ] **Bài 1.1:** Viết test navigate đến trang chủ và verify title
  ```typescript
  // Gợi ý:
  test('Trang chủ hiển thị đúng title', async ({ page }) => {
    await page.goto('https://www.saucedemo.com');
    // Kiểm tra title của trang
    await expect(page).toHaveTitle(/Swag Labs/);
  });
  ```

- [ ] **Bài 1.2:** Dùng Codegen record lại flow: mở trang → nhập username → nhập password → click Login

- [ ] **Bài 1.3:** Chuyển code từ Codegen thành file test `.spec.ts` và chạy thành công

- [ ] **Bài 1.4:** Chạy test với `--headed` để xem trình duyệt hoạt động

- [ ] **Bài 1.5:** Mở `--ui` mode, khám phá giao diện (xem timeline, screenshot từng bước)

---

### ✅ Tiêu Chí Hoàn Thành Tuần 1

- [ ] Gõ `npx playwright test` không bị lỗi, ít nhất 1 test PASSED
- [ ] Giải thích được: `page`, `await`, `async` là gì
- [ ] Mô tả được cấu trúc project (mỗi folder/file làm gì)
- [ ] Dùng Codegen tạo được test tự động
- [ ] Chạy được test ở cả 3 mode: headless, headed, UI

**🎯 Checkpoint:** Nhờ người mentor/đồng nghiệp hỏi ngẫu nhiên 1 trong 5 tiêu chí trên

---

## 🗓️ TUẦN 2: Locators & Assertions

> **Mục tiêu tuần:** Tìm được element chính xác, kiểm tra đúng điều kiện, test không bị flaky

---

### 📚 Kiến Thức Cần Học

#### 1. Locator Strategies - Cách Tìm Element

> 💡 **Locator** là cách Playwright "chỉ đường" đến một phần tử trên trang web. Chọn locator đúng = test ổn định, ít lỗi.

##### 🥇 Ưu tiên 1: `getByRole()` - Tìm theo vai trò ARIA
- [ ] Hiểu concept:
  ```typescript
  // Tìm button có tên "Submit"
  await page.getByRole('button', { name: 'Submit' }).click();

  // Tìm textbox (input) tên "Email"
  await page.getByRole('textbox', { name: 'Email' }).fill('test@gmail.com');

  // Tìm heading level 1
  await page.getByRole('heading', { level: 1 });

  // Các role thường dùng: button, link, textbox, checkbox, radio,
  //                        heading, img, list, listitem, combobox
  ```
  > 💡 Đây là cách được khuyến nghị nhất vì nó test cả accessibility (khả năng truy cập). Nếu screen reader đọc được thì test của bạn cũng tìm được.

- [ ] Thực hành: Tìm button Login, input username/password trên saucedemo.com

##### 🥈 Ưu tiên 2: `getByText()` - Tìm theo nội dung text
- [ ] Hiểu concept:
  ```typescript
  // Tìm element chứa đúng text này
  await page.getByText('Chào mừng bạn').click();

  // Tìm chính xác (exact match)
  await page.getByText('Login', { exact: true }).click();

  // Dùng regex để tìm linh hoạt hơn
  await page.getByText(/welcome/i).isVisible();
  ```
  > ⚠️ Cẩn thận khi text thay đổi theo ngôn ngữ (i18n) - test sẽ bị fail!

##### 🥉 Ưu tiên 3: `getByPlaceholder()` - Tìm input theo placeholder
- [ ] Hiểu concept:
  ```typescript
  // Tìm input có placeholder "Enter your email"
  await page.getByPlaceholder('Enter your email').fill('user@test.com');

  // Tìm input có placeholder "Password"
  await page.getByPlaceholder('Password').fill('secret123');
  ```
  > 💡 Hữu ích cho form fields khi không có label rõ ràng.

##### 4: `getByTestId()` - Tìm theo data-testid (chuyên dụng cho testing)
- [ ] Hiểu concept:
  ```typescript
  // HTML: <button data-testid="submit-btn">Submit</button>
  await page.getByTestId('submit-btn').click();
  ```
  > 💡 **Best practice nhất!** Dev thêm `data-testid` vào HTML để tester dùng. Không bị ảnh hưởng bởi thay đổi UI hay text. Hãy thỏa thuận với dev team để họ thêm vào.

- [ ] Hiểu tại sao KHÔNG dùng CSS selector / XPath làm primary locator:
  ```typescript
  // ❌ Dễ bị break khi UI thay đổi
  await page.locator('.btn-primary > span').click();
  await page.locator('//div[@class="form"]/button[1]').click();

  // ✅ Ổn định hơn nhiều
  await page.getByRole('button', { name: 'Login' }).click();
  ```

##### So Sánh Các Locator:

| Locator | Khi nào dùng | Độ ưu tiên |
|---------|-------------|------------|
| `getByRole` | Luôn thử đầu tiên | ⭐⭐⭐⭐⭐ |
| `getByText` | Element chỉ nhận biết qua text | ⭐⭐⭐⭐ |
| `getByPlaceholder` | Input fields | ⭐⭐⭐⭐ |
| `getByTestId` | Khi dev đã add data-testid | ⭐⭐⭐⭐⭐ |
| `getByLabel` | Form có label rõ ràng | ⭐⭐⭐⭐ |
| CSS Selector | Phương án cuối cùng | ⭐⭐ |

#### 2. Assertions - Kiểm Tra Kết Quả

> 💡 **Assertion** là bước "kiểm tra" - nếu điều kiện không đúng thì test FAIL. Đây là trái tim của automated testing.

- [ ] **`toBeVisible()`** - Element có hiển thị không?
  ```typescript
  // Kiểm tra element hiển thị trên trang
  await expect(page.getByText('Login successful')).toBeVisible();

  // Kiểm tra element KHÔNG hiển thị
  await expect(page.getByText('Error message')).not.toBeVisible();
  ```

- [ ] **`toHaveText()`** - Kiểm tra nội dung text
  ```typescript
  // Kiểm tra text chính xác
  await expect(page.getByRole('heading')).toHaveText('Dashboard');

  // Kiểm tra text chứa chuỗi con
  await expect(page.locator('.welcome-msg')).toContainText('Xin chào');
  ```

- [ ] **`toHaveValue()`** - Kiểm tra giá trị input
  ```typescript
  // Sau khi fill, kiểm tra giá trị trong input
  await expect(page.getByPlaceholder('Username')).toHaveValue('standard_user');
  ```

- [ ] **`toHaveURL()`** - Kiểm tra URL hiện tại
  ```typescript
  // Sau khi login, kiểm tra đã chuyển đến trang đúng
  await expect(page).toHaveURL('https://www.saucedemo.com/inventory.html');

  // Dùng regex
  await expect(page).toHaveURL(/inventory/);
  ```

- [ ] **`toHaveTitle()`** - Kiểm tra title trang
  ```typescript
  await expect(page).toHaveTitle(/Swag Labs/);
  ```

- [ ] **`toBeEnabled()` / `toBeDisabled()`** - Trạng thái button
  ```typescript
  await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
  await expect(page.getByRole('button', { name: 'Loading...' })).toBeDisabled();
  ```

- [ ] **`toHaveCount()`** - Đếm số element
  ```typescript
  // Kiểm tra có đúng 6 sản phẩm trong giỏ hàng
  await expect(page.locator('.inventory-item')).toHaveCount(6);
  ```

> 💡 **Auto-waiting:** Playwright TỰ ĐỘNG chờ element sẵn sàng trước khi assert. Không cần viết `sleep()` hay `waitFor()` thủ công trong hầu hết trường hợp!

#### 3. Test Fixtures

> 💡 **Fixture** là "đồ dùng" được chuẩn bị sẵn cho test, tự động setup trước và cleanup sau mỗi test. `page` bạn hay dùng chính là một built-in fixture!

- [ ] Hiểu các built-in fixtures:
  ```typescript
  test('ví dụ fixtures', async ({
    page,        // Tab trình duyệt
    browser,     // Instance trình duyệt (Chrome/Firefox/Safari)
    context,     // Browser context (như profile riêng, có cookie riêng)
    request,     // Dùng để gọi API (học ở tuần 3)
  }) => {
    // ...
  });
  ```

- [ ] `test.beforeEach()` và `test.afterEach()`:
  ```typescript
  test.describe('Saucedemo Tests', () => {

    // Chạy TRƯỚC mỗi test trong describe block này
    test.beforeEach(async ({ page }) => {
      await page.goto('https://www.saucedemo.com');
      // Login trước mỗi test để không phải lặp lại
      await page.getByPlaceholder('Username').fill('standard_user');
      await page.getByPlaceholder('Password').fill('secret_sauce');
      await page.getByRole('button', { name: 'Login' }).click();
    });

    test('Kiểm tra trang inventory', async ({ page }) => {
      // Vào đây đã login sẵn rồi!
      await expect(page).toHaveURL(/inventory/);
    });

    test('Thêm sản phẩm vào giỏ hàng', async ({ page }) => {
      // Đây cũng đã login sẵn
      await page.getByRole('button', { name: 'Add to cart' }).first().click();
    });

    // Chạy SAU mỗi test (thường dùng để cleanup)
    test.afterEach(async ({ page }) => {
      // Ví dụ: xóa dữ liệu test, logout, etc.
      console.log('Test xong, đang cleanup...');
    });
  });
  ```

#### 4. Screenshot

- [ ] **Screenshot thủ công** - Chụp để debug:
  ```typescript
  test('Test có screenshot', async ({ page }) => {
    await page.goto('https://www.saucedemo.com');

    // Chụp toàn trang
    await page.screenshot({ path: 'screenshot-homepage.png' });

    // Chụp 1 element cụ thể
    await page.locator('.login_logo').screenshot({ path: 'screenshot-logo.png' });

    // Chụp fullpage (kể cả phần cuộn)
    await page.screenshot({ path: 'fullpage.png', fullPage: true });
  });
  ```

- [ ] **Screenshot tự động khi fail** - Thêm vào `playwright.config.ts`:
  ```typescript
  export default defineConfig({
    use: {
      // 'only-on-failure' | 'on' | 'off'
      screenshot: 'only-on-failure',
    },
  });
  ```

---

### 🛠️ Bài Tập Thực Hành Tuần 2

**Website:** `https://www.saucedemo.com`

- [ ] **Bài 2.1:** Viết test login dùng `getByPlaceholder` và `getByRole`:
  ```
  Scenario: Login thành công
  - Vào trang saucedemo.com
  - Nhập username: standard_user
  - Nhập password: secret_sauce
  - Click Login
  - Verify URL chứa "inventory"
  - Verify có hiển thị text "Products"
  ```

- [ ] **Bài 2.2:** Viết test login thất bại:
  ```
  Scenario: Login sai password
  - Nhập username: standard_user
  - Nhập password: wrong_password
  - Click Login
  - Verify error message hiển thị (dùng toBeVisible)
  - Verify text của error message (dùng toHaveText hoặc toContainText)
  ```

- [ ] **Bài 2.3:** Dùng `beforeEach` để tránh lặp code login, viết 3 tests dùng chung setup

- [ ] **Bài 2.4:** Thêm assertion `toHaveCount` để kiểm tra có đúng 6 sản phẩm trên trang inventory

- [ ] **Bài 2.5:** Cấu hình `screenshot: 'only-on-failure'`, cố tình làm 1 test fail, xem screenshot được lưu ở đâu

---

### ✅ Tiêu Chí Hoàn Thành Tuần 2

- [ ] Giải thích được tại sao ưu tiên `getByRole` hơn CSS selector
- [ ] Viết được test login hoàn chỉnh không có code lặp (dùng `beforeEach`)
- [ ] Sử dụng đúng ít nhất 5 loại assertion khác nhau
- [ ] Tìm được element bằng tất cả 4 locator được học
- [ ] Screenshot tự động xuất hiện khi test fail

**🎯 Checkpoint:** Xem report sau khi chạy, giải thích được tại sao test pass/fail

---

## 🗓️ TUẦN 3: Page Object Model & API Testing

> **Mục tiêu tuần:** Tổ chức code chuyên nghiệp, test API endpoints, mock API để test độc lập

---

### 📚 Kiến Thức Cần Học

#### 1. Page Object Model (POM)

> 💡 **POM là gì?** Thay vì viết locator trực tiếp trong test, bạn tạo một "class" đại diện cho mỗi trang web. Class này chứa tất cả locators và actions của trang đó.

**Vấn đề khi KHÔNG dùng POM:**
```typescript
// ❌ Không có POM - locator bị lặp ở nhiều nơi
test('Test 1', async ({ page }) => {
  await page.getByPlaceholder('Username').fill('user1');
  await page.getByPlaceholder('Password').fill('pass1');
  await page.getByRole('button', { name: 'Login' }).click();
});

test('Test 2', async ({ page }) => {
  // Lặp lại y chang! Nếu placeholder đổi thành "Email" thì phải sửa ở mọi nơi!
  await page.getByPlaceholder('Username').fill('user2');
  await page.getByPlaceholder('Password').fill('pass2');
  await page.getByRole('button', { name: 'Login' }).click();
});
```

**Giải pháp với POM:**
- [ ] Tạo cấu trúc folder:
  ```
  tests/
  ├── pages/                    ← Chứa Page Objects
  │   ├── LoginPage.ts
  │   ├── InventoryPage.ts
  │   └── CartPage.ts
  ├── specs/                    ← Chứa test files
  │   ├── login.spec.ts
  │   └── shopping.spec.ts
  └── fixtures/                 ← Custom fixtures (nâng cao)
  ```

- [ ] Tạo `LoginPage.ts`:
  ```typescript
  import { Page, Locator, expect } from '@playwright/test';

  export class LoginPage {
    // Khai báo các locators như "thuộc tính" của class
    readonly usernameInput: Locator;
    readonly passwordInput: Locator;
    readonly loginButton: Locator;
    readonly errorMessage: Locator;

    constructor(page: Page) {
      // Khởi tạo locators một lần duy nhất
      this.usernameInput = page.getByPlaceholder('Username');
      this.passwordInput = page.getByPlaceholder('Password');
      this.loginButton   = page.getByRole('button', { name: 'Login' });
      this.errorMessage  = page.locator('[data-test="error"]');
    }

    // Đóng gói hành động phức tạp thành method đơn giản
    async login(username: string, password: string) {
      await this.usernameInput.fill(username);
      await this.passwordInput.fill(password);
      await this.loginButton.click();
    }

    async verifyErrorMessage(message: string) {
      await expect(this.errorMessage).toBeVisible();
      await expect(this.errorMessage).toContainText(message);
    }
  }
  ```

- [ ] Tạo `InventoryPage.ts`:
  ```typescript
  import { Page, Locator, expect } from '@playwright/test';

  export class InventoryPage {
    readonly productList: Locator;
    readonly cartIcon: Locator;
    readonly pageTitle: Locator;

    constructor(page: Page) {
      this.productList = page.locator('.inventory-item');
      this.cartIcon    = page.locator('.shopping_cart_link');
      this.pageTitle   = page.getByText('Products');
    }

    async addProductToCart(productName: string) {
      // Tìm sản phẩm theo tên và click Add to cart
      await page.locator(`.inventory-item:has-text("${productName}")`)
                .getByRole('button', { name: 'Add to cart' })
                .click();
    }

    async verifyProductCount(count: number) {
      await expect(this.productList).toHaveCount(count);
    }
  }
  ```

- [ ] Dùng Page Objects trong test:
  ```typescript
  import { test, expect } from '@playwright/test';
  import { LoginPage } from '../pages/LoginPage';
  import { InventoryPage } from '../pages/InventoryPage';

  test('Login thành công và thấy sản phẩm', async ({ page }) => {
    const loginPage = new LoginPage(page);
    const inventoryPage = new InventoryPage(page);

    await page.goto('https://www.saucedemo.com');

    // Code test cực kỳ dễ đọc!
    await loginPage.login('standard_user', 'secret_sauce');
    await inventoryPage.verifyProductCount(6);
  });

  test('Login sai password hiện error', async ({ page }) => {
    const loginPage = new LoginPage(page);

    await page.goto('https://www.saucedemo.com');
    await loginPage.login('standard_user', 'wrong_pass');
    await loginPage.verifyErrorMessage('Username and password do not match');
  });
  ```

> 💡 **Lợi ích POM:**
> - Locator thay đổi → chỉ sửa 1 chỗ trong Page Object
> - Test file ngắn gọn, dễ đọc
> - Tái sử dụng code
> - Người mới vào team hiểu ngay test đang làm gì

#### 2. API Testing với `request` Fixture

> 💡 Playwright không chỉ test UI! Nó có thể gọi API trực tiếp, rất hữu ích để **setup test data** nhanh hơn (thay vì click qua UI) hoặc **test backend API**.

- [ ] Hiểu cơ bản:
  ```typescript
  import { test, expect } from '@playwright/test';

  test('Gọi API lấy danh sách users', async ({ request }) => {
    // GET request
    const response = await request.get('https://reqres.in/api/users?page=1');

    // Kiểm tra status code
    expect(response.status()).toBe(200);
    expect(response.ok()).toBeTruthy(); // true nếu status 200-299

    // Parse response body
    const body = await response.json();
    console.log(body);

    // Assert dữ liệu
    expect(body.data).toHaveLength(6);
    expect(body.data[0]).toHaveProperty('email');
  });
  ```

- [ ] **POST request:**
  ```typescript
  test('Tạo user mới', async ({ request }) => {
    const response = await request.post('https://reqres.in/api/users', {
      data: {
        name: 'John Doe',
        job: 'QA Engineer'
      }
    });

    expect(response.status()).toBe(201); // 201 = Created

    const newUser = await response.json();
    expect(newUser.name).toBe('John Doe');
    expect(newUser.id).toBeTruthy(); // ID được tạo tự động
  });
  ```

- [ ] **Dùng API để setup test data trước khi test UI:**
  ```typescript
  test('Kiểm tra profile sau khi tạo', async ({ page, request }) => {
    // ✅ Tạo data qua API (nhanh, không cần click UI)
    const newUser = await request.post('/api/users', {
      data: { name: 'Test User', email: 'test@example.com' }
    });
    const user = await newUser.json();

    // Rồi mới test UI với data vừa tạo
    await page.goto(`/users/${user.id}`);
    await expect(page.getByText('Test User')).toBeVisible();
  });
  ```

#### 3. Mock API (API Mocking / Request Interception)

> 💡 **Mock API là gì?** Thay vì để test gọi API thật (có thể chậm, không ổn định, hoặc có side effects), bạn "chặn" request và trả về dữ liệu giả. Test trở nên nhanh hơn và đáng tin cậy hơn.

- [ ] **`page.route()`** - Chặn và mock response:
  ```typescript
  test('Hiển thị danh sách sản phẩm từ API mock', async ({ page }) => {

    // Chặn bất kỳ request nào đến URL này
    await page.route('**/api/products', async route => {
      // Trả về dữ liệu giả thay vì gọi server thật
      await route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify([
          { id: 1, name: 'Sản phẩm Mock 1', price: 100 },
          { id: 2, name: 'Sản phẩm Mock 2', price: 200 },
        ])
      });
    });

    await page.goto('/products');
    // Trang sẽ hiển thị dữ liệu mock, không phải dữ liệu thật
    await expect(page.getByText('Sản phẩm Mock 1')).toBeVisible();
  });
  ```

- [ ] **Mock lỗi để test error handling:**
  ```typescript
  test('Hiển thị thông báo lỗi khi API fail', async ({ page }) => {

    await page.route('**/api/products', async route => {
      await route.fulfill({
        status: 500, // Giả lập server lỗi
        body: JSON.stringify({ error: 'Internal Server Error' })
      });
    });

    await page.goto('/products');
    // Kiểm tra UI xử lý lỗi đúng cách
    await expect(page.getByText('Không thể tải sản phẩm')).toBeVisible();
  });
  ```

- [ ] **`page.route()` để block request (ví dụ: block ads, analytics):**
  ```typescript
  test('Load nhanh hơn khi block tracking', async ({ page }) => {
    // Block tất cả request đến Google Analytics
    await page.route('**google-analytics.com**', route => route.abort());

    await page.goto('https://example.com');
    // Test chạy nhanh hơn vì không tải analytics
  });
  ```

---

### 🛠️ Bài Tập Thực Hành Tuần 3

**Websites:**
- UI Testing: `https://www.saucedemo.com`
- API Testing: `https://reqres.in` (API miễn phí cho practice)

- [ ] **Bài 3.1 (POM):** Tạo `LoginPage.ts` và `InventoryPage.ts` theo hướng dẫn, refactor lại tests từ tuần 2

- [ ] **Bài 3.2 (POM):** Tạo thêm `CartPage.ts`, viết test flow: Login → Add sản phẩm → Vào Cart → Verify

- [ ] **Bài 3.3 (API GET):** Viết test gọi `GET https://reqres.in/api/users/2`, verify:
  - Status code là 200
  - `data.email` là `janet.weaver@reqres.in`
  - `data.first_name` là `Janet`

- [ ] **Bài 3.4 (API POST):** Viết test gọi `POST https://reqres.in/api/users` với body `{ name: "Tên bạn", job: "QA" }`, verify status 201 và response có `id`

- [ ] **Bài 3.5 (Mock API):** Mock API `GET /api/users` trả về 3 user giả, verify UI hiển thị đúng số user

---

### ✅ Tiêu Chí Hoàn Thành Tuần 3

- [ ] Giải thích được POM giải quyết vấn đề gì
- [ ] Tất cả tests từ tuần 2 đã được refactor dùng POM
- [ ] Viết được API test đủ 3 loại: GET, POST, và kiểm tra status code
- [ ] Mock được API response và verify UI hiển thị đúng
- [ ] Code trong test file không có hardcode locator trực tiếp (tất cả đã vào Page Object)

**🎯 Checkpoint:** Code review với mentor - nhìn vào test file có đọc hiểu ngay flow không?

---

## 🗓️ TUẦN 4: Parallel Testing, CI/CD & Reporting

> **Mục tiêu tuần:** Chạy test nhanh hơn với parallel, tự động hóa trên CI/CD, đọc và share report chuyên nghiệp

---

### 📚 Kiến Thức Cần Học

#### 1. Parallel Testing Configuration

> 💡 **Parallel Testing là gì?** Thay vì chạy test lần lượt (1 → 2 → 3), chạy song song nhiều test cùng lúc. 10 tests thay vì mất 100 giây có thể chỉ mất 20 giây!

- [ ] Cấu hình trong `playwright.config.ts`:
  ```typescript
  import { defineConfig, devices } from '@playwright/test';

  export default defineConfig({
    // Số lượng test workers (process) chạy song song
    // Mặc định: số lượng CPU cores / 2
    workers: process.env.CI ? 2 : 4,

    // Chạy tests trong cùng 1 file theo thứ tự (không parallel trong file)
    // fullyParallel: false = default (tests trong 1 file chạy tuần tự)
    // fullyParallel: true = kể cả trong cùng 1 file cũng chạy song song
    fullyParallel: true,

    // Test trên nhiều browsers cùng lúc
    projects: [
      {
        name: 'chromium',
        use: { ...devices['Desktop Chrome'] },
      },
      {
        name: 'firefox',
        use: { ...devices['Desktop Firefox'] },
      },
      {
        name: 'webkit',
        use: { ...devices['Desktop Safari'] },
      },
      // Test mobile
      {
        name: 'Mobile Chrome',
        use: { ...devices['Pixel 5'] },
      },
    ],
  });
  ```

- [ ] Hiểu **test isolation** trong parallel testing:
  ```typescript
  // ✅ An toàn khi parallel - mỗi test có page riêng
  test('Test A', async ({ page }) => {
    await page.goto('https://example.com');
    // page này độc lập, không ảnh hưởng Test B
  });

  test('Test B', async ({ page }) => {
    await page.goto('https://example.com');
    // page này cũng độc lập
  });

  // ⚠️ Nguy hiểm khi parallel - chia sẻ state
  let sharedData = {}; // ❌ Biến này bị share giữa các tests!

  test('Test C', async ({ page }) => {
    sharedData.user = 'admin'; // Test D có thể đọc giá trị này
  });
  ```

- [ ] **Serial mode** khi test phải chạy theo thứ tự:
  ```typescript
  // Khi test 2 phụ thuộc vào kết quả test 1
  test.describe.serial('Flow mua hàng', () => {
    test('1. Thêm vào giỏ hàng', async ({ page }) => { ... });
    test('2. Checkout', async ({ page }) => { ... }); // Phải chạy sau test 1
    test('3. Xác nhận đơn hàng', async ({ page }) => { ... });
  });
  ```

- [ ] **Retry khi fail** (quan trọng trong CI):
  ```typescript
  export default defineConfig({
    // Tự động retry test fail (giảm flaky tests)
    retries: process.env.CI ? 2 : 0,
    // Trên CI retry 2 lần, local không retry (để dev thấy lỗi ngay)
  });
  ```

#### 2. GitHub Actions CI/CD

> 💡 **CI/CD là gì?** CI = Continuous Integration - tự động chạy tests mỗi khi có code mới push lên. Giúp phát hiện lỗi ngay lập tức thay vì đợi deploy lên production.

- [ ] File `.github/workflows/playwright.yml` (Playwright tạo sẵn khi init):
  ```yaml
  name: Playwright Tests

  # Khi nào chạy CI?
  on:
    push:
      branches: [ main, master ]    # Khi push lên main
    pull_request:
      branches: [ main, master ]    # Khi tạo Pull Request

  jobs:
    test:
      timeout-minutes: 60
      runs-on: ubuntu-latest        # Chạy trên máy chủ Linux của GitHub

      steps:
        # Bước 1: Lấy code từ repository
        - uses: actions/checkout@v4

        # Bước 2: Cài Node.js
        - uses: actions/setup-node@v4
          with:
            node-version: lts/*

        # Bước 3: Cài dependencies
        - name: Install dependencies
          run: npm ci

        # Bước 4: Cài browsers
        - name: Install Playwright Browsers
          run: npx playwright install --with-deps

        # Bước 5: Chạy tests!
        - name: Run Playwright tests
          run: npx playwright test

        # Bước 6: Upload HTML report để xem kết quả
        - uses: actions/upload-artifact@v4
          if: ${{ !cancelled() }}     # Upload kể cả khi tests fail
          with:
            name: playwright-report
            path: playwright-report/
            retention-days: 30        # Giữ report 30 ngày
  ```

- [ ] Hiểu các bước CI:
  > 💡 `npm ci` khác `npm install` ở chỗ: `npm ci` cài đúng version trong `package-lock.json`, không update gì cả. Dùng trong CI để đảm bảo consistent.

- [ ] Xem kết quả CI:
  - [ ] Vào tab **Actions** trên GitHub
  - [ ] Click vào workflow run
  - [ ] Xem log từng step
  - [ ] Download artifact **playwright-report** để xem HTML report

- [ ] **Tùy chỉnh CI theo nhu cầu:**
  ```yaml
  # Chỉ chạy trên Chrome để tiết kiệm thời gian CI
  - name: Run Playwright tests (Chrome only)
    run: npx playwright test --project=chromium

  # Set biến môi trường cho CI
  - name: Run tests
    run: npx playwright test
    env:
      BASE_URL: https://staging.example.com
      TEST_USERNAME: ${{ secrets.TEST_USERNAME }}  # Lấy từ GitHub Secrets
      TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}
  ```

#### 3. HTML Report

> 💡 HTML Report là file báo cáo đẹp, có thể mở bằng trình duyệt, share với team, PM nhìn vào cũng hiểu.

- [ ] Cấu hình trong `playwright.config.ts`:
  ```typescript
  export default defineConfig({
    reporter: [
      ['html', {
        outputFolder: 'playwright-report',  // Tên thư mục chứa report
        open: 'never'                       // 'always' | 'never' | 'on-failure'
      }],
      ['list'],           // In kết quả ra terminal dạng list
      ['dot'],            // In kết quả dạng dots (ngắn gọn)
    ],
  });
  ```

- [ ] Mở HTML report:
  ```bash
  # Cách 1: Sau khi chạy test
  npx playwright show-report

  # Cách 2: Chỉ định folder
  npx playwright show-report playwright-report
  ```

- [ ] Khám phá HTML Report:
  - [ ] Xem tổng quan: bao nhiêu passed/failed/skipped
  - [ ] Click vào test case để xem chi tiết
  - [ ] Xem screenshot khi fail
  - [ ] Xem từng action được thực hiện
  - [ ] Xem thời gian chạy mỗi test

#### 4. Trace Viewer - Debug Như Một Pro

> 💡 **Trace Viewer** là công cụ debug mạnh nhất của Playwright. Nó record lại MỌI hành động trong test (click, scroll, network request, console log) và cho bạn xem lại như video, từng frame một.

- [ ] Bật trace trong `playwright.config.ts`:
  ```typescript
  export default defineConfig({
    use: {
      // 'on' = luôn bật | 'off' = tắt | 'on-first-retry' = bật khi retry
      // 'retain-on-failure' = giữ lại trace khi fail
      trace: 'on-first-retry',
    },
  });
  ```

- [ ] Xem trace:
  ```bash
  # Mở trace viewer (file .zip được tạo sau khi test fail)
  npx playwright show-trace test-results/[test-name]/trace.zip

  # Hoặc upload lên https://trace.playwright.dev
  ```

- [ ] Những gì Trace Viewer cho thấy:
  - [ ] **Timeline**: Dòng thời gian từng action
  - [ ] **Screenshots**: Ảnh chụp tại mỗi bước
  - [ ] **DOM Snapshot**: Cấu trúc HTML tại thời điểm đó
  - [ ] **Network**: Tất cả request/response
  - [ ] **Console**: Log messages
  - [ ] **Errors**: Stack trace khi có lỗi

- [ ] Debug bằng Trace:
  > 💡 Khi test fail và bạn không biết tại sao, mở Trace Viewer là bước đầu tiên. Bạn có thể thấy chính xác: trước khi click button thì trang trông như thế nào, button có visible không, network có trả về lỗi không.

- [ ] **Video recording** (tùy chọn):
  ```typescript
  export default defineConfig({
    use: {
      video: 'on-first-retry', // Record video khi test fail
    },
  });
  ```

---

### 🛠️ Bài Tập Thực Hành Tuần 4

- [ ] **Bài 4.1 (Parallel Config):** Cấu hình chạy tests trên cả 3 browsers (Chrome, Firefox, Safari), chạy và so sánh thời gian với chạy trên 1 browser

- [ ] **Bài 4.2 (Workers):** Thử nghiệm `workers: 1` vs `workers: 4`, đo thời gian chạy 10+ tests, ghi lại kết quả

- [ ] **Bài 4.3 (GitHub Actions):**
  - [ ] Push project lên GitHub repository
  - [ ] Xem GitHub Actions tự động chạy
  - [ ] Tìm và download HTML report từ Artifacts
  - [ ] Cố tình commit 1 test fail, xem CI report kết quả

- [ ] **Bài 4.4 (HTML Report):** Chạy toàn bộ test suite, mở HTML report, viết nhận xét về:
  - Bao nhiêu tests pass/fail?
  - Test nào chậm nhất?
  - Có test nào flaky không?

- [ ] **Bài 4.5 (Trace Viewer):**
  - [ ] Bật `trace: 'on'`
  - [ ] Cố tình viết test fail (assert sai)
  - [ ] Mở Trace Viewer, xem và mô tả lại từng bước

- [ ] **Bonus:** Thêm GitHub Secrets cho credentials, dùng `process.env` trong test thay vì hardcode

---

### ✅ Tiêu Chí Hoàn Thành Tuần 4

- [ ] Test suite chạy song song trên ít nhất 2 browsers không có lỗi
- [ ] GitHub Actions pipeline xanh (all green) với ít nhất 5 tests
- [ ] Download và đọc được HTML report từ GitHub Artifacts
- [ ] Dùng Trace Viewer để phân tích 1 test fail và giải thích nguyên nhân
- [ ] Giải thích được: CI/CD mang lại lợi ích gì cho team?

**🎯 Checkpoint:** Demo live cho team: push code → CI chạy → xem report → fix bug → CI green

---

## 🎓 Tổng Kết Lộ Trình 4 Tuần

### Kỹ Năng Đã Đạt Được

| Kỹ năng | Tuần | Mức độ thành thạo |
|---------|------|-------------------|
| Setup môi trường | 1 | ✅ Thành thạo |
| Viết test cơ bản | 1 | ✅ Thành thạo |
| Locator strategies | 2 | ✅ Thành thạo |
| Assertions | 2 | ✅ Thành thạo |
| Page Object Model | 3 | ✅ Hiểu và áp dụng |
| API Testing | 3 | ✅ Hiểu cơ bản |
| Mock API | 3 | ✅ Hiểu cơ bản |
| Parallel testing | 4 | ✅ Cấu hình được |
| CI/CD | 4 | ✅ Setup được |
| HTML Report | 4 | ✅ Đọc và phân tích |
| Trace Viewer | 4 | ✅ Debug được |

---

### 📖 Tài Nguyên Tham Khảo

- [ ] 📘 [Playwright Official Docs](https://playwright.dev/docs/intro)
- [ ] 🎥 [Playwright YouTube Channel](https://www.youtube.com/@Playwrightdev)
- [ ] 💬 [Playwright Discord Community](https://discord.com/invite/playwright-807756831384403968)
- [ ] 🧪 [Practice Sites: saucedemo.com, reqres.in, demoqa.com]
- [ ] 📝 [Playwright Best Practices](https://playwright.dev/docs/best-practices)

---

### 🚀 Bước Tiếp Theo Sau 4 Tuần

- [ ] Học **Custom Fixtures** - tạo fixture riêng cho team
- [ ] Khám phá **Visual Regression Testing** với `toHaveScreenshot()`
- [ ] Tìm hiểu **Accessibility Testing** với `@axe-core/playwright`
- [ ] Học **Test Data Management** - quản lý dữ liệu test chuyên nghiệp
- [ ] Khám phá **Playwright Component Testing** cho React/Vue/Angular

---

> 💪 **Lời khuyên cuối:** Đừng chỉ đọc - hãy gõ từng dòng code. Những bug bạn gặp trong lúc practice là người thầy tốt nhất. Playwright có docs rất tốt, đừng ngại đọc official docs khi gặp khó khăn!
>
> 🤝 **Hỏi để học nhanh hơn:** Mỗi khi bí, hãy hỏi ngay - đừng mắc kẹt quá 30 phút một mình với 1 vấn đề!