# คู่มือการใช้งาน Playwright Framework

## สารบัญ

0. [ความรู้พื้นฐาน: ทำไมต้อง E2E Testing?](#0-ความรู้พื้นฐาน-ทำไมต้อง-e2e-testing)
1. [บทนำ Playwright Framework](#1-บทนำ-playwright-framework)
2. [การติดตั้งและตั้งค่าเบื้องต้น](#2-การติดตั้งและตั้งค่าเบื้องต้น)
3. [โครงสร้างพื้นฐานของ Test](#3-โครงสร้างพื้นฐานของ-test)
4. [Locators (การค้นหา Elements)](#4-locators-การค้นหา-elements)
5. [Actions (การโต้ตอบกับ Elements)](#5-actions-การโต้ตอบกับ-elements)
6. [Assertions (การตรวจสอบ)](#6-assertions-การตรวจสอบ)
7. [Page Object Model (POM)](#7-page-object-model-pom)
8. [Cross-Browser Testing](#8-cross-browser-testing)
9. [Visual Regression Testing](#9-visual-regression-testing)
10. [การจัดการ Test Data และ Fixtures](#10-การจัดการ-test-data-และ-fixtures)
11. [Advanced Features](#11-advanced-features)
12. [Debugging และ Reporting](#12-debugging-และ-reporting)
13. [Best Practices และ Tips](#13-best-practices-และ-tips)
14. [แบบฝึกหัด](#14-แบบฝึกหัด)

---

## 0. ความรู้พื้นฐาน: ทำไมต้อง E2E Testing?

### 🤔 ปัญหาที่ E2E Testing แก้ได้

ในการพัฒนา Web Application เรามี 3 ระดับการทดสอบ:

```
┌─────────────────────────────────────────────────────────┐
│                    Testing Pyramid                      │
├─────────────────────────────────────────────────────────┤
│                        ▲                                │
│                       ╱ ╲        E2E Tests              │
│                      ╱   ╲      (5-10%)                 │
│                     ╱─────╲     - ทดสอบ User Journey   │
│                    ╱       ╲    - ทั้งระบบทำงานได้ไหม     │
│                   ╱         ╲                           │
│                  ╱           ╲   Integration Tests      │
│                 ╱    (20%)    ╲ (15-20%)                │
│                ╱───────────────╲  - API + UI ทำงาน      │
│               ╱                 ╲   ร่วมกัน               │
│              ╱  Unit Tests (70%) ╲ - Database queries   │
│             ╱═════════════════════╲                     │
│            ╱- Function logic ✓     ╲                    │
│           ╱- Edge cases ✓           ╲                   │
│          ╱- Error handling ✓         ╲                  │
│         └─────────────────────────────┘                 │
└─────────────────────────────────────────────────────────┘
```

### ตัวอย่างจริง: Library Management System

**❌ ถ้าทดสอบเพียง Unit Tests + Integration Tests เท่านั้น**

```javascript
// Unit Test: ✅ PASS
test("calculatePrice function should add discount correctly", () => {
  expect(calculatePrice(100, 0.2)).toBe(80);
});

// Integration Test: ✅ PASS
test("API should return books list from database", async () => {
  const response = await fetch("/api/books");
  expect(response.status).toBe(200);
});

// ✅ ทั้งหมด PASS! ดี!
```

**แต่ปัญหาเกิดขึ้นเมื่อ User ใช้งานจริง:**

1. **Frontend Bug** - JavaScript error หลังจาก click ปุ่ม (UI Tests ไม่ได้ทำ)
2. **Data Flow Issue** - ข้อมูลจากฐานข้อมูล load บนหน้าเว็บไม่ถูก (Integration Tests ไม่ได้ check)
3. **Cross-Browser Issue** - ทำงานใน Chrome แต่พังใน Safari
4. **Performance Issue** - Page load ช้ามากทำให้ User กดปุ่มซ้ำๆ

**✅ ด้วย E2E Testing จะทำให้เจอปัญหา User ได้จริง**

```javascript
// E2E Test: ❌ FAIL - จะเจอ Bug ที่ User พบจริง!
test("user can borrow book successfully", async ({ page }) => {
  // 1. User ไปหน้า library
  await page.goto("/library");

  // 2. ค้นหาหนังสือ
  await page.fill("#search", "JavaScript");
  await page.click("#search-btn");

  // ❌ FAIL: หน้าเว็บครั่งเตือนหรือโหลดไม่สำเร็จ
  // (Bug ที่ Unit Tests ไม่ได้ทำ)

  // 3. Click Borrow
  const borrowBtn = page.getByRole("button", { name: "Borrow" });
  await expect(borrowBtn).toBeVisible(); // อาจพังตรงนี้

  // 4. ตรวจสอบหนังสือถูก Add ไปยัง "My Books"
  await expect(page.locator("#my-books")).toContainText("JavaScript");
});
```

### 🎯 ประโยชน์ของ E2E Testing

| ด้าน                      | ประโยชน์                                            |
| ------------------------- | --------------------------------------------------- |
| **User Confidence**       | ✅ ตรวจสอบ Real User Journey ไม่ใช่เฉพาะ Component  |
| **Bug Detection**         | ✅ เจอ Integration Issues (API + UI + Database)     |
| **Cross-Browser**         | ✅ ตรวจสอบทำงานบน Chrome, Firefox, Safari           |
| **Regression Prevention** | ✅ ป้องกัน Features เดิมพังเมื่อเพิ่ม Features ใหม่ |
| **Documentation**         | ✅ Code เป็น "สเปค" ของ System (Executable Spec)    |

### 📊 ข้อมูลสถิติ

```
จากการสำรวจ 500+ Software Teams:

E2E Tests ช่วยจับ Bug ได้:
- 45% ของ Bugs ทั้งหมด ⭐⭐⭐⭐
- ลดจำนวน Production Issues ลง 60%
- ROI ใน 6 เดือน แรก: 300%

ที่สำคัญ:
- Unit Tests ตรวจสอบ "Unit ทำงานถูก"
- E2E Tests ตรวจสอบ "ระบบทั้งหมดทำงานถูก"
```

---

## 1. บทนำ Playwright Framework

### 1.1 Playwright คืออะไร?

**Playwright** เป็น Modern End-to-End Testing Framework ที่พัฒนาโดย Microsoft ออกแบบมาเพื่อทดสอบ Web Applications แบบอัตโนมัติ รองรับการทำงานกับ browsers หลายตัวพร้อมกัน

### 1.2 คุณสมบัติเด่นของ Playwright

| คุณสมบัติ                | รายละเอียด                                |
| ------------------------ | ----------------------------------------- |
| **Cross-Browser**        | รองรับ Chromium, Firefox, WebKit (Safari) |
| **Auto-Wait**            | รอ elements พร้อมก่อนทำ action อัตโนมัติ  |
| **Web-First Assertions** | Assertions ที่ออกแบบมาสำหรับ web โดยเฉพาะ |
| **Parallel Testing**     | รัน tests พร้อมกันหลาย workers            |
| **Tracing & Debugging**  | เครื่องมือ debug ที่ทรงพลัง               |
| **Mobile Emulation**     | จำลอง mobile devices ได้                  |
| **Network Interception** | ดักจับและ mock network requests           |
| **Visual Testing**       | Screenshot comparison ในตัว               |
| **Code Generation**      | สร้าง test code จากการ record actions     |

### 1.3 Playwright vs Selenium vs Cypress

| Feature        | Playwright           | Selenium       | Cypress         |
| -------------- | -------------------- | -------------- | --------------- |
| Speed          | ⭐⭐⭐⭐⭐           | ⭐⭐⭐         | ⭐⭐⭐⭐        |
| Cross-Browser  | ✅ All modern        | ✅ All         | ⚠️ Limited      |
| Auto-Wait      | ✅ Built-in          | ❌ Manual      | ✅ Built-in     |
| Parallel       | ✅ Native            | ⚠️ Grid needed | ⚠️ Paid feature |
| Mobile         | ✅ Emulation         | ✅ Appium      | ❌ No           |
| Language       | JS/TS/Python/C#/Java | Many           | JS/TS only      |
| Learning Curve | Medium               | High           | Low             |

### 🎯 เลือก Framework ไหน? (Decision Matrix)

**ใช้ Playwright เมื่อ:**

- ✅ ต้องรัน Tests บน Desktop + Mobile browsers พร้อมกัน
- ✅ ต้อง Performance ที่ดี (Parallel execution)
- ✅ ต้องจำลอง Network requests / API mocking
- ✅ ทำงาน Teams ขนาดกลางถึงใหญ่ (ต้อง CI/CD integration)
- ✅ โปรเจกต์ "ระยะยาว" ต้อง Maintainability

**ใช้ Selenium เมื่อ:**

- ✅ ต้องรองรับ Legacy Browsers (IE, Old Firefox)
- ✅ ต้องทำงานกับ Mobile Apps (Native + Web via Appium)
- ✅ ต้องใช้ได้ที่สุด (ตัวแปรโอเค แล้ว)

**ใช้ Cypress เมื่อ:**

- ✅ Project เล็กๆ / ทีม 1-2 คน
- ✅ ต้อง Learning curve ต่ำ (เข้าใจง่าย)
- ✅ ทำ E2E + Visual Regression test เท่านั้น
- ⚠️ ข้อเสีย: ไม่ support Safari, Firefox ได้ดีและ Cross-browser testing

### 1.4 Testing Pyramid อธิบายรายละเอียด

**Testing Pyramid** บอกเราว่าควรเขียน Tests ประเภทไหนเท่าไหร่ (Ideal Ratio):

E2E Tests (5-10%)

- ทดสอบ "User Journey" ทั้งหมด
- API + Database + Frontend ทำงาน
- ข้อดี: ตรวจสอบ Real User experience
- ข้อเสีย: ช้า

Integration (15-20%)

- ทดสอบ API + UI ทำงานร่วมกัน
- ตรวจสอบ Database queries
- ข้อดี: หลีกเลี่ยง Mocking
- ข้อเสีย: ยังต้อง Manual

Unit Tests (70%)

- ทดสอบ Individual Functions
- Edge cases Error handling
- ข้อดี: เร็วมาก เพราะไม่ต้อง connect อะไร

#### 🔍 ตัวอย่างเปรียบเทียบ: Library Borrowing Feature

**Unit Test (Jest)** - ตรวจเฉพาะ Function Logic

```javascript
// ✅ PASS - Function logic ถูก
test("calculateFine should compute 5 baht per day", () => {
  const daysLate = 3;
  expect(calculateFine(daysLate)).toBe(15); // 3 * 5
});
```

**Integration Test** - ตรวจ API + Database ทำงานร่วมกัน

```javascript
// ✅ PASS - API ส่งค่า ถูก Database บันทึกได้
test("POST /api/borrow should save book to database", async () => {
  const response = await fetch("/api/borrow", {
    method: "POST",
    body: JSON.stringify({ bookId: 1, memberId: 5 }),
  });
  expect(response.status).toBe(200);

  // ตรวจ Database
  const book = await db.query("SELECT * FROM borrowings WHERE id=?");
  expect(book.memberId).toBe(5);
});
```

**E2E Test (Playwright)** - ตรวจ User Journey ทั้งหมด

```javascript
// ❌ FAIL - แม้ Unit + Integration tests ผ่าน
// แต่ User เจออะไร?
test("user can borrow book from homepage", async ({ page }) => {
  // 1. User ไปหน้า Library
  await page.goto("http://localhost:3000/library");

  // 2. ค้นหาหนังสือ
  await page.fill("#search", "JavaScript Guide");
  await page.click("#search-btn");

  // 3. ดูรายละเอียด
  await page.getByText("JavaScript Guide").click();

  // ❌ FAIL - หนังสือเสิญหาย? Frontend error?
  // (Unit test ไม่ได้ cover)

  // 4. Click Borrow
  const borrowBtn = page.getByRole("button", { name: "Borrow" });
  await borrowBtn.click();

  // 5. ตรวจสอบหนังสือ add ไป "My Books"
  await page.goto("/my-books");
  await expect(page.getByText("JavaScript Guide")).toBeVisible();
});
```

#### 📊 สรุป: ทำไมต้องหลายระดับ

| ระดับ           | ทำไมต้อง                  | ตัวอย่าง Bug ที่จะจับได้                                            |
| --------------- | ------------------------- | ------------------------------------------------------------------- |
| **Unit**        | ตรวจ Logic ของ Function   | `price = 100 - 20` ควรได้ 80 ไม่ใช่ 120                             |
| **Integration** | ตรวจ Component ทำงานร่วม  | API ส่ง Data ผิด Format, Database connection fail                   |
| **E2E**         | ตรวจ User experience จริง | Click ไม่ได้ เพราะ JavaScript error, Page load ไม่สำเร็จ, Style ผิด |

#### ⚡ ความเร็วเปรียบเทียบ

```
Unit Test (Jest):         ⚡ 50 ms   (0.05 วินาที)
Integration Test:         ⚡ 500 ms  (0.5 วินาที)
E2E Test (Playwright):    ⚡ 5000 ms (5 วินาที)

บน 100 tests:
- Unit: 5 วินาที ✅ เร็วมาก
- Integration: 50 วินาที
- E2E: 500 วินาที (8 นาที 20 วินาที) ⚠️ นาน

ทำไมต้องรอนาน? เพราะต้องเปิดเบราว์เซอร์, โหลด HTML/CSS/JS, รอ
API responses, รอ Database queries ทั้งหมด
```

---

## 2. การติดตั้งและตั้งค่าเบื้องต้น

### 2.1 ข้อกำหนดเบื้องต้น (Prerequisites)

- Node.js เวอร์ชัน 18 LTS หรือสูงกว่า
- npm หรือ yarn
- VS Code (แนะนำ)

### 2.2 การติดตั้ง Playwright

**ทำไมต้องติดตั้ง Browsers?**
Playwright จะควบคุมเบราว์เซอร์เพื่อเข้าใจการทำงานจริงของ Web App คล้ายกับ User ใช้งาน แต่เบราว์เซอร์ต้องพิเศษสำหรับการ Automate (ไม่ใช่ Chrome ปกติ)

#### วิธีที่ 1: สร้างโปรเจกต์ใหม่ (แนะนำ) 🌟

```bash
# สร้างโปรเจกต์ Playwright ใหม่พร้อม Templates
npm init playwright@latest

# ระบบจะถามคำถาม interactive:
# ✔ Do you want to use TypeScript or JavaScript? · JavaScript
# ✔ Where to put your end-to-end tests? · tests
# ✔ Add a GitHub Actions workflow? · false (เพิ่มได้ทีหลัง)
# ✔ Install Playwright browsers? · true (ต้องตอบ true)
```

**ข้อดี:**

- ✅ ติดตั้งสำเร็จในครั้งเดียว
- ✅ ได้ Templates พื้นฐาน
- ✅ ไม่ต้องตั้งค่า config เอง

#### วิธีที่ 2: เพิ่มเข้าโปรเจกต์ที่มีอยู่แล้ว

```bash
# ลง npm package
npm install --save-dev @playwright/test

# ติดตั้ง browsers เทพพลัง
npx playwright install

# ถ้าติดตั้ง browsers ไม่หมด ให้บังคับติดตั้งทั้งหมด
npx playwright install --with-deps

# ตรวจสอบ browser ที่ติดตั้งแล้ว
npx playwright install --list
```

**ข้อดี:**

- ✅ ติดตั้งได้ on-demand บน Project ที่มี
- ✅ สามารถเลือก Browser เฉพาะตัวที่ต้อง

#### ⚡ วิธี 1 vs วิธี 2

| ลักษณะ       | วิธี 1   | วิธี 2  |
| ------------ | -------- | ------- |
| โปรเจกต์ใหม่ | ✅ Best  | ❌      |
| โปรเจกต์เก่า | ❌       | ✅ Best |
| บัญชี        | สร้างให้ | ต้องเอง |
| แก้ไข config | ไม่ต้อง  | ต้องเอง |

---

### 2.3 โครงสร้างโฟลเดอร์หลังติดตั้ง

```
my-project/
├── tests/
│   └── example.spec.js       # ไฟล์ test ตัวอย่าง
├── tests-examples/
│   └── demo-todo-app.spec.js # ตัวอย่าง todo app test
├── playwright.config.js       # ไฟล์ configuration
├── package.json
└── package-lock.json
```

---

### 2.4 การตั้งค่า playwright.config.js

**ทำไมต้องใช้ Config File?**
เพราะ Playwright มีหลายตัวเลือก (browsers, workers, reporters) และ Config File ช่วยให้ตั้งค่าครั้งเดียวแล้วใช้ซ้ำได้ ไม่ต้องพิมพ์คำสั่ง CLI ซ้ำๆ

```javascript
// playwright.config.js
const { defineConfig, devices } = require("@playwright/test");

module.exports = defineConfig({
  // 📁 โฟลเดอร์ที่เก็บ tests
  // ⚠️ ต้องตรงกับที่จริง มิฉะนั้น Playwright จะหาไฟล์ไม่เจอ
  testDir: "./tests",

  // ⚡ รัน tests แบบ Parallel (พร้อมกันหลาย Process)
  // ข้อดี: เร็วขึ้น 3-5 เท่า! (4 workers = 1/4 เวลา)
  // ⚠️ ข้อเสีย: ใช้ RAM มากขึ้น บางครั้งอาจ Conflict
  fullyParallel: true,

  // 🚫 ห้าม test.only() ใน CI environment
  // เหตุผล: Developer อาจลืม .only() และ deploy
  // แล้วจึงรันแค่ Test เดียวในการ Production
  forbidOnly: !!process.env.CI,

  // 🔄 จำนวนครั้งที่ Retry เมื่อ Test FAIL
  // - Local: ไม่ retry (ให้ developer fix ทันที)
  // - CI: retry 2 ครั้ง (เพราะ CI Server อาจ slow)
  retries: process.env.CI ? 2 : 0,

  // 👷 จำนวน Workers (Parallel Processes)
  // - Local: undefined = ให้ Playwright เลือก (ปกติ = จำนวน CPU cores)
  // - CI: 1 worker = ทำทีละ test (ประหยัด Resource)
  workers: process.env.CI ? 1 : undefined,

  // 📊 Reporter - วิธีแสดงผล Test
  reporter: [
    ["html"], // บันทึก HTML report ใน ./playwright-report/
    ["list"], // แสดง List ใน Console
    // ตัวอื่น: ["json"], ["junit"], ["github"]
  ],

  // ⚙️ Global Settings ใช้สำหรับทุก Tests
  use: {
    // 🌐 Base URL - URL หลักของ Web App
    // แล้ว page.goto("/") จะไป http://localhost:3000/
    baseURL: "http://localhost:3000",

    // 🎬 Trace - บันทึก Action ทั้งหมด (เมื่อ FAIL)
    // "on-first-retry" = บันทึกเมื่อ retry ครั้งแรก
    // ช่วยเหลือในการ Debug: เห็น DOM, Network, Console ทั้งหมด
    trace: "on-first-retry",

    // 📸 Screenshot - เก็บภาพ Screen เมื่อ FAIL
    // "only-on-failure" = เก็บแค่เมื่อ FAIL (ประหยัด Storage)
    screenshot: "only-on-failure",

    // 🎥 Video Recording - บันทึก Video ของ User Journey
    // "retain-on-failure" = เก็บวิดีโอ แค่เมื่อ FAIL
    video: "retain-on-failure",
  },

  // 🌐 Browsers ที่จะทดสอบ (กำหนดใน projects)
  // Playwright จะรัน Test ทั้งหมด บน Browsers แต่ละตัว
  projects: [
    // ✅ Desktop Browsers
    {
      name: "chromium",
      use: { ...devices["Desktop Chrome"] }, // Chrome/Chromium
    },
    {
      name: "firefox",
      use: { ...devices["Desktop Firefox"] }, // Firefox
    },
    {
      name: "webkit",
      use: { ...devices["Desktop Safari"] }, // Safari (WebKit Engine)
    },

    // ✅ Mobile Browsers (จำลอง Mobile Device)
    {
      name: "Mobile Chrome",
      use: { ...devices["Pixel 5"] }, // จำลอง Pixel 5 (Android)
    },
    {
      name: "Mobile Safari",
      use: { ...devices["iPhone 12"] }, // จำลอง iPhone 12 (iOS)
    },
  ],

  // 🚀 Web Server - รัน Server ก่อน Tests
  // เหตุผล: Tests ต้องเชื่อมต่อ Web App ที่กำลังรัน
  webServer: {
    command: "npm run start",
    url: "http://localhost:3000",
    reuseExistingServer: !process.env.CI,
    timeout: 120 * 1000,
  },
});
```

### 2.5 การตั้งค่า package.json Scripts

```json
{
  "scripts": {
    "test": "playwright test",
    "test:headed": "playwright test --headed",
    "test:ui": "playwright test --ui",
    "test:debug": "playwright test --debug",
    "test:chromium": "playwright test --project=chromium",
    "test:firefox": "playwright test --project=firefox",
    "test:webkit": "playwright test --project=webkit",
    "test:mobile": "playwright test --project='Mobile Chrome'",
    "test:report": "playwright show-report",
    "codegen": "playwright codegen localhost:3000"
  }
}
```

### 2.6 VS Code Extension

ติดตั้ง **Playwright Test for VS Code** extension:

- รัน tests จาก VS Code
- Debug tests
- View test results
- Record new tests

### 2.7 โครงสร้างโฟลเดอร์แนะนำสำหรับโปรเจกต์จริง

```
library-management-tests/
├── tests/
│   ├── e2e/                    # End-to-end tests
│   │   ├── auth/
│   │   │   ├── login.spec.js
│   │   │   └── register.spec.js
│   │   ├── books/
│   │   │   ├── search.spec.js
│   │   │   ├── borrow.spec.js
│   │   │   └── return.spec.js
│   │   └── user/
│   │       └── profile.spec.js
│   ├── visual/                 # Visual regression tests
│   │   ├── homepage.spec.js
│   │   └── components.spec.js
│   └── api/                    # API tests (optional)
│       └── books-api.spec.js
├── pages/                      # Page Object Models
│   ├── LoginPage.js
│   ├── HomePage.js
│   ├── SearchPage.js
│   └── BookDetailPage.js
├── fixtures/                   # Test fixtures
│   └── test-data.js
├── utils/                      # Utilities
│   └── helpers.js
├── playwright.config.js
└── package.json
```

---

## 3. โครงสร้างพื้นฐานของ Test

### 3.1 Test แรก - มาเข้าใจทีละบรรทัด

**ทำไมต้องเข้าใจโครงสร้าง Test?**
เพราะทุกครั้งที่คุณเขียน Test ต้องตามรูปแบบเดียวกัน รู้ว่า `test()`, `expect()`, `async` หมายถึงอะไร จึงได้เขียน Test ได้ถูกต้องได้เร็ว

```javascript
// tests/example.spec.js

// 📚 นำเข้าเครื่องมือทั่วไป
// test() = ฟังก์ชันสร้าง Test Case
// expect() = ฟังก์ชัน Assertion (ตรวจสอบผลลัพธ์)
const { test, expect } = require("@playwright/test");

// 🧪 สร้าง Test Case ชื่อ "has title"
test("has title", async ({ page }) => {
  // 🌐 ไปยังหน้าเว็บที่ URL นี้
  // page = Object ที่ Playwright ให้มา เป็นตัวแทน Browser Tab
  // await = รอให้การขึ้นหน้าเสร็จจึงจะไปบรรทัดถัดไป
  await page.goto("https://playwright.dev/");

  // ✅ ตรวจสอบ: Page Title มี "Playwright" หรือไม่
  // toHaveTitle() = Assertion ว่า Page title ตรงกัน
  // /Playwright/ = Regular Expression (Pattern) ตรวจสอบว่ามีคำว่า "Playwright"
  await expect(page).toHaveTitle(/Playwright/);
});

// 🧪 Test Case ที่ 2
test("get started link", async ({ page }) => {
  await page.goto("https://playwright.dev/");

  // 🖱️ ค้นหา Link ที่มี Text "Get started" แล้ว Click
  // getByRole() = Locator Strategy ค้นหาตาม Semantic Role
  // "link" = ประเภท Element (HTML <a> tag)
  // { name: "Get started" } = ตัวเลือก ค้นหา Text ที่ตรงกัน
  await page.getByRole("link", { name: "Get started" }).click();

  // ✅ ตรวจสอบ: มี Heading ชื่อ "Installation" บนหน้าไหม
  // toBeVisible() = ตรวจสอบว่า Element มองเห็นได้
  await expect(
    page.getByRole("heading", { name: "Installation" }),
  ).toBeVisible();
});
```

#### 🔑 Key Concepts ที่ต้องเข้าใจ

| คำศัพท์      | ความหมาย                              | ตัวอย่าง                                              |
| ------------ | ------------------------------------- | ----------------------------------------------------- |
| **test()**   | ฟังก์ชันสร้าง 1 Test Case             | `test("user can login", async ({ page }) => { ... })` |
| **async**    | ฟังก์ชันอาจจะ "รอ" บางอย่าง           | การไปเว็บ, ค้นหา Element, กด button ใช้ `await`       |
| **page**     | ตัวแทนของ Browser Tab                 | `.goto()`, `.click()`, `.fill()`                      |
| **expect()** | เช็ค Assert ว่า "ผลลัพธ์เป็นอย่างนี้" | `expect(page).toHaveTitle("...") `                    |
| **await**    | "รอให้เสร็จจึงไปบรรทัดถัดไป"          | `await page.goto()` = รอจนหน้าเว็บโหลดเสร็จ           |

---

### 3.2 รัน Tests - โหมดต่างๆ

**ทำไมมีหลายโหมดรัน?**

- ☑️ **Headless** = ทำงานเบื้องหลัง (ไม่เห็น Browser) ⚡ เร็ว ใช้ CI/CD
- ☑️ **Headed** = เห็น Browser ทำงาน 🔍 Debug ได้
- ☑️ **UI Mode** = มี GUI ให้คลิก 🎨 Develop ได้

```bash
# ⚡ Headless Mode - ใช้ใน CI/CD Pipeline (GitHub Actions เป็นต้น)
# ข้อดี: เร็ว, ใช้ Resource น้อย
npx playwright test

# 🔍 Headed Mode - เห็น Browser ทำงาน (Debug ตัวเอง)
npx playwright test --headed

# 🎨 UI Mode - มี GUI ให้ Interact (พัฒนา Test ได้)
# แนะนำสำหรับ Development! เห็น Step-by-step ทำงาน
npx playwright test --ui

# 🎯 รันเฉพาะ Tests บางไฟล์
npx playwright test tests/login.spec.js

# 🔎 รันเฉพาะ Tests ที่มี "login" ในชื่อ (ใช้ Regex)
npx playwright test --grep "login"

# ❌ รันทั้งหมด ยกเว้นที่มี "skip" (ใช้ --grep-invert)
npx playwright test --grep-invert "skip"

# 🌐 รันเฉพาะ Browser เดียว
npx playwright test --project=chromium
npx playwright test --project=firefox --project=webkit

# 🐛 Debug Mode - ทีละ Step พร้อม Inspector
npx playwright test --debug
```

#### 🏃 เลือกโหมดไหน?

| สถานการณ์               | โหมด                             | เหตุผล                   |
| ----------------------- | -------------------------------- | ------------------------ |
| **พัฒนา Test ใหม่**     | `--ui`                           | เห็น Step-by-step ได้เลย |
| **Debug Test ที่ Fail** | `--headed` หรือ `--debug`        | เห็น Browser + Inspector |
| **ก่อน Commit Code**    | `npx playwright test` (headless) | เช็คว่า Pass ทั้งหมด     |
| **CI/CD Pipeline**      | `npx playwright test`            | ประหยัด Resource         |

---

### 3.3 โครงสร้างของ Test File

```javascript
const { test, expect } = require("@playwright/test");

// Hook: รันก่อนทุก test ใน file
test.beforeEach(async ({ page }) => {
  await page.goto("/");
});

// Hook: รันหลังทุก test
test.afterEach(async ({ page }) => {
  // cleanup
});

// Hook: รันครั้งเดียวก่อน tests ทั้งหมด
test.beforeAll(async () => {
  // global setup
});

// Hook: รันครั้งเดียวหลัง tests ทั้งหมด
test.afterAll(async () => {
  // global teardown
});

// Test case
test("test name", async ({ page }) => {
  // test code
});

// Test พร้อม description
test("user can login with valid credentials", async ({ page }) => {
  // Arrange
  await page.goto("/login");

  // Act
  await page.fill("#username", "testuser");
  await page.fill("#password", "password123");
  await page.click("#login-button");

  // Assert
  await expect(page).toHaveURL("/dashboard");
});
```

### 3.4 การจัดกลุ่ม Tests ด้วย describe

```javascript
const { test, expect } = require("@playwright/test");

test.describe("Authentication", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/login");
  });

  test.describe("Login", () => {
    test("should login with valid credentials", async ({ page }) => {
      await page.fill("#username", "testuser");
      await page.fill("#password", "password123");
      await page.click("#login-button");

      await expect(page).toHaveURL("/dashboard");
    });

    test("should show error with invalid credentials", async ({ page }) => {
      await page.fill("#username", "wronguser");
      await page.fill("#password", "wrongpass");
      await page.click("#login-button");

      await expect(page.locator(".error-message")).toBeVisible();
      await expect(page.locator(".error-message")).toContainText("Invalid");
    });

    test("should show error for empty fields", async ({ page }) => {
      await page.click("#login-button");

      await expect(page.locator("#username:invalid")).toBeVisible();
    });
  });

  test.describe("Logout", () => {
    test.beforeEach(async ({ page }) => {
      // Login first
      await page.fill("#username", "testuser");
      await page.fill("#password", "password123");
      await page.click("#login-button");
      await expect(page).toHaveURL("/dashboard");
    });

    test("should logout successfully", async ({ page }) => {
      await page.click("#logout-button");

      await expect(page).toHaveURL("/login");
    });
  });
});
```

### 3.5 Test Annotations

```javascript
const { test, expect } = require("@playwright/test");

// Skip test
test.skip("skipped test", async ({ page }) => {
  // จะไม่ถูกรัน
});

// Skip conditionally
test("skip on webkit", async ({ page, browserName }) => {
  test.skip(browserName === "webkit", "Not supported on Safari");
  // ...
});

// Run only this test
test.only("focused test", async ({ page }) => {
  // รันเฉพาะ test นี้
});

// Mark as failing (expected to fail)
test.fail("known bug", async ({ page }) => {
  // test ที่รู้ว่า fail
});

// Slow test - เพิ่ม timeout
test.slow("slow test", async ({ page }) => {
  // timeout จะเพิ่มเป็น 3 เท่า
});

// Custom timeout
test("custom timeout", async ({ page }) => {
  test.setTimeout(60000); // 60 seconds
  // ...
});

// Add tags
test("login @smoke @auth", async ({ page }) => {
  // รันด้วย: npx playwright test --grep @smoke
});
```

### 3.6 Parameterized Tests

```javascript
const { test, expect } = require("@playwright/test");

// Test หลายค่า
const testCases = [
  { username: "user1", password: "pass1", expected: "Welcome, user1" },
  { username: "user2", password: "pass2", expected: "Welcome, user2" },
  { username: "admin", password: "admin123", expected: "Welcome, admin" },
];

for (const { username, password, expected } of testCases) {
  test(`login as ${username}`, async ({ page }) => {
    await page.goto("/login");
    await page.fill("#username", username);
    await page.fill("#password", password);
    await page.click("#login-button");

    await expect(page.locator(".welcome-message")).toContainText(expected);
  });
}

// หรือใช้ test.describe.configure
test.describe("Login tests", () => {
  const users = ["user1", "user2", "user3"];

  for (const user of users) {
    test(`should login as ${user}`, async ({ page }) => {
      // ...
    });
  }
});
```

---

## 4. Locators (การค้นหา Elements)

Locators เป็นวิธีการระบุ elements บนหน้าเว็บ Playwright มี locators หลายแบบ

### 🎯 ทำไมต้องเลือก Locators อย่างถูกต้อง?

การเลือก Locator ที่ดี คือ **"หัวใจของการทำ E2E Testing ที่ยั่งยืน"** ครับ เพราะ:

1. **CSS Selectors แบบ Fragile** - ถ้า Developer เปลี่ยน Class name หรือ ID ก็พังทันที

   ```javascript
   // ❌ Bad - พังง่าย ถ้า HTML เปลี่ยน
   await page.locator(".btn.btn-primary.btn-lg.submit-form-btn").click();
   // ถ้า HTML เปลี่ยนเป็น <button class="submit btn-primary"> ก็พัง!
   ```

2. **getByRole() = Accessible** - ใช้ ARIA roles ซึ่งบ่งบอกว่า Element นั้น "มีความหมาย" แบบไหน

   ```javascript
   // ✅ Good - เสถียร เพราะใช้ semantic meaning
   await page.getByRole("button", { name: "Submit" }).click();
   // ทำงาน ไม่ว่า HTML ด้านใน มีความเปลี่ยน "ตราบใดที่มันยังเป็นปุ่ม"
   ```

3. **ตรงกับ UX** - `getByRole` ใช้วิธีเดียวกับ **Screen Reader** ซึ่งหมายความว่า
   - ถ้า Locators ของเราทำงาน → UI สามารถเข้าถึงได้ (Accessible)
   - ถ้า Locators ของเรา Fail → UI มีปัญหา Accessibility ด้วย

### 4.1 Recommended Locators (แนะนำ)

```javascript
const { test, expect } = require("@playwright/test");

test("recommended locators", async ({ page }) => {
  await page.goto("/");

  // 1. getByRole - ดีที่สุด! ใช้ accessibility role
  await page.getByRole("button", { name: "Submit" }).click();
  await page.getByRole("link", { name: "Home" }).click();
  await page.getByRole("heading", { name: "Welcome" });
  await page.getByRole("textbox", { name: "Email" }).fill("test@example.com");
  await page.getByRole("checkbox", { name: "Remember me" }).check();
  await page
    .getByRole("combobox", { name: "Country" })
    .selectOption("Thailand");

  // 2. getByLabel - สำหรับ form elements
  await page.getByLabel("Username").fill("testuser");
  await page.getByLabel("Password").fill("password123");
  await page.getByLabel("Remember me").check();

  // 3. getByPlaceholder - ใช้ placeholder text
  await page.getByPlaceholder("Enter your email").fill("test@example.com");
  await page.getByPlaceholder("Search...").fill("javascript");

  // 4. getByText - ใช้ text content
  await page.getByText("Welcome back!");
  await page.getByText("Click here", { exact: true }); // exact match
  await page.getByText(/welcome/i); // regex, case-insensitive

  // 5. getByAltText - สำหรับ images
  await page.getByAltText("Company Logo").click();

  // 6. getByTitle - ใช้ title attribute
  await page.getByTitle("Close dialog").click();

  // 7. getByTestId - ใช้ data-testid attribute (fallback)
  await page.getByTestId("submit-button").click();
  await page.getByTestId("user-avatar").click();
});
```

### 4.2 CSS และ XPath Locators

```javascript
test("css and xpath locators", async ({ page }) => {
  // CSS Selectors
  await page.locator("#login-button").click(); // ID
  await page.locator(".btn-primary").click(); // Class
  await page.locator("button.submit").click(); // Tag + Class
  await page.locator('[data-testid="submit"]').click(); // Attribute
  await page.locator('input[type="email"]').fill("test@example.com");
  await page.locator("div.card > h2").click(); // Child selector
  await page.locator("ul.menu li:first-child").click(); // Pseudo-class
  await page.locator(".item:nth-child(3)").click(); // nth-child

  // XPath (ใช้เมื่อจำเป็นเท่านั้น)
  await page.locator('xpath=//button[@id="submit"]').click();
  await page.locator('xpath=//div[contains(@class, "card")]//h2').click();
  await page.locator('xpath=//a[text()="Click here"]').click();
});
```

### 4.3 Filtering Locators

```javascript
test("filtering locators", async ({ page }) => {
  // Filter by text
  await page.locator(".product-card").filter({ hasText: "JavaScript" }).click();

  // Filter by NOT having text
  await page
    .locator(".product-card")
    .filter({ hasNotText: "Sold Out" })
    .click();

  // Filter by child element
  await page
    .locator(".product-card")
    .filter({
      has: page.locator(".in-stock-badge"),
    })
    .click();

  // Filter by NOT having child
  await page
    .locator(".product-card")
    .filter({
      hasNot: page.locator(".sold-out-badge"),
    })
    .click();

  // Chaining filters
  await page
    .locator(".product-card")
    .filter({ hasText: "JavaScript" })
    .filter({ has: page.locator(".in-stock-badge") })
    .click();
});
```

### 4.4 Locator Operations

```javascript
test("locator operations", async ({ page }) => {
  // first, last, nth
  await page.locator(".item").first().click();
  await page.locator(".item").last().click();
  await page.locator(".item").nth(2).click(); // index 2 (third item)

  // count
  const itemCount = await page.locator(".item").count();
  console.log(`Found ${itemCount} items`);

  // all - get all matching elements
  const items = await page.locator(".item").all();
  for (const item of items) {
    console.log(await item.textContent());
  }

  // Chaining locators
  await page.locator(".card").locator(".title").click();
  await page.locator(".menu").locator("li").first().click();

  // or - match any
  await page
    .locator("button.primary")
    .or(page.locator("button.submit"))
    .click();

  // and - match all conditions
  await page.locator("button").and(page.locator(".primary")).click();
});
```

### 4.5 Frame Locators

```javascript
test("frame locators", async ({ page }) => {
  // Locate frame by name or URL
  const frame = page.frameLocator('iframe[name="content"]');

  // Interact with elements inside frame
  await frame.locator("#submit-button").click();
  await frame.getByRole("textbox").fill("Hello");

  // Nested frames
  const innerFrame = page.frameLocator("#outer").frameLocator("#inner");
  await innerFrame.locator("button").click();
});
```

### 4.6 ตารางสรุป Locator Strategies

| Priority | Locator              | เมื่อไหร่ใช้                      |
| -------- | -------------------- | --------------------------------- |
| 1        | `getByRole()`        | ทุกครั้งที่เป็นไปได้ (accessible) |
| 2        | `getByLabel()`       | Form inputs ที่มี label           |
| 3        | `getByPlaceholder()` | Inputs ที่มี placeholder          |
| 4        | `getByText()`        | Elements ที่มี text ชัดเจน        |
| 5        | `getByTestId()`      | เมื่อไม่มี option อื่น            |
| 6        | `locator()` CSS      | ต้องการ complex selector          |
| 7        | `locator()` XPath    | Last resort เท่านั้น              |

### 4.7 Best Practice: ใช้ data-testid

เพิ่ม `data-testid` ใน HTML สำหรับ testing:

```html
<!-- HTML -->
<button data-testid="submit-button" class="btn btn-primary">Submit</button>

<div data-testid="user-card" class="card">
  <h2 data-testid="user-name">John Doe</h2>
  <p data-testid="user-email">john@example.com</p>
</div>
```

```javascript
// Test
test("using data-testid", async ({ page }) => {
  await page.getByTestId("submit-button").click();

  const userName = await page.getByTestId("user-name").textContent();
  expect(userName).toBe("John Doe");
});
```

---

## 5. Actions (การโต้ตอบกับ Elements)

**ทำไมต้องรู้ Actions ต่างๆ?**
Actions = วิธีที่ User โต้ตอบกับ Web App (คลิก, พิมพ์, drag-drop ฯลฯ) ถ้าเราเขียน Test ที่ใช้ Actions เหมือน User จริง → Test จึงจับ Real User Issues ได้

### 5.1 Mouse Actions - การใช้เมาส์

```javascript
test("mouse actions - ทำอะไรได้บ้าง", async ({ page }) => {
  // 🖱️ Click ปกติ - ใช้มากที่สุด
  // เหตุ: Button, Link, Menu ทั้งหมดต้อง Click
  await page.locator("#button").click();

  // 🖱️ Right Click - แสดง Context Menu
  // เหตุ: บางครั้งต้องทดสอบ Right-Click Menu (Delete, Edit, Share)
  await page.locator("#button").click({ button: "right" });

  // 🖱️ Double Click - Click 2 ครั้ง
  // เหตุ: การแก้ไข Inline, หรือการเลือก Text ทั้งหมด
  await page.locator("#button").dblclick();

  // 🖱️ Click + Keyboard Modifier (Ctrl, Shift, Alt)
  // เหตุ: Open Link in New Tab (Ctrl+Click), Multi-Select (Shift+Click)
  await page.locator("#link").click({ modifiers: ["Control"] }); // ⌘ Mac
  await page.locator("#link").click({ modifiers: ["Shift"] }); // Shift+Click

  // 🖱️ Click ที่ตำแหน่ง Specific
  // เหตุ: ทดสอบ Canvas, Map, หรือพื้นที่ที่ต้อง Click ที่จุดเฉพาะ
  await page.locator("#canvas").click({ position: { x: 100, y: 200 } });

  // ⚠️ Force Click - Skip ความสำคัญ (Dangerous!)
  // เหตุ: เมื่อ Element ถูก Disabled หรือซ่อนเพราะ Business Logic
  // ข้อเสีย: อาจหลีกเลี่ยง Real Issues (เช่น Button Disabled เพราะ Form Invalid)
  await page.locator("#hidden-button").click({ force: true });

  // 👆 Hover - เลื่อน Mouse ไปเหนือ Element
  // เหตุ: Tooltip, Hover Menu, Highlight Effect
  await page.locator(".menu-item").hover();

  // 🎯 Drag and Drop - ลากไปวาง
  // เหตุ: File Upload, Reorder Items, Move Task
  await page.locator("#source").dragTo(page.locator("#target"));
});
```

#### 🎯 เลือก Action ไหน?

| สิ่งที่ต้องทำ        | Action                               | ตัวอย่าง                                                     |
| -------------------- | ------------------------------------ | ------------------------------------------------------------ |
| Submit Form          | `.click()`                           | `await page.getByRole("button", { name: "Submit" }).click()` |
| Open Link in New Tab | `.click({ modifiers: ["Control"] })` | Cmd/Ctrl+Click link                                          |
| Hover Tooltip        | `.hover()`                           | `await page.locator(".info-icon").hover()`                   |
| Drag to Reorder      | `.dragTo()`                          | Drag task ไปตำแหน่งใหม่                                      |
| Right-Click Menu     | `.click({ button: "right" })`        | Delete, Edit, Copy                                           |

---

### 5.2 Text Input - การพิมพ์ข้อความ

```javascript
test("text input actions", async ({ page }) => {
  // 📝 fill() - ล้างค่าเดิม แล้วใส่ค่าใหม่ (เร็วที่สุด!)
  // ใช้เมื่อ: Form Login, Search, โดยทั่วไป
  // ข้อดี: เร็ว ✅, ไม่ต้อง clear ก่อน
  await page.locator("#username").fill("testuser");
  await page.getByLabel("Email").fill("test@example.com");

  // 📝 clear() - ล้างค่าใน Input
  // ใช้เมื่อ: ต้องลบข้อความออกก่อน (เช่น มีค่า Default)
  await page.locator("#search").clear();

  // 📝 type() - พิมพ์ทีละตัวอักษร (Realistic Typing!)
  // ใช้เมื่อ: ทดสอบ Auto-Complete, Real-time Validation
  // { delay: 100 } = ประมาณ 100ms ระหว่าง Character
  // ข้อดี: เหมือน User พิมพ์จริงๆ
  // ข้อเสีย: ช้า
  await page.locator("#search").type("hello world", { delay: 100 });

  // ⌨️ press() - กด Keyboard Keys
  // เหตุ: Enter เพื่อ Submit, Escape เพื่อ Close, Ctrl+A เพื่อเลือกทั้งหมด
  await page.locator("#search").press("Enter");
  await page.locator("#editor").press("Control+A"); // Select all
  await page.locator("#editor").press("Control+C"); // Copy
  await page.locator("#editor").press("Control+V"); // Paste
  await page.locator("#editor").press("Backspace");
  await page.locator("#editor").press("Tab");

  // 📝 fill() + Special Characters
  // ใช้สำหรับ Password, Email ที่มี Special Chars
  await page.locator("#password").fill("P@ssw0rd!#$%");
});
```

#### 🤔 fill() vs type() - ทำไมมี 2 แบบ?

```javascript
// ❌ ปัญหา: type() ช้า
test("type is slow", async ({ page }) => {
  // type 20 ตัวอักษร = 20 * 100ms = 2000ms (2 วินาที)
  await page.locator("#username").type("verylongusernamehello", { delay: 100 });
  // เอาเวลา!
});

// ✅ แก้ไข: ใช้ fill() สำหรับ Form Login
test("fill is fast", async ({ page }) => {
  await page.locator("#username").fill("verylongusernamehello");
  // ทันที! แต่ไม่มี Key Events
});

// ✅ ใช้ type() สำหรับ Auto-Complete
test("type for autocomplete", async ({ page }) => {
  // ต้อง type ทีละตัว เพื่อให้ Auto-Complete Suggestions เกิดขึ้น
  await page.locator("#country-search").type("Thai", { delay: 50 });
  await expect(page.getByText("Thailand")).toBeVisible();
});
```

---

### 5.3 Form Elements - การทำงานกับ Form

```javascript
test("form element actions", async ({ page }) => {
  // ☑️ Checkbox - ติ๊ก/ยกเลิก
  // ใช้ .check() ถ้าต้องให้ติ๊ก
  await page.locator("#agree").check();
  // ใช้ .uncheck() ถ้าต้องให้ยกเลิก
  await page.locator("#newsletter").uncheck();
  // ใช้ .setChecked() ถ้าต้องตั้งค่า (true/false)
  await page.getByLabel("Remember me").setChecked(true);

  // ✅ ตรวจสอบ Checkbox State
  await expect(page.locator("#agree")).toBeChecked();
  await expect(page.locator("#newsletter")).not.toBeChecked();

  // ⭕ Radio Buttons - เลือก 1 ใน N
  // Radio buttons มักไม่มี .uncheck() ต้อง .check() ตัวใหม่แทน
  await page.locator("#gender-male").check();
  await page.getByLabel("Female").check(); // จะเลิก Male โดยอัตโนมัติ

  // 🔽 Select (Dropdown) - เลือกจากรายการ
  // selectOption() รองรับหลายวิธี: value, label, index
  await page.locator("#country").selectOption("TH"); // by value
  await page.locator("#country").selectOption({ label: "Thailand" }); // by label
  await page.locator("#country").selectOption({ index: 5 }); // by index (0-based)

  // 🔽 Multiple Select - เลือกหลายตัวพร้อม
  await page.locator("#colors").selectOption(["red", "green", "blue"]);

  // 📁 File Upload - เลือกไฟล์
  // setInputFiles() ส่ง File Path ให้กับ <input type="file">
  await page.locator("#file-upload").setInputFiles("path/to/file.pdf");
  // หลายไฟล์
  await page.locator("#file-upload").setInputFiles(["file1.pdf", "file2.pdf"]);

  // 📁 Clear File Input - เลิก Upload
  await page.locator("#file-upload").setInputFiles([]);
});
```

---

### 5.4 Keyboard Actions - การใช้ Keyboard

```javascript
test("keyboard actions", async ({ page }) => {
  // ⌨️ press() - กด Key เดียว
  // ใช้: Enter, Escape, Tab, F5, Delete
  await page.keyboard.press("Escape"); // Close Dialog
  await page.keyboard.press("F5"); // Refresh Page
  await page.keyboard.press("Enter"); // Submit

  // ⌨️ type() - พิมพ์ Text (Global)
  // ใช้เมื่อ Focus ไม่ได้ Specific Element
  await page.keyboard.type("Hello World");

  // ⌨️ Key Combinations - Ctrl, Shift, Alt
  await page.keyboard.press("Control+Shift+P"); // VS Code Cmd Palette
  await page.keyboard.press("Control+Z"); // Undo
  await page.keyboard.press("Control+Y"); // Redo

  // ⌨️ Hold + Release - กด + ปล่อย
  // ใช้: Multi-select (Shift + Click), Drag (Mouse Down + Move + Up)
  await page.keyboard.down("Shift");
  await page.keyboard.press("KeyA");
  await page.keyboard.press("KeyB");
  await page.keyboard.up("Shift");

  // ⌨️ insertText() - ใส่ Text ทันที (ไม่มี Key Events)
  // ใช้เมื่อ: ต้องใส่ Text เร็ว เช่น Paste
  await page.keyboard.insertText("Pasted text");
});
```

---

### 5.5 Navigation - การนำทาง

```javascript
test("navigation actions", async ({ page }) => {
  // 🌐 goto() - ไปยัง URL
  // ใช้มากที่สุด เพื่อเริ่ม Test
  await page.goto("https://example.com");
  await page.goto("/login"); // ใช้ baseURL จาก config

  // ⏳ goto() + Wait Condition
  // { waitUntil: "networkidle" } = รอจนไม่มี Network Request เพิ่มเติม (ช้า)
  // { waitUntil: "domcontentloaded" } = รอจนโหลด HTML/CSS เสร็จ (เร็ว)
  await page.goto("https://example.com", { waitUntil: "networkidle" });
  await page.goto("https://example.com", { waitUntil: "domcontentloaded" });

  // ⬅️ goBack() / ➡️ goForward() - ย้อนกลับ/เดินหน้า
  // ใช้ History Browser (เหมือน Back Button)
  await page.goBack();
  await page.goForward();

  // 🔄 reload() - รีเฟรช Page
  // ใช้เมื่อ: ต้องรีโหลดข้อมูล, เทส Cache Behavior
  await page.reload();

  // 🔗 waitForURL() - รอจนกว่า URL เปลี่ยน
  // ใช้เมื่อ: Click ปุ่มและต้องรอ Navigation
  await page.waitForURL("**/dashboard"); // Wildcard
  await page.waitForURL(/.*\/success/); // Regex

  // 📍 url() - ดึง Current URL
  const currentUrl = page.url();
  console.log("Current URL:", currentUrl);
  await expect(page).toHaveURL("/dashboard");
});
```

---

### 5.6 Waiting - การรอให้พร้อม

```javascript
test("waiting actions - รอจนพร้อม", async ({ page }) => {
  // ⏳ waitFor() - รอให้ Element เปลี่ยนสถานะ
  // { state: "hidden" } = รอจนหายไป (เช่น Loading spinner)
  // { state: "visible" } = รอจนปรากฏ (เช่น Success message)
  await page.locator(".loading").waitFor({ state: "hidden" });
  await page.locator(".content").waitFor({ state: "visible" });

  // ⏳ waitFor() + timeout option
  // ถ้ารอนาน > timeout จะ FAIL (ป้องกัน Infinite Wait)
  await page.locator(".success-message").waitFor({
    state: "visible",
    timeout: 5000, // 5 วินาที
  });

  await page.locator(".modal").waitFor({ state: "attached" });
  await page.locator(".modal").waitFor({ state: "detached" });

  // Wait for network request
  const responsePromise = page.waitForResponse("**/api/users");
  await page.click("#load-users");
  const response = await responsePromise;

  // Wait for request
  const requestPromise = page.waitForRequest("**/api/submit");
  await page.click("#submit");
  const request = await requestPromise;

  // Wait for load state
  await page.waitForLoadState("networkidle");
  await page.waitForLoadState("domcontentloaded");

  // Wait for function to return true
  await page.waitForFunction(() => {
    return document.querySelectorAll(".item").length > 5;
  });

  // Fixed timeout (avoid if possible)
  await page.waitForTimeout(1000); // 1 second
});
```

### 5.7 Dialogs (Alert, Confirm, Prompt)

```javascript
test("handling dialogs", async ({ page }) => {
  // Handle alert
  page.on("dialog", async (dialog) => {
    console.log("Dialog message:", dialog.message());
    await dialog.accept();
  });
  await page.click("#show-alert");

  // Handle confirm
  page.once("dialog", async (dialog) => {
    expect(dialog.type()).toBe("confirm");
    await dialog.accept(); // Click OK
    // await dialog.dismiss();  // Click Cancel
  });
  await page.click("#show-confirm");

  // Handle prompt
  page.once("dialog", async (dialog) => {
    expect(dialog.type()).toBe("prompt");
    await dialog.accept("My Input"); // Enter text and OK
  });
  await page.click("#show-prompt");
});
```

---

## 6. Assertions (การตรวจสอบ)

Playwright มี Web-First Assertions ที่จะ auto-retry จนกว่าจะผ่านหรือ timeout

### 6.1 Page Assertions

```javascript
const { test, expect } = require("@playwright/test");

test("page assertions", async ({ page }) => {
  await page.goto("/");

  // Title
  await expect(page).toHaveTitle("Library Management System");
  await expect(page).toHaveTitle(/Library/); // regex

  // URL
  await expect(page).toHaveURL("http://localhost:3000/");
  await expect(page).toHaveURL(/.*\/dashboard/);
  await expect(page).toHaveURL("/dashboard"); // partial match
});
```

### 6.2 Locator Assertions

```javascript
test("locator assertions", async ({ page }) => {
  await page.goto("/");

  // Visibility
  await expect(page.locator(".header")).toBeVisible();
  await expect(page.locator(".loading")).toBeHidden();
  await expect(page.locator(".loading")).not.toBeVisible();

  // Enabled/Disabled
  await expect(page.locator("#submit-btn")).toBeEnabled();
  await expect(page.locator("#disabled-btn")).toBeDisabled();

  // Text content
  await expect(page.locator(".welcome")).toHaveText("Welcome, John!");
  await expect(page.locator(".welcome")).toHaveText(/Welcome/);
  await expect(page.locator(".welcome")).toContainText("Welcome");

  // Multiple elements text
  await expect(page.locator(".menu-item")).toHaveText([
    "Home",
    "About",
    "Contact",
  ]);

  // Value (for inputs)
  await expect(page.locator("#email")).toHaveValue("test@example.com");
  await expect(page.locator("#email")).toHaveValue(/.*@.*\.com/);

  // Attribute
  await expect(page.locator("#link")).toHaveAttribute("href", "/about");
  await expect(page.locator("#img")).toHaveAttribute("src", /logo\.png/);

  // CSS class
  await expect(page.locator(".btn")).toHaveClass(/active/);
  await expect(page.locator(".btn")).toHaveClass("btn btn-primary");

  // CSS property
  await expect(page.locator(".error")).toHaveCSS("color", "rgb(255, 0, 0)");

  // Count
  await expect(page.locator(".item")).toHaveCount(5);

  // Checkbox/Radio
  await expect(page.locator("#agree")).toBeChecked();
  await expect(page.locator("#newsletter")).not.toBeChecked();

  // Focus
  await expect(page.locator("#username")).toBeFocused();

  // Editable
  await expect(page.locator("#readonly-field")).not.toBeEditable();

  // Empty
  await expect(page.locator("#search")).toBeEmpty();

  // Attached to DOM
  await expect(page.locator(".modal")).toBeAttached();
});
```

### 6.3 Soft Assertions

Soft assertions ไม่หยุด test เมื่อ fail แต่จะรายงานทีหลัง:

```javascript
test("soft assertions", async ({ page }) => {
  await page.goto("/dashboard");

  // ถ้า fail ก็ยังรัน assertions ต่อไป
  await expect.soft(page.locator(".welcome")).toHaveText("Welcome");
  await expect.soft(page.locator(".user-name")).toHaveText("John");
  await expect.soft(page.locator(".notification-count")).toHaveText("5");

  // รายงาน failures ทั้งหมดตอนจบ test
});
```

### 6.4 Custom Timeout

```javascript
test("custom assertion timeout", async ({ page }) => {
  // Default timeout สำหรับ assertions คือ 5 วินาที

  // กำหนด timeout เฉพาะ assertion นี้
  await expect(page.locator(".slow-loading")).toBeVisible({ timeout: 30000 });

  // หรือกำหนดใน config
  // expect.configure({ timeout: 10000 });
});
```

### 6.5 Polling Assertions

```javascript
test("polling assertions", async ({ page }) => {
  // ใช้สำหรับตรวจสอบค่าที่ไม่ใช่ locator
  await expect
    .poll(
      async () => {
        const response = await page.request.get("/api/status");
        return response.status();
      },
      {
        message: "API should return 200",
        timeout: 30000,
      },
    )
    .toBe(200);

  // ตรวจสอบ count ที่เปลี่ยนแปลง
  await expect
    .poll(async () => {
      return await page.locator(".notification").count();
    })
    .toBeGreaterThan(0);
});
```

### 6.6 Generic Assertions (Non-Retrying)

```javascript
test("generic assertions", async ({ page }) => {
  // เหมือน Jest - ไม่ retry
  const title = await page.title();
  expect(title).toBe("My Page");
  expect(title).toContain("Page");

  const count = await page.locator(".item").count();
  expect(count).toBeGreaterThan(0);
  expect(count).toBeLessThanOrEqual(10);

  const text = await page.locator(".message").textContent();
  expect(text).toMatch(/success/i);

  // Arrays
  const items = await page.locator(".item").allTextContents();
  expect(items).toContain("Item 1");
  expect(items).toHaveLength(5);

  // Objects
  const data = await page.evaluate(() => window.appData);
  expect(data).toEqual({ id: 1, name: "Test" });
});
```

---

## 7. Page Object Model (POM)

Page Object Model เป็น design pattern ที่แยก logic ของแต่ละหน้าออกจาก tests ทำให้ code สะอาด บำรุงรักษาง่าย

### 7.1 โครงสร้าง Page Object

```
project/
├── pages/
│   ├── BasePage.js
│   ├── LoginPage.js
│   ├── HomePage.js
│   ├── SearchPage.js
│   └── BookDetailPage.js
├── tests/
│   ├── login.spec.js
│   └── search.spec.js
└── playwright.config.js
```

### 7.2 Base Page Class

```javascript
// pages/BasePage.js
class BasePage {
  constructor(page) {
    this.page = page;
  }

  async navigate(path = "/") {
    await this.page.goto(path);
  }

  async getTitle() {
    return await this.page.title();
  }

  async waitForPageLoad() {
    await this.page.waitForLoadState("networkidle");
  }

  async takeScreenshot(name) {
    await this.page.screenshot({ path: `screenshots/${name}.png` });
  }
}

module.exports = BasePage;
```

### 7.3 Login Page

```javascript
// pages/LoginPage.js
const BasePage = require("./BasePage");

class LoginPage extends BasePage {
  constructor(page) {
    super(page);

    // Locators
    this.usernameInput = page.getByTestId("username");
    this.passwordInput = page.getByTestId("password");
    this.loginButton = page.getByRole("button", { name: "Login" });
    this.errorMessage = page.locator(".error-message");
    this.rememberMeCheckbox = page.getByLabel("Remember me");
    this.forgotPasswordLink = page.getByRole("link", {
      name: "Forgot password?",
    });
  }

  async goto() {
    await this.navigate("/login");
  }

  async login(username, password) {
    await this.usernameInput.fill(username);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
  }

  async loginWithRememberMe(username, password) {
    await this.usernameInput.fill(username);
    await this.passwordInput.fill(password);
    await this.rememberMeCheckbox.check();
    await this.loginButton.click();
  }

  async getErrorMessage() {
    return await this.errorMessage.textContent();
  }

  async isErrorVisible() {
    return await this.errorMessage.isVisible();
  }

  async clickForgotPassword() {
    await this.forgotPasswordLink.click();
  }
}

module.exports = LoginPage;
```

### 7.4 Home Page

```javascript
// pages/HomePage.js
const BasePage = require("./BasePage");

class HomePage extends BasePage {
  constructor(page) {
    super(page);

    // Locators
    this.welcomeMessage = page.getByTestId("welcome-message");
    this.searchInput = page.getByPlaceholder("Search books...");
    this.searchButton = page.getByRole("button", { name: "Search" });
    this.userMenu = page.getByTestId("user-menu");
    this.logoutButton = page.getByRole("menuitem", { name: "Logout" });
    this.bookCards = page.locator(".book-card");
    this.categoriesNav = page.locator(".categories-nav");
  }

  async goto() {
    await this.navigate("/");
  }

  async getWelcomeText() {
    return await this.welcomeMessage.textContent();
  }

  async search(query) {
    await this.searchInput.fill(query);
    await this.searchButton.click();
  }

  async logout() {
    await this.userMenu.click();
    await this.logoutButton.click();
  }

  async getBookCount() {
    return await this.bookCards.count();
  }

  async clickBookByTitle(title) {
    await this.bookCards.filter({ hasText: title }).click();
  }

  async selectCategory(category) {
    await this.categoriesNav.getByRole("link", { name: category }).click();
  }
}

module.exports = HomePage;
```

### 7.5 Search Page

```javascript
// pages/SearchPage.js
const BasePage = require("./BasePage");

class SearchPage extends BasePage {
  constructor(page) {
    super(page);

    // Locators
    this.searchInput = page.getByTestId("search-input");
    this.searchButton = page.getByTestId("search-button");
    this.searchResults = page.locator(".search-result-item");
    this.noResultsMessage = page.getByText("No results found");
    this.filterCategory = page.getByLabel("Category");
    this.filterYear = page.getByLabel("Year");
    this.sortSelect = page.getByLabel("Sort by");
    this.loadingIndicator = page.locator(".loading-spinner");
    this.resultCount = page.getByTestId("result-count");
  }

  async goto() {
    await this.navigate("/search");
  }

  async search(query) {
    await this.searchInput.fill(query);
    await this.searchButton.click();
    await this.loadingIndicator.waitFor({ state: "hidden" });
  }

  async getResultCount() {
    const text = await this.resultCount.textContent();
    return parseInt(text.match(/\d+/)[0]);
  }

  async getResultTitles() {
    return await this.searchResults.locator(".title").allTextContents();
  }

  async filterByCategory(category) {
    await this.filterCategory.selectOption(category);
    await this.loadingIndicator.waitFor({ state: "hidden" });
  }

  async sortBy(option) {
    await this.sortSelect.selectOption(option);
    await this.loadingIndicator.waitFor({ state: "hidden" });
  }

  async clickResult(index) {
    await this.searchResults.nth(index).click();
  }

  async isNoResultsVisible() {
    return await this.noResultsMessage.isVisible();
  }
}

module.exports = SearchPage;
```

### 7.6 Book Detail Page

```javascript
// pages/BookDetailPage.js
const BasePage = require("./BasePage");

class BookDetailPage extends BasePage {
  constructor(page) {
    super(page);

    // Locators
    this.bookTitle = page.getByTestId("book-title");
    this.bookAuthor = page.getByTestId("book-author");
    this.bookISBN = page.getByTestId("book-isbn");
    this.bookDescription = page.getByTestId("book-description");
    this.availableCopies = page.getByTestId("available-copies");
    this.borrowButton = page.getByRole("button", { name: "Borrow" });
    this.reserveButton = page.getByRole("button", { name: "Reserve" });
    this.returnButton = page.getByRole("button", { name: "Return" });
    this.successMessage = page.locator(".success-message");
    this.errorMessage = page.locator(".error-message");
  }

  async goto(bookId) {
    await this.navigate(`/books/${bookId}`);
  }

  async getBookInfo() {
    return {
      title: await this.bookTitle.textContent(),
      author: await this.bookAuthor.textContent(),
      isbn: await this.bookISBN.textContent(),
      description: await this.bookDescription.textContent(),
      availableCopies: parseInt(await this.availableCopies.textContent()),
    };
  }

  async borrowBook() {
    await this.borrowButton.click();
    await this.successMessage.waitFor({ state: "visible" });
  }

  async reserveBook() {
    await this.reserveButton.click();
    await this.successMessage.waitFor({ state: "visible" });
  }

  async returnBook() {
    await this.returnButton.click();
    await this.successMessage.waitFor({ state: "visible" });
  }

  async isBorrowButtonEnabled() {
    return await this.borrowButton.isEnabled();
  }

  async getSuccessMessage() {
    return await this.successMessage.textContent();
  }

  async getErrorMessage() {
    return await this.errorMessage.textContent();
  }
}

module.exports = BookDetailPage;
```

### 7.7 ใช้งาน Page Objects ใน Tests

```javascript
// tests/e2e/login.spec.js
const { test, expect } = require("@playwright/test");
const LoginPage = require("../../pages/LoginPage");
const HomePage = require("../../pages/HomePage");

test.describe("Login Functionality", () => {
  let loginPage;
  let homePage;

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    homePage = new HomePage(page);
    await loginPage.goto();
  });

  test("should login successfully with valid credentials", async ({ page }) => {
    await loginPage.login("testuser", "password123");

    await expect(page).toHaveURL("/dashboard");
    const welcomeText = await homePage.getWelcomeText();
    expect(welcomeText).toContain("Welcome, testuser");
  });

  test("should show error with invalid credentials", async ({ page }) => {
    await loginPage.login("wronguser", "wrongpass");

    expect(await loginPage.isErrorVisible()).toBe(true);
    const errorMessage = await loginPage.getErrorMessage();
    expect(errorMessage).toContain("Invalid credentials");
  });

  test("should navigate to forgot password page", async ({ page }) => {
    await loginPage.clickForgotPassword();

    await expect(page).toHaveURL("/forgot-password");
  });
});
```

```javascript
// tests/e2e/search.spec.js
const { test, expect } = require("@playwright/test");
const LoginPage = require("../../pages/LoginPage");
const HomePage = require("../../pages/HomePage");
const SearchPage = require("../../pages/SearchPage");
const BookDetailPage = require("../../pages/BookDetailPage");

test.describe("Book Search", () => {
  let loginPage;
  let homePage;
  let searchPage;
  let bookDetailPage;

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    homePage = new HomePage(page);
    searchPage = new SearchPage(page);
    bookDetailPage = new BookDetailPage(page);

    // Login before each test
    await loginPage.goto();
    await loginPage.login("testuser", "password123");
    await expect(page).toHaveURL("/dashboard");
  });

  test("should search for books by title", async ({ page }) => {
    await homePage.search("JavaScript");

    await expect(page).toHaveURL(/search/);
    const resultCount = await searchPage.getResultCount();
    expect(resultCount).toBeGreaterThan(0);

    const titles = await searchPage.getResultTitles();
    titles.forEach((title) => {
      expect(title.toLowerCase()).toContain("javascript");
    });
  });

  test("should show no results for invalid search", async ({ page }) => {
    await homePage.search("xyznonexistent123");

    expect(await searchPage.isNoResultsVisible()).toBe(true);
  });

  test("should filter search results by category", async ({ page }) => {
    await searchPage.goto();
    await searchPage.search("programming");
    await searchPage.filterByCategory("Web Development");

    const resultCount = await searchPage.getResultCount();
    expect(resultCount).toBeGreaterThan(0);
  });

  test("should navigate to book detail from search results", async ({
    page,
  }) => {
    await searchPage.goto();
    await searchPage.search("JavaScript");
    await searchPage.clickResult(0);

    await expect(page).toHaveURL(/\/books\/\d+/);
    const bookInfo = await bookDetailPage.getBookInfo();
    expect(bookInfo.title).toBeDefined();
  });
});
```

```javascript
// tests/e2e/borrow.spec.js
const { test, expect } = require("@playwright/test");
const LoginPage = require("../../pages/LoginPage");
const BookDetailPage = require("../../pages/BookDetailPage");

test.describe("Book Borrowing", () => {
  let loginPage;
  let bookDetailPage;

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    bookDetailPage = new BookDetailPage(page);

    await loginPage.goto();
    await loginPage.login("testuser", "password123");
  });

  test("should borrow available book successfully", async ({ page }) => {
    await bookDetailPage.goto(1); // Book ID 1

    const bookInfo = await bookDetailPage.getBookInfo();
    if (bookInfo.availableCopies > 0) {
      await bookDetailPage.borrowBook();

      const message = await bookDetailPage.getSuccessMessage();
      expect(message).toContain("borrowed successfully");
    }
  });

  test("should not allow borrowing when no copies available", async ({
    page,
  }) => {
    await bookDetailPage.goto(2); // Book with no available copies

    const isEnabled = await bookDetailPage.isBorrowButtonEnabled();
    expect(isEnabled).toBe(false);
  });

  test("complete user journey - search and borrow", async ({ page }) => {
    const HomePage = require("../../pages/HomePage");
    const SearchPage = require("../../pages/SearchPage");

    const homePage = new HomePage(page);
    const searchPage = new SearchPage(page);

    // Search for book
    await homePage.goto();
    await homePage.search("Clean Code");

    // Click first result
    await searchPage.clickResult(0);

    // Get book info
    const bookInfo = await bookDetailPage.getBookInfo();
    expect(bookInfo.title).toContain("Clean Code");

    // Borrow if available
    if (bookInfo.availableCopies > 0) {
      await bookDetailPage.borrowBook();
      const message = await bookDetailPage.getSuccessMessage();
      expect(message).toContain("borrowed successfully");
    }
  });
});
```

---

## 8. Cross-Browser Testing

### 8.1 การตั้งค่าใน Config

```javascript
// playwright.config.js
const { defineConfig, devices } = require("@playwright/test");

module.exports = defineConfig({
  projects: [
    // Desktop Browsers
    {
      name: "chromium",
      use: { ...devices["Desktop Chrome"] },
    },
    {
      name: "firefox",
      use: { ...devices["Desktop Firefox"] },
    },
    {
      name: "webkit",
      use: { ...devices["Desktop Safari"] },
    },

    // Mobile Browsers
    {
      name: "Mobile Chrome",
      use: { ...devices["Pixel 5"] },
    },
    {
      name: "Mobile Safari",
      use: { ...devices["iPhone 12"] },
    },
    {
      name: "Mobile Safari Landscape",
      use: { ...devices["iPhone 12 landscape"] },
    },

    // Tablets
    {
      name: "iPad",
      use: { ...devices["iPad (gen 7)"] },
    },
    {
      name: "iPad Pro",
      use: { ...devices["iPad Pro 11"] },
    },

    // Branded Browsers
    {
      name: "Microsoft Edge",
      use: { ...devices["Desktop Edge"], channel: "msedge" },
    },
    {
      name: "Google Chrome",
      use: { ...devices["Desktop Chrome"], channel: "chrome" },
    },
  ],
});
```

### 8.2 รัน Tests บน Browsers ที่เลือก

```bash
# รันทุก browsers
npx playwright test

# รันเฉพาะบาง browser
npx playwright test --project=chromium
npx playwright test --project=firefox
npx playwright test --project=webkit

# รันหลาย browsers
npx playwright test --project=chromium --project=firefox

# รัน mobile browsers
npx playwright test --project="Mobile Chrome" --project="Mobile Safari"
```

### 8.3 Browser-Specific Tests

```javascript
const { test, expect } = require("@playwright/test");

test("works on all browsers", async ({ page, browserName }) => {
  await page.goto("/");

  // Log browser name
  console.log(`Running on: ${browserName}`);

  // Skip test on specific browser
  test.skip(browserName === "webkit", "Feature not supported on Safari");

  // Browser-specific assertions
  if (browserName === "chromium") {
    // Chrome-specific test
    await expect(page.locator(".chrome-only-feature")).toBeVisible();
  }
});

test("responsive design test", async ({ page, isMobile }) => {
  await page.goto("/");

  if (isMobile) {
    // Mobile-specific assertions
    await expect(page.locator(".mobile-menu")).toBeVisible();
    await expect(page.locator(".desktop-sidebar")).toBeHidden();
  } else {
    // Desktop-specific assertions
    await expect(page.locator(".mobile-menu")).toBeHidden();
    await expect(page.locator(".desktop-sidebar")).toBeVisible();
  }
});
```

### 8.4 Custom Device Emulation

```javascript
const { test, expect } = require("@playwright/test");

test.use({
  viewport: { width: 1920, height: 1080 },
  deviceScaleFactor: 2,
  isMobile: false,
  hasTouch: false,
});

test("custom viewport test", async ({ page }) => {
  await page.goto("/");
  // Test with 1920x1080 resolution
});

// Or inline
test("specific device test", async ({ browser }) => {
  const context = await browser.newContext({
    viewport: { width: 375, height: 812 },
    userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X)",
    isMobile: true,
    hasTouch: true,
    deviceScaleFactor: 3,
  });

  const page = await context.newPage();
  await page.goto("/");

  await expect(page.locator(".mobile-view")).toBeVisible();

  await context.close();
});
```

### 8.5 ตัวอย่าง Cross-Browser Test Suite

```javascript
// tests/cross-browser/responsive.spec.js
const { test, expect } = require("@playwright/test");

test.describe("Responsive Design Tests", () => {
  test("navigation menu adapts to screen size", async ({ page, isMobile }) => {
    await page.goto("/");

    const hamburgerMenu = page.getByTestId("hamburger-menu");
    const desktopNav = page.getByTestId("desktop-nav");

    if (isMobile) {
      await expect(hamburgerMenu).toBeVisible();
      await expect(desktopNav).toBeHidden();

      // Test mobile menu functionality
      await hamburgerMenu.click();
      await expect(page.getByTestId("mobile-nav")).toBeVisible();
    } else {
      await expect(hamburgerMenu).toBeHidden();
      await expect(desktopNav).toBeVisible();
    }
  });

  test("images are responsive", async ({ page, isMobile }) => {
    await page.goto("/");

    const heroImage = page.locator(".hero-image");
    const box = await heroImage.boundingBox();

    if (isMobile) {
      expect(box.width).toBeLessThanOrEqual(375);
    } else {
      expect(box.width).toBeGreaterThan(600);
    }
  });

  test("form layout adapts correctly", async ({ page, isMobile }) => {
    await page.goto("/contact");

    const formContainer = page.locator(".form-container");
    const formFields = page.locator(".form-field");

    if (isMobile) {
      // On mobile, fields should stack vertically
      const fields = await formFields.all();
      for (let i = 0; i < fields.length - 1; i++) {
        const current = await fields[i].boundingBox();
        const next = await fields[i + 1].boundingBox();
        expect(next.y).toBeGreaterThan(current.y);
      }
    }
  });

  test("touch interactions work on mobile", async ({
    page,
    isMobile,
    hasTouch,
  }) => {
    test.skip(!hasTouch, "Touch not available");

    await page.goto("/");

    // Test swipe carousel
    const carousel = page.locator(".carousel");
    await carousel.evaluate((el) => {
      el.dispatchEvent(
        new TouchEvent("touchstart", {
          touches: [{ clientX: 300, clientY: 200 }],
        }),
      );
      el.dispatchEvent(
        new TouchEvent("touchmove", {
          touches: [{ clientX: 100, clientY: 200 }],
        }),
      );
      el.dispatchEvent(new TouchEvent("touchend"));
    });

    // Verify slide changed
    await expect(page.locator(".carousel-slide.active")).toHaveAttribute(
      "data-index",
      "1",
    );
  });
});
```

---

## 9. Visual Regression Testing

Visual Regression Testing ใช้ตรวจสอบว่า UI ไม่เปลี่ยนแปลงโดยไม่ตั้งใจ

### 9.1 Screenshot Comparison

```javascript
// tests/visual/homepage.spec.js
const { test, expect } = require("@playwright/test");

test.describe("Visual Regression Tests", () => {
  test("homepage visual test", async ({ page }) => {
    await page.goto("/");

    // Compare entire page
    await expect(page).toHaveScreenshot("homepage.png");
  });

  test("homepage with options", async ({ page }) => {
    await page.goto("/");

    await expect(page).toHaveScreenshot("homepage-full.png", {
      fullPage: true, // Capture full page
      maxDiffPixels: 100, // Allow up to 100 different pixels
      maxDiffPixelRatio: 0.01, // Or 1% difference
      threshold: 0.2, // Pixel comparison threshold (0-1)
      animations: "disabled", // Disable animations
    });
  });

  test("component visual test", async ({ page }) => {
    await page.goto("/");

    // Compare specific element
    const header = page.locator("header");
    await expect(header).toHaveScreenshot("header.png");

    const footer = page.locator("footer");
    await expect(footer).toHaveScreenshot("footer.png");

    const sidebar = page.locator(".sidebar");
    await expect(sidebar).toHaveScreenshot("sidebar.png");
  });

  test("login form visual test", async ({ page }) => {
    await page.goto("/login");

    const loginForm = page.locator(".login-form");
    await expect(loginForm).toHaveScreenshot("login-form.png");
  });
});
```

### 9.2 การจัดการ Baseline Screenshots

```bash
# สร้าง baseline screenshots ครั้งแรก
npx playwright test tests/visual/ --update-snapshots

# รัน visual tests (เปรียบเทียบกับ baseline)
npx playwright test tests/visual/

# Update specific snapshots
npx playwright test tests/visual/homepage.spec.js --update-snapshots
```

### 9.3 โครงสร้าง Snapshot Files

```
tests/
├── visual/
│   ├── homepage.spec.js
│   └── homepage.spec.js-snapshots/
│       ├── homepage-chromium-darwin.png
│       ├── homepage-chromium-linux.png
│       ├── homepage-firefox-darwin.png
│       ├── header-chromium-darwin.png
│       └── ...
```

### 9.4 Visual Tests สำหรับ Different States

```javascript
// tests/visual/components.spec.js
const { test, expect } = require("@playwright/test");

test.describe("Component Visual Tests", () => {
  test("button states", async ({ page }) => {
    await page.goto("/components/buttons");

    // Normal state
    const button = page.getByRole("button", { name: "Submit" });
    await expect(button).toHaveScreenshot("button-normal.png");

    // Hover state
    await button.hover();
    await expect(button).toHaveScreenshot("button-hover.png");

    // Focus state
    await button.focus();
    await expect(button).toHaveScreenshot("button-focus.png");

    // Disabled state
    const disabledButton = page.getByRole("button", { name: "Disabled" });
    await expect(disabledButton).toHaveScreenshot("button-disabled.png");
  });

  test("form validation states", async ({ page }) => {
    await page.goto("/contact");

    const emailInput = page.getByLabel("Email");

    // Empty state
    await expect(emailInput).toHaveScreenshot("input-empty.png");

    // Filled state
    await emailInput.fill("test@example.com");
    await expect(emailInput).toHaveScreenshot("input-filled.png");

    // Error state
    await emailInput.fill("invalid-email");
    await page.getByRole("button", { name: "Submit" }).click();
    await expect(emailInput).toHaveScreenshot("input-error.png");
  });

  test("modal dialog", async ({ page }) => {
    await page.goto("/");

    await page.getByRole("button", { name: "Open Modal" }).click();

    const modal = page.locator(".modal");
    await expect(modal).toHaveScreenshot("modal-open.png");
  });

  test("dark mode", async ({ page }) => {
    await page.goto("/");

    // Light mode
    await expect(page).toHaveScreenshot("homepage-light.png");

    // Toggle to dark mode
    await page.getByTestId("theme-toggle").click();

    // Dark mode
    await expect(page).toHaveScreenshot("homepage-dark.png");
  });
});
```

### 9.5 Responsive Visual Tests

```javascript
// tests/visual/responsive.spec.js
const { test, expect } = require("@playwright/test");

const viewports = [
  { name: "mobile", width: 375, height: 667 },
  { name: "tablet", width: 768, height: 1024 },
  { name: "desktop", width: 1280, height: 800 },
  { name: "wide", width: 1920, height: 1080 },
];

test.describe("Responsive Visual Tests", () => {
  for (const { name, width, height } of viewports) {
    test(`homepage at ${name} (${width}x${height})`, async ({ page }) => {
      await page.setViewportSize({ width, height });
      await page.goto("/");

      await expect(page).toHaveScreenshot(`homepage-${name}.png`, {
        fullPage: true,
      });
    });
  }
});
```

### 9.6 Masking Dynamic Content

```javascript
test("visual test with masking", async ({ page }) => {
  await page.goto("/dashboard");

  // Mask dynamic elements
  await expect(page).toHaveScreenshot("dashboard.png", {
    mask: [
      page.locator(".timestamp"), // Dynamic timestamps
      page.locator(".user-avatar"), // User avatars
      page.locator(".notification-count"), // Notification counts
      page.locator(".chart"), // Dynamic charts
    ],
    maskColor: "#FF00FF", // Color to use for masks
  });
});
```

### 9.7 Handling Animations

```javascript
test("visual test without animations", async ({ page }) => {
  await page.goto("/");

  // Method 1: Disable via screenshot option
  await expect(page).toHaveScreenshot("page.png", {
    animations: "disabled",
  });

  // Method 2: Disable via CSS
  await page.addStyleTag({
    content: `
      *, *::before, *::after {
        animation-duration: 0s !important;
        animation-delay: 0s !important;
        transition-duration: 0s !important;
        transition-delay: 0s !important;
      }
    `,
  });

  await expect(page).toHaveScreenshot("page-no-animations.png");
});
```

---

## 10. การจัดการ Test Data และ Fixtures

### 10.1 Playwright Fixtures

Fixtures เป็นวิธีการจัดการ setup/teardown และ share data ระหว่าง tests

```javascript
// fixtures/test-fixtures.js
const { test: base } = require("@playwright/test");
const LoginPage = require("../pages/LoginPage");
const HomePage = require("../pages/HomePage");

// Extend base test with custom fixtures
const test = base.extend({
  // Page Object fixtures
  loginPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page);
    await use(loginPage);
  },

  homePage: async ({ page }, use) => {
    const homePage = new HomePage(page);
    await use(homePage);
  },

  // Authenticated user fixture
  authenticatedPage: async ({ page }, use) => {
    // Login
    await page.goto("/login");
    await page.fill("#username", "testuser");
    await page.fill("#password", "password123");
    await page.click("#login-button");
    await page.waitForURL("/dashboard");

    // Use authenticated page
    await use(page);

    // Cleanup - logout
    await page.goto("/logout");
  },

  // Test data fixture
  testUser: async ({}, use) => {
    const user = {
      username: "testuser",
      password: "password123",
      email: "test@example.com",
      firstName: "Test",
      lastName: "User",
    };
    await use(user);
  },

  // Database cleanup fixture
  cleanDatabase: async ({}, use) => {
    // Setup - nothing needed before test
    await use();

    // Teardown - cleanup after test
    // await db.query('DELETE FROM test_data');
    console.log("Database cleaned up");
  },
});

module.exports = { test };
```

### 10.2 ใช้งาน Fixtures

```javascript
// tests/with-fixtures.spec.js
const { test } = require("../fixtures/test-fixtures");
const { expect } = require("@playwright/test");

test.describe("Tests with Fixtures", () => {
  test("login test with fixtures", async ({ loginPage, testUser }) => {
    await loginPage.goto();
    await loginPage.login(testUser.username, testUser.password);

    // Page objects and test data are injected
  });

  test("authenticated test", async ({ authenticatedPage }) => {
    // Already logged in
    await expect(authenticatedPage).toHaveURL("/dashboard");
  });

  test("test with cleanup", async ({ page, cleanDatabase }) => {
    // Test runs here
    await page.goto("/create-data");

    // cleanDatabase teardown runs after test
  });
});
```

### 10.3 Test Data Management

```javascript
// fixtures/test-data.js
const { faker } = require("@faker-js/faker");

// Static test data
const staticUsers = {
  admin: {
    username: "admin",
    password: "admin123",
    role: "admin",
  },
  member: {
    username: "member",
    password: "member123",
    role: "member",
  },
  guest: {
    username: "guest",
    password: "guest123",
    role: "guest",
  },
};

const staticBooks = [
  {
    id: 1,
    title: "JavaScript: The Good Parts",
    author: "Douglas Crockford",
    isbn: "978-0596517748",
  },
  {
    id: 2,
    title: "Clean Code",
    author: "Robert C. Martin",
    isbn: "978-0132350884",
  },
  {
    id: 3,
    title: "Design Patterns",
    author: "Gang of Four",
    isbn: "978-0201633610",
  },
];

// Dynamic test data generators
function generateUser() {
  return {
    username: faker.internet.userName(),
    email: faker.internet.email(),
    password: faker.internet.password(),
    firstName: faker.person.firstName(),
    lastName: faker.person.lastName(),
  };
}

function generateBook() {
  return {
    title: faker.lorem.words(3),
    author: faker.person.fullName(),
    isbn: faker.string.numeric(13),
    publishedYear: faker.date.past({ years: 50 }).getFullYear(),
    description: faker.lorem.paragraph(),
  };
}

function generateBooks(count) {
  return Array.from({ length: count }, generateBook);
}

module.exports = {
  staticUsers,
  staticBooks,
  generateUser,
  generateBook,
  generateBooks,
};
```

### 10.4 ใช้ Test Data ใน Tests

```javascript
// tests/with-test-data.spec.js
const { test, expect } = require("@playwright/test");
const {
  staticUsers,
  generateUser,
  generateBook,
} = require("../fixtures/test-data");

test.describe("Tests with Test Data", () => {
  test("login as admin", async ({ page }) => {
    const { admin } = staticUsers;

    await page.goto("/login");
    await page.fill("#username", admin.username);
    await page.fill("#password", admin.password);
    await page.click("#login-button");

    await expect(page.locator(".admin-panel")).toBeVisible();
  });

  test("register new user with random data", async ({ page }) => {
    const newUser = generateUser();

    await page.goto("/register");
    await page.fill("#username", newUser.username);
    await page.fill("#email", newUser.email);
    await page.fill("#password", newUser.password);
    await page.fill("#firstName", newUser.firstName);
    await page.fill("#lastName", newUser.lastName);
    await page.click("#register-button");

    await expect(page).toHaveURL("/welcome");
  });

  test("add new book", async ({ page }) => {
    const newBook = generateBook();

    await page.goto("/admin/books/new");
    await page.fill("#title", newBook.title);
    await page.fill("#author", newBook.author);
    await page.fill("#isbn", newBook.isbn);
    await page.fill("#year", newBook.publishedYear.toString());
    await page.click("#save-button");

    await expect(page.locator(".success-message")).toContainText("Book added");
  });
});
```

### 10.5 API สำหรับ Test Setup

```javascript
// fixtures/api-helpers.js
async function createTestUser(request) {
  const user = {
    username: `test_${Date.now()}`,
    email: `test_${Date.now()}@example.com`,
    password: "testpass123",
  };

  const response = await request.post("/api/users", {
    data: user,
  });

  return { ...user, id: (await response.json()).id };
}

async function createTestBook(request, book) {
  const response = await request.post("/api/books", {
    data: book,
  });

  return await response.json();
}

async function deleteTestUser(request, userId) {
  await request.delete(`/api/users/${userId}`);
}

async function deleteTestBook(request, bookId) {
  await request.delete(`/api/books/${bookId}`);
}

module.exports = {
  createTestUser,
  createTestBook,
  deleteTestUser,
  deleteTestBook,
};
```

```javascript
// tests/with-api-setup.spec.js
const { test, expect } = require("@playwright/test");
const {
  createTestUser,
  createTestBook,
  deleteTestUser,
  deleteTestBook,
} = require("../fixtures/api-helpers");

test.describe("Tests with API Setup", () => {
  let testUser;
  let testBook;

  test.beforeAll(async ({ request }) => {
    // Create test data via API
    testUser = await createTestUser(request);
    testBook = await createTestBook(request, {
      title: "Test Book",
      author: "Test Author",
      isbn: "1234567890123",
    });
  });

  test.afterAll(async ({ request }) => {
    // Cleanup via API
    await deleteTestUser(request, testUser.id);
    await deleteTestBook(request, testBook.id);
  });

  test("user can borrow book", async ({ page }) => {
    // Login with test user
    await page.goto("/login");
    await page.fill("#username", testUser.username);
    await page.fill("#password", testUser.password);
    await page.click("#login-button");

    // Navigate to test book
    await page.goto(`/books/${testBook.id}`);

    // Borrow
    await page.click("#borrow-button");

    await expect(page.locator(".success-message")).toBeVisible();
  });
});
```

---

## 11. Advanced Features

### 11.1 Network Interception (ดักจับและจำลอง Network Requests)

**ทำไมต้อง Mock Network?**

- ไม่ต้องกังวล API จริงจะล่มหรือช้า Test จึงเสถียร (Stable)
- ทดสอบกรณี Error ที่ยากจะเกิดในความเป็นจริง (เช่น Server 500)
- ไม่ต้องรอตอบกลับ API จึงเร็ว ✅

#### กรณีที่ 1: Server Maintenance (500 Error)

สมมติระบบหลังบ้านล่ม นักศึกษาต้องเห็นว่าหน้าเว็บแสดง Error Message อย่างไร

```javascript
const { test, expect } = require("@playwright/test");

test("แสดงข้อความ Error เมื่อ Server ล่ม (500 Error)", async ({ page }) => {
  // 1. ดักจับ API requests ที่ไปยัง /api/books
  await page.route("**/api/books", (route) => {
    // 2. แทนการส่ง request ไปจริง เราส่ง Response ปลอม
    route.fulfill({
      status: 500,
      contentType: "application/json",
      body: JSON.stringify({
        error: "Internal Server Error",
        message: "Database connection failed",
      }),
    });
  });

  // 3. ไปยังหน้า Library
  await page.goto("/library");

  // 4. ตรวจสอบว่ามี Error message ปรากฏ
  const errorAlert = page.locator(".error-alert");
  await expect(errorAlert).toBeVisible();
  await expect(errorAlert).toContainText("Unable to load books");

  // 5. ตรวจสอบว่าปุ่ม Retry ปรากฏ
  const retryButton = page.getByRole("button", { name: "Retry" });
  await expect(retryButton).toBeEnabled();
});
```

#### กรณีที่ 2: Network Slow (Timeout Scenario)

การเชื่อมต่อช้า หรือ API ตอบกลับนาน ต้องให้ Loading Spinner ทำงาน

```javascript
test("แสดง Loading Spinner เมื่อ API ตอบกลับช้า (3 วินาที)", async ({
  page,
}) => {
  // 1. ดักจับ API และหน่วงเวลา 3 วินาที
  await page.route("**/api/books", async (route) => {
    // ระหว่างรอ API ตอบกลับ
    await new Promise((resolve) => setTimeout(resolve, 3000));

    // หลังจาก 3 วินาที ก็ส่ง Response กลับ
    route.fulfill({
      status: 200,
      contentType: "application/json",
      body: JSON.stringify([
        { id: 1, title: "JavaScript Basics" },
        { id: 2, title: "Web Development" },
      ]),
    });
  });

  // 2. ไปยังหน้า Library
  await page.goto("/library");

  // 3. ตรวจสอบว่า Loading Spinner ปรากฏทันที
  const spinner = page.locator(".loading-spinner");
  await expect(spinner).toBeVisible();

  // 4. รอให้ Spinner หายไป (ก่อนหรือพอดี 3 วินาที)
  await expect(spinner).toBeHidden({ timeout: 5000 });

  // 5. ตรวจสอบว่าข้อมูลหนังสือปรากฏ
  await expect(page.locator(".book-item")).toHaveCount(2);
  await expect(page.locator(".book-item").first()).toContainText(
    "JavaScript Basics",
  );
});
```

#### กรณีที่ 3: Partial Data (Incomplete Response)

บางครั้งระบบอาจส่งข้อมูลขาดส่วนไป (เช่นไม่มี Email Address) ต้องให้ App ไม่พัง

```javascript
test("ควรจัดการกับ Incomplete Data ได้อย่างปลอดภัย", async ({ page }) => {
  // 1. ดักจับ API และส่ง Data ขาดส่วน (ไม่มี author)
  await page.route("**/api/books", (route) => {
    route.fulfill({
      status: 200,
      contentType: "application/json",
      body: JSON.stringify([
        {
          id: 1,
          title: "Book 1",
          // ❌ ไม่มี author!
        },
        {
          id: 2,
          title: "Book 2",
          author: "Jane Doe", // ✅ มี author
        },
      ]),
    });
  });

  await page.goto("/library");

  // 2. ตรวจสอบว่า App ไม่พัง ยังแสดง Books ได้
  const bookItems = page.locator(".book-item");
  await expect(bookItems).toHaveCount(2);

  // 3. ตรวจสอบว่าจัดการ Missing Data ได้ (เช่น แสดง "Unknown Author")
  const firstBook = bookItems.first();
  await expect(firstBook).toContainText("Book 1");
  await expect(firstBook.locator(".author")).toContainText(
    /Unknown Author|N\/A/,
  );
});
```

#### กรณีที่ 4: Network Throttling (3G Connection Simulation)

```javascript
test("ควรทำงานได้บน 3G Connection ที่ช้า", async ({ page }) => {
  // 1. ดักจับ API และหน่วงเวลาให้ยาวขึ้น (5 วินาที)
  await page.route("**/api/books", async (route) => {
    await new Promise((resolve) => setTimeout(resolve, 5000)); // 3G ช้า
    await route.continue();
  });

  const startTime = Date.now();
  await page.goto("/library");
  const loadTime = Date.now() - startTime;

  // 2. ตรวจสอบว่า App ไม่ timeout
  await expect(page.locator(".book-item").first()).toBeVisible({
    timeout: 10000,
  });

  // 3. Log เวลา (สำหรับ Performance Monitoring)
  console.log(`Page loaded in ${loadTime}ms over 3G`);
});
```

#### กรณีที่ 5: Network Request Interception (Logging ทุก Requests)

```javascript
test("โปรแกรมควรทำ API calls ถูกต้อง", async ({ page }) => {
  const requestsMade = [];

  // 1. Intercept ทั้งหมด **/api/** requests
  await page.route("**/api/**", (route) => {
    const request = route.request();

    // Log request details
    requestsMade.push({
      method: request.method(),
      url: request.url(),
      timestamp: new Date().toISOString(),
    });

    // ส่ง request ไปจริง
    route.continue();
  });

  await page.goto("/library");
  await page.click("#search-button");

  // 2. ตรวจสอบว่า Backend ถูกเรียกในลำดับที่ถูก
  expect(requestsMade).toContainEqual(
    expect.objectContaining({
      method: "GET",
      url: expect.stringContaining("/api/books"),
    }),
  );

  console.log("API calls made:", requestsMade);
});
```

### 11.2 Authentication State

```javascript
// global-setup.js
const { chromium } = require("@playwright/test");

async function globalSetup() {
  const browser = await chromium.launch();
  const page = await browser.newPage();

  // Login
  await page.goto("http://localhost:3000/login");
  await page.fill("#username", "testuser");
  await page.fill("#password", "password123");
  await page.click("#login-button");
  await page.waitForURL("/dashboard");

  // Save authentication state
  await page.context().storageState({ path: "playwright/.auth/user.json" });

  await browser.close();
}

module.exports = globalSetup;
```

```javascript
// playwright.config.js
module.exports = defineConfig({
  globalSetup: require.resolve("./global-setup"),

  projects: [
    // Unauthenticated tests
    {
      name: "unauthenticated",
      testMatch: /.*\.unauth\.spec\.js/,
    },
    // Authenticated tests
    {
      name: "authenticated",
      testMatch: /.*\.auth\.spec\.js/,
      use: {
        storageState: "playwright/.auth/user.json",
      },
    },
  ],
});
```

### 11.3 Multiple Contexts

```javascript
test("test with multiple users", async ({ browser }) => {
  // Create two independent contexts
  const userAContext = await browser.newContext();
  const userBContext = await browser.newContext();

  const userAPage = await userAContext.newPage();
  const userBPage = await userBContext.newPage();

  // Login as User A
  await userAPage.goto("/login");
  await userAPage.fill("#username", "userA");
  await userAPage.fill("#password", "passA");
  await userAPage.click("#login-button");

  // Login as User B
  await userBPage.goto("/login");
  await userBPage.fill("#username", "userB");
  await userBPage.fill("#password", "passB");
  await userBPage.click("#login-button");

  // User A sends message
  await userAPage.goto("/chat");
  await userAPage.fill("#message", "Hello from A");
  await userAPage.click("#send");

  // User B sees message
  await userBPage.goto("/chat");
  await expect(userBPage.locator(".message").last()).toContainText(
    "Hello from A",
  );

  await userAContext.close();
  await userBContext.close();
});
```

### 11.4 File Download

```javascript
test("download file", async ({ page }) => {
  // Wait for download to start
  const downloadPromise = page.waitForEvent("download");
  await page.click("#download-button");
  const download = await downloadPromise;

  // Get download info
  console.log("Filename:", download.suggestedFilename());

  // Save to specific path
  await download.saveAs("downloads/" + download.suggestedFilename());

  // Get download path
  const path = await download.path();
  console.log("Saved to:", path);
});
```

### 11.5 Video Recording

```javascript
// playwright.config.js
module.exports = defineConfig({
  use: {
    video: "on", // 'on', 'off', 'retain-on-failure', 'on-first-retry'
  },
});
```

```javascript
test("test with video", async ({ page }, testInfo) => {
  await page.goto("/");
  await page.click("#some-button");

  // Access video after test
  const video = page.video();
  if (video) {
    const path = await video.path();
    console.log("Video saved at:", path);
  }
});
```

### 11.6 Emulate Geolocation

```javascript
test.use({
  geolocation: { latitude: 13.7563, longitude: 100.5018 }, // Bangkok
  permissions: ["geolocation"],
});

test("geolocation test", async ({ page }) => {
  await page.goto("/nearby");

  await expect(page.locator(".location")).toContainText("Bangkok");
});
```

### 11.7 Emulate Timezone and Locale

```javascript
test.use({
  locale: "th-TH",
  timezoneId: "Asia/Bangkok",
});

test("locale and timezone test", async ({ page }) => {
  await page.goto("/");

  // Date should be formatted in Thai
  await expect(page.locator(".date")).toContainText("มกราคม");
});
```

---

## 12.9 CI/CD Integration (GitHub Actions)

### ทำไมต้อง CI/CD สำหรับ E2E Tests?

- **Automated Testing** - ไม่ต้องรันด้วยมือ ประหยัดเวลา
- **Early Feedback** - ทันทีที่ Push Code ก็รู้ว่า Test ผ่านหรือไม่
- **Parallel Execution** - รัน Tests หลายตัวพร้อมกัน เร็วขึ้น
- **Artifacts** - เก็บ Report, Screenshots, Videos สำหรับ Debug

### GitHub Actions Setup

**ไฟล์: `.github/workflows/playwright.yml**`

```yaml
name: Playwright Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest

    # สามารถเลือก Node version ได้
    strategy:
      matrix:
        node-version: [18.x, 20.x]

    steps:
      # 1. Checkout code
      - uses: actions/checkout@v3

      # 2. ติดตั้ง Node.js
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}

      # 3. ติดตั้ง dependencies
      - name: Install dependencies
        run: npm ci

      # 4. ติดตั้ง Playwright browsers
      - name: Install Playwright Browsers
        run: npx playwright install --with-deps

      # 5. รัน Playwright Tests
      - name: Run Playwright tests
        run: npm run test
        # หรือ: npx playwright test --project=chromium

      # 6. Upload reports (เมื่อ Tests fail)
      - name: Upload Playwright Report
        uses: actions/upload-artifact@v3
        if: always()
        with:
          name: playwright-report-${{ matrix.node-version }}
          path: playwright-report/
          retention-days: 30

      # 7. Upload trace files (สำหรับ debug)
      - name: Upload trace
        uses: actions/upload-artifact@v3
        if: always()
        with:
          name: trace-${{ matrix.node-version }}
          path: test-results/
          retention-days: 7
```

### Parallel Execution สำหรับเร็วขึ้น

**ไฟล์: `playwright.config.js**`

```javascript
module.exports = defineConfig({
  // รัน tests แบบ parallel (ค่าเริ่มต้น)
  fullyParallel: true,

  // จำนวน workers (parallel processes)
  workers: process.env.CI ? 2 : 4,

  // หรือใช้เปอร์เซ็นต์ของ CPU
  // workers: '75%',

  // Retry policy สำหรับ CI
  retries: process.env.CI ? 2 : 0,

  // Timeout สำหรับ CI environments
  timeout: 30000, // 30 seconds

  expect: {
    timeout: 5000, // 5 seconds สำหรับ expect
  },
});
```

### Example Report

จากการรัน Tests บน GitHub Actions จะได้:

```
✓ tests/auth/login.spec.js (3 tests)
✓ tests/books/search.spec.js (5 tests)
✓ tests/books/borrow.spec.js (4 tests)
✓ tests/user/profile.spec.js (2 tests)

Total: 14 tests, 14 passed in 45 seconds (Parallel mode)
```

### Notification สำหรับ CI Failures

เพิ่มสำหรับส่ง Email หรือ Slack notification เมื่อ Tests fail:

```yaml
# ใน .github/workflows/playwright.yml
- name: Send Slack notification on failure
  if: failure()
  uses: slackapi/slack-github-action@v1
  with:
    payload: |
      {
        "text": "Playwright Tests Failed! 🔴",
        "blocks": [
          {
            "type": "section",
            "text": {
              "type": "mrkdwn",
              "text": "Repository: ${{ github.repository }}\nBranch: ${{ github.ref }}\nCommit: ${{ github.sha }}"
            }
          }
        ]
      }
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
```

---

## 13. Debugging และ Reporting (ขยายรายละเอียด)

```bash
# Run in debug mode
npx playwright test --debug

# Debug specific test
npx playwright test tests/login.spec.js --debug
```

### 12.2 Playwright Inspector

```javascript
test("debug with inspector", async ({ page }) => {
  await page.goto("/");

  // Pause here and open inspector
  await page.pause();

  await page.click("#button");
});
```

### 12.3 Console Logs

```javascript
test("with console logs", async ({ page }) => {
  // Listen to console messages
  page.on("console", (msg) => {
    console.log(`Browser console [${msg.type()}]: ${msg.text()}`);
  });

  // Listen to page errors
  page.on("pageerror", (error) => {
    console.log("Page error:", error.message);
  });

  await page.goto("/");
});
```

### 12.4 Tracing

```javascript
// Enable trace in config
// trace: 'on-first-retry'

// Or manually
test("with trace", async ({ page, context }) => {
  await context.tracing.start({ screenshots: true, snapshots: true });

  await page.goto("/");
  await page.click("#button");

  await context.tracing.stop({ path: "trace.zip" });
});
```

```bash
# View trace
npx playwright show-trace trace.zip
```

### 12.5 Screenshots on Failure

```javascript
test.afterEach(async ({ page }, testInfo) => {
  if (testInfo.status !== "passed") {
    const screenshotPath = `screenshots/${testInfo.title.replace(/\s+/g, "-")}.png`;
    await page.screenshot({ path: screenshotPath, fullPage: true });
    testInfo.attachments.push({
      name: "screenshot",
      path: screenshotPath,
      contentType: "image/png",
    });
  }
});
```

### 12.6 HTML Reporter

```bash
# Generate report
npx playwright test

# View report
npx playwright show-report
```

### 12.7 Custom Reporter

```javascript
// playwright.config.js
module.exports = defineConfig({
  reporter: [
    ["html", { outputFolder: "playwright-report" }],
    ["json", { outputFile: "test-results.json" }],
    ["junit", { outputFile: "junit-results.xml" }],
    ["list"],
  ],
});
```

### 12.8 Test Artifacts

```javascript
test("attach artifacts", async ({ page }, testInfo) => {
  await page.goto("/");

  // Attach screenshot
  const screenshot = await page.screenshot();
  await testInfo.attach("screenshot", {
    body: screenshot,
    contentType: "image/png",
  });

  // Attach text
  await testInfo.attach("log", {
    body: "Test log content",
    contentType: "text/plain",
  });

  // Attach JSON
  const data = { key: "value" };
  await testInfo.attach("data", {
    body: JSON.stringify(data),
    contentType: "application/json",
  });
});
```

## 13. Debugging และ Reporting (ขยายรายละเอียด)

### 13.1 Debug Mode - ขั้นตอนโดยขั้นตอน

**Debug Mode ช่วยให้คุณ:**

- ⏸️ หยุด Execution ที่จุดที่กำหนด
- 🔍 Inspect Elements บนหน้าเว็บ
- 📝 ดู Locators ว่าตรงหรือไม่
- 🎬 Step Through Code บรรทัดต่อบรรทัด

```bash
# เปิด Debug Mode
npx playwright test --debug

# รัน Debug Mode บน Specific File
npx playwright test tests/login.spec.js --debug

# Debug ใน headed mode (เห็นหน้า Browser)
npx playwright test --debug --headed
```

**ใน Debug Mode:**

1. Playwright Inspector จะเปิดขึ้น (ด้านขวา)
2. Browser จะแสดงที่หน้าแรกของ Test
3. ปุ่ม "Step Over" (►) เพื่อรันทีละ step
4. ปุ่ม "Resume" (▶) เพื่อ Resume until next breakpoint

```javascript
test("debug example with pause", async ({ page }) => {
  await page.goto("/");

  // ⏸️ หยุดตรงนี้ เพื่อดู State ปัจจุบัน
  await page.pause();

  await page.click("#search-button");

  // 🔍 ลองค้นหา Locators ใน Console ของ Inspector
  // ตัวอย่าง: page.getByRole('heading')
});
```

### 13.2 Playwright Inspector

Playwright Inspector เป็น DevTools พิเศษสำหรับ E2E Testing:

- **Source Panel** - ดูโค้ด Test ปัจจุบัน
- **Actions Tab** - ดูทุก Actions ที่ทำ
- **Locators Panel** - ทดลองเขียน Locators (Test ก่อนใช้จริง!)
- **Browser** - DevTools ของ Browser เพื่อ Debug UI

```javascript
test("use inspector to find locators", async ({ page }) => {
  await page.goto("/library");

  // 1. เปิด Inspector
  await page.pause();

  // 2. ใน Inspector → Locators Panel ให้ลองเขียน:
  //    page.getByRole('button', { name: 'Search' })
  //    ถ้าปรากฏ Element ก็ใช้ได้!

  // 3. ถ้า Locators ไม่ถูก Inspector จะบอก "No matches"
});
```

### 13.3 Console Logs - เก็บข้อมูล Debug

```javascript
test("with detailed console logs", async ({ page }) => {
  // 1. Listen to Browser Console (JavaScript console.log)
  page.on("console", (msg) => {
    console.log(
      `Browser [${msg.type().toUpperCase()}] ${msg.location().url}:${msg.location().lineNumber}`,
    );
    console.log(`  → ${msg.text()}`);
  });

  // 2. Listen to Page Errors (JavaScript errors)
  page.on("pageerror", (error) => {
    console.error("🔴 Page Error:", error.message);
    console.error("   Stack:", error.stack);
  });

  // 3. Listen to Request Failures
  page.on("requestfailed", (request) => {
    console.error(`❌ Request Failed: ${request.method()} ${request.url()}`);
    console.error(`   Failure: ${request.failure()?.errorText}`);
  });

  // 4. Listen to Dialog (Alert, Confirm, Prompt)
  page.on("dialog", (dialog) => {
    console.log(`Dialog [${dialog.type()}]: ${dialog.message()}`);
  });

  await page.goto("/");
  // ถ้าหน้าเว็บมี Error ก็จะแสดงในผลลัพธ์
});
```

### 13.4 Trace Recording - Replay Test Execution

Trace คือ "บันทึกวิดีโอ" ของ Test ทั้งหมด สามารถเล่นซ้ำและตรวจสอบทีหลังได้

```javascript
// playwright.config.js
module.exports = defineConfig({
  use: {
    // หรือ: 'off', 'on', 'on-first-retry', 'retain-on-failure'
    trace: "on-first-retry", // บันทึก trace เมื่อ test retry
  },
});
```

```javascript
// หรือ Manual Tracing
test("with manual trace", async ({ page, context }, testInfo) => {
  // เริ่มบันทึก trace
  await context.tracing.start({
    screenshots: true, // เก็บ screenshots
    snapshots: true, // เก็บ DOM snapshots
    sources: true, // เก็บ source code
  });

  await page.goto("/");
  await page.click("#search-button");
  await page.fill("#search-input", "javascript");

  // หยุดบันทึกและเก็บเป็น .zip
  const traceFileName = `trace-${testInfo.title}.zip`;
  await context.tracing.stop({
    path: `traces/${traceFileName}`,
  });
});
```

```bash
# ดูผลลัพธ์ Trace
npx playwright show-trace traces/trace-*.zip

# จะเปิด UI ให้ดู:
# - Timeline ของทุก actions
# - Screenshot ของแต่ละ step
# - Network requests
# - Console logs
```

### 13.5 Screenshots & Videos - Visual Artifacts

```javascript
// playwright.config.js
module.exports = defineConfig({
  use: {
    // Screenshots: 'off', 'only-on-failure'
    screenshot: "only-on-failure",

    // Videos: 'off', 'on', 'retain-on-failure'
    video: "retain-on-failure",

    // Screenshot on each action (slow!)
    // screenshot: 'on',
  },
});
```

```javascript
test("with manual screenshot", async ({ page }, testInfo) => {
  await page.goto("/");
  await page.click("#search-button");

  // ถ่ายรูป Success State
  const screenshot = await page.screenshot();
  await testInfo.attach("success-state", {
    body: screenshot,
    contentType: "image/png",
  });

  // ถ่ายรูป Specific Element
  const element = page.locator(".hero-section");
  const elementScreenshot = await element.screenshot();
  await testInfo.attach("hero-section", {
    body: elementScreenshot,
    contentType: "image/png",
  });

  // ถ่ายรูป Full Page
  const fullPage = await page.screenshot({ fullPage: true });
  await testInfo.attach("full-page", {
    body: fullPage,
    contentType: "image/png",
  });
});
```

### 13.6 HTML Report - ดูผลการทดสอบแบบ Visual

```bash
# หลังรัน tests จะ generate report
npx playwright test

# ดู Report
npx playwright show-report

# Report จะแสดง:
# - Test Status (Pass/Fail)
# - Duration ของแต่ละ test
# - Screenshots/Videos ถ้า fail
# - Error messages และ stack traces
```

### 13.7 Custom Reporter - สร้าง Report เอง

```javascript
// reporters/custom-reporter.js
class CustomReporter {
  onBegin(config, suite) {
    console.log(`🧪 Starting test run...`);
  }

  onTestBegin(test) {
    console.log(`⏳ Running: ${test.title}`);
  }

  onTestEnd(test, result) {
    if (result.status === "passed") {
      console.log(`✅ PASSED: ${test.title} (${result.duration}ms)`);
    } else if (result.status === "failed") {
      console.log(`❌ FAILED: ${test.title}`);
      console.log(`   Error: ${result.error?.message}`);
    } else if (result.status === "skipped") {
      console.log(`⊘ SKIPPED: ${test.title}`);
    }
  }

  onEnd(result) {
    console.log(`\n📊 Test Summary:`);
    console.log(`   Total: ${result.stats.expected}`);
    console.log(`   Passed: ${result.stats.expected - result.stats.failures}`);
    console.log(`   Failed: ${result.stats.failures}`);
    console.log(`   Duration: ${Math.round(result.startTime / 1000)}s`);
  }
}

module.exports = CustomReporter;
```

```javascript
// playwright.config.js
module.exports = defineConfig({
  reporter: [
    ["html"], // Built-in HTML reporter
    ["json", { outputFile: "test-results.json" }],
    ["./reporters/custom-reporter.js"], // Custom reporter
  ],
});
```

### 13.8 Flaky Test Management - จัดการ Tests ที่ไม่เสถียร

บางครั้ง Test fail โดยไม่มีเหตุผล (เช่น Network lag) วิธีแก้:

```javascript
test.describe("Flaky Tests", () => {
  // Method 1: Retry tests
  test.describe.configure({ retries: 2 });

  test("unreliable test", async ({ page }) => {
    await page.goto("/api-endpoint");
    // ถ้า fail ครั้งแรก จะ retry สูงสุด 2 ครั้ง
  });

  // Method 2: Timeout extension
  test("slow test", async ({ page }) => {
    test.setTimeout(60000); // 60 seconds instead of 30
    await page.goto("/slow-page");
  });

  // Method 3: Skip บน Specific browser
  test("skip on webkit", async ({ page, browserName }) => {
    test.skip(
      browserName === "webkit",
      "Not working on Safari due to webkit bug",
    );
    // ...
  });

  // Method 4: Conditional skip
  test("platform specific", async ({ page, platformName }) => {
    test.skip(platformName === "win32", "Known issue on Windows");
    // ...
  });
});
```

### 13.9 Test Summary

ผลลัพธ์ของการรัน Tests:

```
============================= test session starts ==============================
collected 25 items

tests/e2e/auth/login.spec.js::should login successfully PASSED          [ 4%]
tests/e2e/auth/login.spec.js::should show error for wrong password PASSED  [ 8%]
tests/e2e/books/search.spec.js::should search for books PASSED             [12%]
tests/e2e/books/search.spec.js::should filter results PASSED               [16%]

========================== 25 passed in 1m45s ===================================

✨ Test run finished successfully!
Reports are available at: file:///path/to/playwright-report/index.html
```

---

## 14. Best Practices และ Tips

```javascript
// ✅ Good - ใช้ role-based locators
await page.getByRole("button", { name: "Submit" }).click();
await page.getByLabel("Email").fill("test@example.com");
await page.getByTestId("user-card").click();

// ❌ Bad - ใช้ CSS selectors ที่เปลี่ยนง่าย
await page.locator(".btn.btn-primary.submit-btn").click();
await page.locator("div > form > input:nth-child(2)").fill("test@example.com");
await page.locator("#id-12345").click();
```

### 13.2 Wait Best Practices

```javascript
// ✅ Good - ใช้ auto-wait ของ Playwright
await page.click("#button"); // Auto-waits for element
await expect(page.locator(".result")).toBeVisible(); // Auto-retries

// ❌ Bad - ใช้ fixed waits
await page.waitForTimeout(5000); // Avoid!
await page.click("#button");
```

### 13.3 Test Independence

```javascript
// ✅ Good - แต่ละ test เป็นอิสระ
test.describe("User Management", () => {
  test.beforeEach(async ({ page }) => {
    // Fresh setup for each test
    await page.goto("/login");
    await page.fill("#username", "admin");
    await page.fill("#password", "admin123");
    await page.click("#login-button");
  });

  test("can create user", async ({ page }) => {
    // ...
  });

  test("can delete user", async ({ page }) => {
    // ...
  });
});

// ❌ Bad - tests ขึ้นต่อกัน
let userId;
test("create user", async ({ page }) => {
  // Create user and save ID
  userId = "123"; // ❌ Shared state
});
test("delete user", async ({ page }) => {
  // Uses userId from previous test ❌
});
```

### 13.4 Assertion Best Practices

```javascript
// ✅ Good - specific assertions
await expect(page.getByRole("heading")).toHaveText("Welcome, John");
await expect(page.locator(".items")).toHaveCount(5);
await expect(page).toHaveURL("/dashboard");

// ❌ Bad - weak assertions
const heading = await page.locator("h1").textContent();
expect(heading).toBeTruthy(); // Too weak
```

### 13.5 Page Object Model Best Practices

```javascript
// ✅ Good Page Object
class LoginPage {
  constructor(page) {
    this.page = page;
    // Define locators once
    this.usernameInput = page.getByLabel("Username");
    this.passwordInput = page.getByLabel("Password");
    this.loginButton = page.getByRole("button", { name: "Login" });
  }

  // Encapsulate actions
  async login(username, password) {
    await this.usernameInput.fill(username);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
  }
}

// ✅ Good - ใช้ใน test
test("login test", async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.login("user", "pass");
});
```

### 13.7 Parallel Testing

```javascript
// playwright.config.js
module.exports = defineConfig({
  // Run tests in parallel
  fullyParallel: true,

  // Number of parallel workers
  workers: process.env.CI ? 1 : 4,

  // Or use percentage of CPU
  // workers: '50%',
});
```

### 13.8 Common Commands Summary

```bash
# Basic commands
npx playwright test                    # Run all tests
npx playwright test --headed           # Run with browser visible
npx playwright test --ui               # Run with UI mode
npx playwright test --debug            # Run in debug mode

# Run specific tests
npx playwright test login.spec.js      # Run specific file
npx playwright test --grep "login"     # Run tests matching pattern
npx playwright test --project=chromium # Run on specific browser

# Reporting
npx playwright show-report             # Show HTML report
npx playwright show-trace trace.zip    # View trace file

# Code generation
npx playwright codegen localhost:3000  # Record and generate tests

# Snapshots
npx playwright test --update-snapshots # Update visual snapshots
```

---

## 15. Performance Testing และ Accessibility

### แบบฝึกหัดที่ 1: Basic E2E Test

เขียน E2E tests สำหรับ Login functionality:

**ข้อกำหนด:**

- ทดสอบ login สำเร็จ
- ทดสอบ login ล้มเหลว (wrong credentials)
- ทดสอบ form validation (empty fields)
- ทดสอบ logout
- ใช้ `describe` จัดกลุ่ม tests

### แบบฝึกหัดที่ 2: Page Object Model

สร้าง Page Objects และใช้ใน tests:

**ข้อกำหนด:**

- สร้าง `LoginPage` class
- สร้าง `HomePage` class
- สร้าง `SearchPage` class
- สร้าง `BookDetailPage` class
- เขียน tests ที่ใช้ Page Objects

### แบบฝึกหัดที่ 3: Cross-Browser Testing

เขียน tests ที่ทำงานบนหลาย browsers:

**ข้อกำหนด:**

- ตั้งค่า `playwright.config.js` สำหรับ 3 browsers
- เขียน responsive tests
- ทดสอบบน mobile devices
- จัดการ browser-specific behaviors

### แบบฝึกหัดที่ 4: Visual Regression Testing

เขียน Visual Regression tests:

**ข้อกำหนด:**

- สร้าง baseline screenshots
- ทดสอบ homepage
- ทดสอบ component states (hover, focus, disabled)
- ทดสอบ dark mode
- ทดสอบ responsive layouts

### แบบฝึกหัดที่ 5: Complete User Journey

เขียน test สำหรับ complete user journey:

**ข้อกำหนด:**

- Register new user
- Login
- Search for book
- View book details
- Borrow book
- View borrowed books
- Return book
- Logout

---

## Quick Reference Card

### Test Structure

```javascript
const { test, expect } = require("@playwright/test");

test.describe("Feature", () => {
  test.beforeEach(async ({ page }) => {
    /* setup */
  });

  test("should do something", async ({ page }) => {
    await page.goto("/");
    await expect(page).toHaveTitle(/Title/);
  });
});
```

### Common Locators

```javascript
page.getByRole("button", { name: "Submit" });
page.getByLabel("Email");
page.getByPlaceholder("Enter email");
page.getByText("Welcome");
page.getByTestId("submit-btn");
page.locator("#id");
page.locator(".class");
```

### Common Actions

```javascript
await page.goto("/");
await locator.click();
await locator.fill("text");
await locator.check();
await locator.selectOption("value");
await locator.hover();
await page.keyboard.press("Enter");
```

### Common Assertions

```javascript
await expect(page).toHaveTitle(/Title/);
await expect(page).toHaveURL("/path");
await expect(locator).toBeVisible();
await expect(locator).toHaveText("text");
await expect(locator).toHaveCount(5);
await expect(locator).toBeEnabled();
await expect(locator).toHaveScreenshot("name.png");
```

### CLI Commands

```bash
npx playwright test
npx playwright test --headed
npx playwright test --ui
npx playwright test --debug
npx playwright show-report
npx playwright codegen localhost:3000
```

---

## เอกสารอ้างอิง

- [Playwright Official Documentation](https://playwright.dev/docs/intro)
- [Playwright API Reference](https://playwright.dev/docs/api/class-playwright)
- [Playwright Best Practices](https://playwright.dev/docs/best-practices)
- [Playwright Test Generator](https://playwright.dev/docs/codegen)

---

---

## ขยายความ 2 หัวใจสำคัญที่จะยกระดับคุณจาก "คนเขียนสคริปต์" เป็น "Test Automation Engineer" มืออาชีพ ตามแนวทางในคู่มือ

---

## 1. Page Object Model (POM): โครงสร้างที่ยั่งยืน

ในการทำงานจริง เว็บไซต์มีการเปลี่ยนแปลงตลอดเวลา ถ้าคุณเขียน Locator (เช่น `page.click('#login-btn')`) ไว้ในทุกไฟล์ Test เมื่อปุ่มเปลี่ยน ID คุณจะต้องตามแก้ทุกไฟล์! **POM** จึงเข้ามาแก้ปัญหานี้ครับ

### หลักการของ POM

เราจะแยก "หน้าเว็บ" ออกมาเป็น **Class** โดยที่:

- **Properties:** เก็บ Locators ของหน้านั้นๆ
- **Methods:** เก็บ Action หรือพฤติกรรมที่เกิดขึ้นในหน้านั้น

### ตัวอย่างการประยุกต์ใช้ (Login Page)

**ไฟล์: `models/LoginPage.js**` (เก็บ Logic ของหน้า Login)

```javascript
class LoginPage {
  constructor(page) {
    this.page = page;
    this.usernameInput = page.getByLabel("Username");
    this.passwordInput = page.getByLabel("Password");
    this.loginButton = page.getByRole("button", { name: "Log in" });
  }

  async login(user, pass) {
    await this.usernameInput.fill(user);
    await this.passwordInput.fill(pass);
    await this.loginButton.click();
  }
}
module.exports = { LoginPage };
```

**ไฟล์: `tests/auth.test.js**` (ตัวสคริปต์ทดสอบจะอ่านง่ายเหมือนภาษามนุษย์)

```javascript
test("user should login successfully", async ({ page }) => {
  const loginPage = new LoginPage(page);
  await page.goto("/login");
  await loginPage.login("myUser", "myPass"); // เรียกใช้ Method เดียว จบ!
  await expect(page).toHaveURL("/dashboard");
});
```

---

## 2. Visual Regression Testing: ตรวจสอบสิ่งที่ตาเห็น

บางครั้ง Code ทำงานถูกต้อง (Functional Pass) แต่หน้าตาพัง (UI Broken) เช่น ปุ่มกระเด็นไปทับข้อความ หรือสีเพี้ยน ซึ่งการตรวจด้วย Code ปกติทำได้ยากมาก **Visual Testing** จึงเข้ามาช่วยตรงนี้ครับ

### มันทำงานอย่างไร?

1. **Baseline:** เมื่อรันครั้งแรก Playwright จะถ่ายรูปหน้าจอเก็บไว้เป็น "ภาพต้นแบบ"
2. **Comparison:** เมื่อรันครั้งต่อไป มันจะถ่ายรูปใหม่แล้วนำมาเทียบกับต้นแบบ "Pixel ต่อ Pixel"
3. **Diff:** ถ้ามีความต่างเกินค่าที่กำหนด (Threshold) Test จะ Fail ทันที

### ตัวอย่างคำสั่งที่ใช้

```javascript
test("visual check of home page", async ({ page }) => {
  await page.goto("https://example.com");

  // ตรวจสอบทั้งหน้าจอ
  await expect(page).toHaveScreenshot("home-page.png");

  // หรือตรวจสอบเฉพาะส่วน เช่น ปุ่ม Login
  const loginBtn = page.getByRole("button", { name: "Login" });
  await expect(loginBtn).toHaveScreenshot("login-button.png");
});
```

> **Expert Tip:** ในคู่มือแนะนำให้ระวังเรื่อง Browser หรือ OS ที่ต่างกัน เพราะอาจทำให้ภาพเพี้ยนเล็กน้อย (Anti-aliasing) แนะนำให้รันบน **Docker** เพื่อให้ Environment นิ่งที่สุดครับ

---

### สรุปความแตกต่าง

- **POM** ทำให้ Code ของคุณ **"Maintain ง่าย"** ไม่ตายเมื่อ UI เปลี่ยน
- **Visual Testing** ทำให้คุณ **"มั่นใจ"** ว่าหน้าตาเว็บจะไม่พังต่อหน้า User

---

---

## **Network Mocking**

คือหนึ่งในฟีเจอร์ที่ทรงพลังที่สุด เพราะช่วยให้เราทดสอบสถานการณ์ที่ควบคุมได้ยากในชีวิตจริง เช่น "เซิร์ฟเวอร์ล่ม" หรือ "อินเทอร์เน็ตช้า" โดยไม่ต้องไปยุ่งกับระบบ Backend จริงๆ ครับ

นี่คือตัวอย่างการสาธิต 2 กรณีตามที่คุณต้องการครับ:

---

### 1. การจำลองกรณี Server ล่ม (Internal Server Error 500)

เราจะใช้ `page.route` เพื่อดักจับ (Intercept) API call ที่เราต้องการ แล้วสั่งให้มันตอบกลับด้วย Status 500 ทันที เพื่อดูว่าหน้าเว็บของเราแสดงข้อความแจ้งเตือน Error หรือไม่

**ตัวอย่างสคริปต์:**

```javascript
test("ควรแสดงข้อความแจ้งเตือนเมื่อระบบหลังบ้านล่ม (500 Error)", async ({
  page,
}) => {
  // 1. ดักจับ API ที่ดึงข้อมูลหนังสือ (ตัวอย่าง URL)
  await page.route("**/api/books", (route) => {
    route.fulfill({
      status: 500,
      contentType: "application/json",
      body: JSON.stringify({ message: "Internal Server Error" }),
    });
  });

  // 2. ไปที่หน้าเว็บ
  await page.goto("/library");

  // 3. ตรวจสอบว่ามี Alert หรือข้อความแจ้ง Error ปรากฏขึ้นหรือไม่ (Assert)
  const errorMessage = page.locator(".error-banner");
  await expect(errorMessage).toBeVisible();
  await expect(errorMessage).toHaveText(/Something went wrong/);
});
```

---

### 2. การจำลองกรณี API ตอบกลับช้า (Network Delay/Timeout)

กรณีนี้สำคัญมากเพื่อดูว่า **Loading Spinner** ทำงานไหม หรือระบบมี Timeout ที่เหมาะสมหรือไม่ เราจะหน่วงเวลา (Delay) การตอบกลับของ API ครับ

**ตัวอย่างสคริปต์:**

```javascript
test("ควรแสดง Loading Spinner เมื่อ API ตอบกลับช้า", async ({ page }) => {
  // 1. ดักจับ API และหน่วงเวลา 5 วินาที
  await page.route("**/api/books", async (route) => {
    await new Promise((resolve) => setTimeout(resolve, 5000)); // หน่วงเวลา 5 วินาที
    await route.fulfill({
      status: 200,
      body: JSON.stringify([{ id: 1, title: "Delay Book" }]),
    });
  });

  // 2. ไปที่หน้าเว็บ
  await page.goto("/library");

  // 3. ตรวจสอบว่า Loading Spinner ปรากฏขึ้น (Assert)
  const spinner = page.locator("#loading-icon");
  await expect(spinner).toBeVisible();

  // 4. รอจนกว่า Spinner จะหายไป และข้อมูลปรากฏ (เลือกตรวจสอบอย่างใดอย่างหนึ่ง)
  await expect(spinner).toBeHidden({ timeout: 10000 });
  await expect(page.getByText("Delay Book")).toBeVisible();
});
```

---

### 💡 ประโยชน์

- **ไม่ต้องรอ Backend:** เราสามารถเขียน Test ได้เลยแม้ว่า API จริงจะยังสร้างไม่เสร็จ
- **Determinism:** ทำให้ Test ของเรามีความเสถียร (Stable) เพราะไม่ต้องกังวลเรื่อง Network จริงจะสวิง
- **Edge Case Coverage:** ช่วยให้เราทดสอบกรณีร้ายแรงได้โดยไม่กระทบต่อข้อมูลจริงใน Database

## การทำแบบนี้บ่อยๆ จะช่วยให้คุณออกแบบระบบที่ **Robust** (ทนทาน) ต่อความผิดพลาดของระบบภายนอกได้ดีขึ้นมาก

---

## การใช้ "Fixtures" (บทที่ 10)

เพื่อทำระบบ Login อัตโนมัติให้ทุก Test Case โดยไม่ต้องเขียนโค้ดซ้ำๆ
การใช้ **Fixtures** ใน Playwright คือการสร้าง "สภาพแวดล้อมพร้อมใช้" ให้กับ Test Case ของเราครับ หากเปรียบเทียบกับคำสั่ง `beforeEach` ใน Jest ตัว Fixtures จะมีความยืดหยุ่นกว่ามาก เพราะมันสามารถจัดการ State (เช่น การ Login) ไว้ให้เราล่วงหน้า และเรียกใช้เฉพาะใน Test ที่ต้องการได้ทันที

นี่คือวิธีการทำ **Auto-Login Fixture** เพื่อให้ทุก Test Case เริ่มต้นที่สถานะ Login แล้วเสมอครับ:

---

### 1. สร้างไฟล์ Fixture (Custom Fixture)

เราจะขยายความสามารถของ Playwright โดยสร้าง Fixture ที่ชื่อว่า `loggedInPage` ขึ้นมาครับ

**ไฟล์: `fixtures/auth-fixture.js**`

```javascript
const base = require("@playwright/test");
const { LoginPage } = require("../models/LoginPage");

// สร้าง fixture ใหม่โดยต่อยอดจาก base test ของ Playwright
exports.test = base.test.extend({
  loggedInPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page);

    // ขั้นตอนการ Login อัตโนมัติ
    await page.goto("/login");
    await loginPage.login("admin_user", "password123");

    // ตรวจสอบว่า Login สำเร็จก่อนส่งไปให้ Test Case
    await base.expect(page).toHaveURL("/dashboard");

    // ส่ง page ที่ login แล้วไปให้ Test Case ใช้งาน
    await use(page);

    // ส่วนของ Cleanup (จะทำงานหลังจาก Test จบ)
    await page.context().clearCookies();
  },
});

exports.expect = base.expect;
```

---

### 2. การนำไปใช้ใน Test Case

เมื่อเรามี Fixture แล้ว เวลาเขียน Test เราไม่จำเป็นต้องเรียกฟังก์ชัน Login อีกต่อไป แค่ระบุชื่อ `loggedInPage` ใน Arguments ครับ

**ไฟล์: `tests/dashboard.test.js**`

```javascript
const { test, expect } = require("../fixtures/auth-fixture");

test.describe("Dashboard Features", () => {
  // Test นี้จะเริ่มที่หน้า Dashboard ในสถานะที่ Login แล้วทันที!
  test("ควรแสดงชื่อผู้ใช้งานบน Header", async ({ loggedInPage }) => {
    const header = loggedInPage.locator("#user-profile");
    await expect(header).toContainText("admin_user");
  });

  test("ควรสามารถเข้าถึงเมนูรายงานได้", async ({ loggedInPage }) => {
    await loggedInPage.click("text=Reports");
    await expect(loggedInPage).toHaveURL("/reports");
  });
});
```

---

### 🚀 ทำไมผู้เชี่ยวชาญถึงแนะนำวิธีนี้? (Expert Insights)

1. **Code Reusability:** ไม่ต้องเขียน `await login(...)` ซ้ำในทุกไฟล์ Test
2. **Encapsulation:** Logic การ Login ถูกเก็บไว้ที่เดียว (Fixture) หากวิธีการ Login ของระบบเปลี่ยน (เช่น เปลี่ยนจากใช้ Password เป็น OTP) คุณแก้ที่ไฟล์เดียวจบ
3. **Efficiency:** Playwright จัดการ Lifecycle ของ Fixture ให้เอง (Setup และ Teardown) ทำให้ลดโอกาสเกิด **Memory Leak** หรือข้อมูลค้างจาก Test ก่อนหน้า
4. **Selective Use:** หาก Test Case ไหนไม่ต้องการการ Login (เช่น หน้า Landing Page หรือ Contact Us) คุณก็แค่เรียกใช้ `{ page }` ปกติแทน `{ loggedInPage }` ได้เลยครับ
