# Playwright: Learning by Example

# สารบัญ

1. [Level 1: เริ่มต้น - Hello Playwright](#level-1-เริ่มต้น---hello-playwright)
2. [Level 2: การหา Elements](#level-2-การหา-elements-locators)
3. [Level 3: การโต้ตอบกับหน้าเว็บ](#level-3-การโต้ตอบกับหน้าเว็บ-actions)
4. [Level 4: การตรวจสอบผลลัพธ์](#level-4-การตรวจสอบผลลัพธ์-assertions)
5. [Level 5: การจัดการ Test](#level-5-การจัดการ-test-structure)
6. [Level 6: Page Object Model](#level-6-page-object-model)
7. [Level 7: Test Data & Fixtures](#level-7-test-data--fixtures)
8. [Level 8: Cross-Browser Testing](#level-8-cross-browser-testing)
9. [Level 9: Visual Regression Testing](#level-9-visual-regression-testing)
10. [Level 10: Advanced Techniques](#level-10-advanced-techniques)
11. [Level 11: CI/CD Integration](#level-11-cicd-integration)
12. [แบบฝึกหัดรวม](#แบบฝึกหัดรวม-library-management-system)

---

# Level 1: เริ่มต้น - Hello Playwright

## 🎯 เป้าหมาย

- ติดตั้ง Playwright
- เขียน Test แรก
- รัน Test และดูผลลัพธ์

## 📦 การติดตั้ง

```bash
# สร้างโปรเจกต์ใหม่
mkdir playwright-tutorial
cd playwright-tutorial
npm init -y

# ติดตั้ง Playwright
npm install --save-dev @playwright/test

# ติดตั้ง Browsers
npx playwright install
```

## 🔰 Example 1.1: Test แรกของคุณ

สร้างไฟล์ `tests/first.spec.js`:

```javascript
// tests/first.spec.js
const { test, expect } = require("@playwright/test");

test("เปิดหน้า Google", async ({ page }) => {
  // 1. ไปที่หน้าเว็บ
  await page.goto("https://www.google.com");

  // 2. ตรวจสอบ title
  await expect(page).toHaveTitle(/Google/);
});
```

**รัน Test:**

```bash
npx playwright test
```

**อธิบาย:**

- `test()` - สร้าง test case
- `page` - object ที่ใช้ควบคุม browser
- `goto()` - เปิด URL
- `expect()` - ตรวจสอบผลลัพธ์

## 🔰 Example 1.2: Test หลายขั้นตอน

```javascript
// tests/search.spec.js
const { test, expect } = require("@playwright/test");

test("ค้นหาใน Google", async ({ page }) => {
  // Step 1: เปิดหน้า Google
  await page.goto("https://www.google.com");

  // Step 2: พิมพ์คำค้นหา
  await page.fill('textarea[name="q"]', "Playwright testing");

  // Step 3: กด Enter
  await page.keyboard.press("Enter");

  // Step 4: รอผลลัพธ์และตรวจสอบ
  await expect(page).toHaveTitle(/Playwright/);
});
```

## 🔰 Example 1.3: ดูผลลัพธ์แบบ UI Mode

```bash
# รัน Test พร้อม UI
npx playwright test --ui

# รันแบบเห็น Browser
npx playwright test --headed

# รันแบบ Debug
npx playwright test --debug
```

## 📝 สรุป Level 1

| คำสั่ง        | หน้าที่            |
| ------------- | ------------------ |
| `test()`      | สร้าง test case    |
| `page.goto()` | เปิด URL           |
| `expect()`    | ตรวจสอบผลลัพธ์     |
| `--headed`    | รันแบบเห็น browser |
| `--debug`     | รันแบบ debug       |

---

# Level 2: การหา Elements (Locators)

## 🎯 เป้าหมาย

- เข้าใจ Locator strategies
- เลือกวิธีที่เหมาะสม
- หา elements แบบต่างๆ

## 🔰 Example 2.1: Locators พื้นฐาน (แนะนำ ✅)

```javascript
// tests/locators-basic.spec.js
const { test, expect } = require("@playwright/test");

test("Locators ที่แนะนำ", async ({ page }) => {
  await page.goto("http://localhost:3000");

  // ✅ 1. getByRole - ดีที่สุด! (Accessibility)
  await page.getByRole("button", { name: "Login" }).click();
  await page.getByRole("link", { name: "Sign Up" }).click();
  await page.getByRole("textbox", { name: "Username" }).fill("test");
  await page.getByRole("heading", { name: "Welcome" });

  // ✅ 2. getByLabel - ดีมากสำหรับ form
  await page.getByLabel("Email").fill("test@example.com");
  await page.getByLabel("Password").fill("secret123");

  // ✅ 3. getByPlaceholder
  await page.getByPlaceholder("Search books...").fill("JavaScript");

  // ✅ 4. getByText - สำหรับ text content
  await page.getByText("Welcome to Library").isVisible();

  // ✅ 5. getByTestId - stable ที่สุด!
  await page.getByTestId("login-button").click();
  await page.getByTestId("search-input").fill("Python");
});
```

## 🔰 Example 2.2: data-testid (Best Practice)

**เพิ่มใน HTML:**

```html
<button data-testid="login-button">Login</button>
<input data-testid="search-input" placeholder="Search..." />
<div data-testid="book-card">...</div>
```

**ใช้ใน Test:**

```javascript
// tests/testid.spec.js
const { test, expect } = require("@playwright/test");

test("ใช้ data-testid", async ({ page }) => {
  await page.goto("http://localhost:3000");

  // หา element ด้วย data-testid
  const loginBtn = page.getByTestId("login-button");
  const searchInput = page.getByTestId("search-input");
  const bookCards = page.getByTestId("book-card");

  // ใช้งาน
  await searchInput.fill("JavaScript");
  await loginBtn.click();

  // นับจำนวน elements
  await expect(bookCards).toHaveCount(5);
});
```

## 🔰 Example 2.3: CSS & XPath (ใช้เมื่อจำเป็น)

```javascript
// tests/locators-css-xpath.spec.js
const { test, expect } = require("@playwright/test");

test("CSS และ XPath Locators", async ({ page }) => {
  await page.goto("http://localhost:3000");

  // ⚠️ CSS Selector - ใช้เมื่อไม่มีทางเลือกอื่น
  await page.locator(".book-card").first().click();
  await page.locator("#main-content").isVisible();
  await page.locator("button.primary").click();

  // ⚠️ XPath - หลีกเลี่ยงถ้าเป็นไปได้
  await page.locator('//button[contains(text(), "Submit")]').click();

  // Attribute selectors
  await page.locator('[type="submit"]').click();
  await page.locator('[href="/login"]').click();
});
```

## 🔰 Example 2.4: การ Filter และ Chain Locators

```javascript
// tests/locators-advanced.spec.js
const { test, expect } = require("@playwright/test");

test("Filter และ Chain Locators", async ({ page }) => {
  await page.goto("http://localhost:3000/books");

  // Filter by text
  const jsBook = page
    .getByTestId("book-card")
    .filter({ hasText: "JavaScript" });
  await jsBook.click();

  // Filter by child element
  const availableBooks = page
    .getByTestId("book-card")
    .filter({ has: page.getByText("Available") });
  await expect(availableBooks).toHaveCount(3);

  // Chain locators (หา element ภายใน element)
  const bookCard = page.getByTestId("book-card").first();
  const title = bookCard.locator(".title");
  const borrowBtn = bookCard.getByRole("button", { name: "Borrow" });

  await expect(title).toHaveText("JavaScript Guide");
  await borrowBtn.click();

  // nth element
  await page.getByTestId("book-card").nth(2).click(); // element ที่ 3
  await page.getByTestId("book-card").last().click(); // element สุดท้าย
});
```

## 🔰 Example 2.5: รอ Element (Auto-waiting)

```javascript
// tests/waiting.spec.js
const { test, expect } = require("@playwright/test");

test("Playwright รอ element อัตโนมัติ", async ({ page }) => {
  await page.goto("http://localhost:3000");

  // ✅ Playwright รออัตโนมัติ - ไม่ต้องเขียน wait!
  await page.getByTestId("slow-button").click(); // รอจนกว่า button จะ clickable

  // รอ element เฉพาะ (กรณีต้องการ explicit wait)
  await page.waitForSelector(".loading", { state: "hidden" });
  await page.waitForSelector(".results", { state: "visible" });

  // รอ network idle
  await page.waitForLoadState("networkidle");

  // รอ URL เปลี่ยน
  await page.waitForURL("**/dashboard");
});
```

## 📝 สรุป Level 2: Locator Priority

| ลำดับ | Locator            | เมื่อไหร่ใช้         | ตัวอย่าง                               |
| ----- | ------------------ | -------------------- | -------------------------------------- |
| 1 ✅  | `getByTestId`      | ทุกครั้งที่เป็นไปได้ | `getByTestId('login-btn')`             |
| 2 ✅  | `getByRole`        | Accessibility        | `getByRole('button', {name: 'Login'})` |
| 3 ✅  | `getByLabel`       | Form inputs          | `getByLabel('Email')`                  |
| 4 ✅  | `getByPlaceholder` | Input fields         | `getByPlaceholder('Search...')`        |
| 5 ✅  | `getByText`        | Text content         | `getByText('Welcome')`                 |
| 6 ⚠️  | `locator(css)`     | เมื่อจำเป็น          | `locator('.card')`                     |
| 7 ❌  | `locator(xpath)`   | หลีกเลี่ยง           | `locator('//button')`                  |

---

# Level 3: การโต้ตอบกับหน้าเว็บ (Actions)

## 🎯 เป้าหมาย

- Click, Type, Select
- Keyboard & Mouse actions
- File upload, Dialogs

## 🔰 Example 3.1: การ Click

```javascript
// tests/actions-click.spec.js
const { test, expect } = require("@playwright/test");

test("ประเภทของการ Click", async ({ page }) => {
  await page.goto("http://localhost:3000");

  // Click ปกติ
  await page.getByTestId("button").click();

  // Double click
  await page.getByTestId("item").dblclick();

  // Right click (context menu)
  await page.getByTestId("item").click({ button: "right" });

  // Click with modifier keys
  await page.getByTestId("link").click({ modifiers: ["Control"] }); // Ctrl+Click
  await page.getByTestId("item").click({ modifiers: ["Shift"] }); // Shift+Click

  // Force click (ข้าม visibility check)
  await page.getByTestId("hidden-button").click({ force: true });

  // Click at position
  await page.getByTestId("canvas").click({ position: { x: 100, y: 50 } });
});
```

## 🔰 Example 3.2: การพิมพ์ข้อความ

```javascript
// tests/actions-typing.spec.js
const { test, expect } = require("@playwright/test");

test("การพิมพ์ข้อความ", async ({ page }) => {
  await page.goto("http://localhost:3000/login");

  // fill() - เคลียร์ก่อนแล้วพิมพ์ (แนะนำ)
  await page.getByLabel("Username").fill("john_doe");
  await page.getByLabel("Password").fill("secret123");

  // type() - พิมพ์ทีละตัวอักษร (เหมือนคนพิมพ์)
  await page.getByLabel("Search").type("JavaScript", { delay: 100 });

  // clear() - ลบข้อความทั้งหมด
  await page.getByLabel("Username").clear();

  // pressSequentially() - พิมพ์ทีละตัว (ใหม่กว่า type)
  await page.getByLabel("OTP").pressSequentially("123456", { delay: 50 });
});
```

## 🔰 Example 3.3: Keyboard Actions

```javascript
// tests/actions-keyboard.spec.js
const { test, expect } = require("@playwright/test");

test("Keyboard actions", async ({ page }) => {
  await page.goto("http://localhost:3000/search");

  // กดปุ่ม single key
  await page.keyboard.press("Enter");
  await page.keyboard.press("Tab");
  await page.keyboard.press("Escape");

  // กด key combinations
  await page.keyboard.press("Control+a"); // Select all
  await page.keyboard.press("Control+c"); // Copy
  await page.keyboard.press("Control+v"); // Paste
  await page.keyboard.press("Control+Shift+Delete"); // Multiple modifiers

  // Type text ด้วย keyboard
  await page.keyboard.type("Hello World");

  // Hold key
  await page.keyboard.down("Shift");
  await page.keyboard.press("ArrowDown");
  await page.keyboard.press("ArrowDown");
  await page.keyboard.up("Shift");
});
```

## 🔰 Example 3.4: Select, Checkbox, Radio

```javascript
// tests/actions-form.spec.js
const { test, expect } = require("@playwright/test");

test("Form interactions", async ({ page }) => {
  await page.goto("http://localhost:3000/register");

  // Select dropdown
  await page.getByLabel("Country").selectOption("thailand"); // by value
  await page.getByLabel("Country").selectOption({ label: "Thailand" }); // by label
  await page.getByLabel("Country").selectOption({ index: 2 }); // by index

  // Multiple select
  await page
    .getByLabel("Languages")
    .selectOption(["thai", "english", "japanese"]);

  // Checkbox
  await page.getByLabel("Accept terms").check();
  await page.getByLabel("Newsletter").uncheck();

  // ตรวจสอบ checkbox state
  await expect(page.getByLabel("Accept terms")).toBeChecked();
  await expect(page.getByLabel("Newsletter")).not.toBeChecked();

  // Radio button
  await page.getByLabel("Male").check();
  await page.getByRole("radio", { name: "Premium" }).check();
});
```

## 🔰 Example 3.5: File Upload

```javascript
// tests/actions-upload.spec.js
const { test, expect } = require("@playwright/test");
const path = require("path");

test("File upload", async ({ page }) => {
  await page.goto("http://localhost:3000/upload");

  // Upload single file
  await page
    .getByLabel("Profile Picture")
    .setInputFiles("test-files/photo.jpg");

  // Upload multiple files
  await page
    .getByLabel("Documents")
    .setInputFiles(["test-files/doc1.pdf", "test-files/doc2.pdf"]);

  // Upload with absolute path
  const filePath = path.join(__dirname, "test-files", "photo.jpg");
  await page.getByTestId("file-input").setInputFiles(filePath);

  // Clear file input
  await page.getByLabel("Profile Picture").setInputFiles([]);

  // Handle file chooser dialog
  const fileChooserPromise = page.waitForEvent("filechooser");
  await page.getByRole("button", { name: "Upload" }).click();
  const fileChooser = await fileChooserPromise;
  await fileChooser.setFiles("test-files/photo.jpg");
});
```

## 🔰 Example 3.6: Dialogs (Alert, Confirm, Prompt)

```javascript
// tests/actions-dialogs.spec.js
const { test, expect } = require("@playwright/test");

test("Handle dialogs", async ({ page }) => {
  await page.goto("http://localhost:3000");

  // ต้อง setup listener ก่อน trigger dialog!

  // Alert
  page.on("dialog", async (dialog) => {
    console.log(dialog.message()); // อ่านข้อความ
    await dialog.accept(); // กด OK
  });
  await page.getByRole("button", { name: "Show Alert" }).click();

  // Confirm - Accept
  page.once("dialog", (dialog) => dialog.accept());
  await page.getByRole("button", { name: "Delete" }).click();

  // Confirm - Dismiss
  page.once("dialog", (dialog) => dialog.dismiss());
  await page.getByRole("button", { name: "Delete" }).click();

  // Prompt - ใส่ค่า
  page.once("dialog", (dialog) => dialog.accept("John Doe"));
  await page.getByRole("button", { name: "Enter Name" }).click();
});
```

## 🔰 Example 3.7: Hover, Drag & Drop

```javascript
// tests/actions-mouse.spec.js
const { test, expect } = require("@playwright/test");

test("Mouse actions", async ({ page }) => {
  await page.goto("http://localhost:3000");

  // Hover
  await page.getByTestId("menu-item").hover();
  await expect(page.getByTestId("dropdown")).toBeVisible();

  // Drag and Drop
  await page.getByTestId("draggable").dragTo(page.getByTestId("droppable"));

  // Drag with steps
  const source = page.getByTestId("item-1");
  const target = page.getByTestId("item-3");
  await source.dragTo(target, {
    sourcePosition: { x: 10, y: 10 },
    targetPosition: { x: 50, y: 50 },
  });

  // Manual drag (more control)
  await page.getByTestId("slider").hover();
  await page.mouse.down();
  await page.mouse.move(500, 300);
  await page.mouse.up();
});
```

## 📝 สรุป Level 3: Actions

| Action       | คำสั่ง            | ตัวอย่าง                        |
| ------------ | ----------------- | ------------------------------- |
| Click        | `click()`         | `locator.click()`               |
| Double Click | `dblclick()`      | `locator.dblclick()`            |
| Type         | `fill()`          | `locator.fill('text')`          |
| Clear        | `clear()`         | `locator.clear()`               |
| Select       | `selectOption()`  | `locator.selectOption('value')` |
| Check        | `check()`         | `locator.check()`               |
| Upload       | `setInputFiles()` | `locator.setInputFiles('path')` |
| Hover        | `hover()`         | `locator.hover()`               |
| Drag         | `dragTo()`        | `source.dragTo(target)`         |

---

# Level 4: การตรวจสอบผลลัพธ์ (Assertions)

## 🎯 เป้าหมาย

- Page Assertions
- Locator Assertions
- Soft Assertions

## 🔰 Example 4.1: Page Assertions

```javascript
// tests/assertions-page.spec.js
const { test, expect } = require("@playwright/test");

test("Page assertions", async ({ page }) => {
  await page.goto("http://localhost:3000");

  // ตรวจสอบ 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(/localhost/);
  await expect(page).toHaveURL("**/dashboard"); // glob pattern

  // ตรวจสอบ Screenshot (Visual Testing)
  await expect(page).toHaveScreenshot("homepage.png");
});
```

## 🔰 Example 4.2: Locator Assertions - Visibility

```javascript
// tests/assertions-visibility.spec.js
const { test, expect } = require("@playwright/test");

test("Visibility assertions", async ({ page }) => {
  await page.goto("http://localhost:3000");

  // เห็นบนหน้าจอ
  await expect(page.getByTestId("header")).toBeVisible();

  // ไม่เห็นบนหน้าจอ
  await expect(page.getByTestId("loading")).not.toBeVisible();
  await expect(page.getByTestId("loading")).toBeHidden();

  // มีอยู่ใน DOM (อาจไม่เห็น)
  await expect(page.getByTestId("hidden-data")).toBeAttached();

  // ไม่มีอยู่ใน DOM
  await expect(page.getByTestId("removed")).not.toBeAttached();

  // Element ว่างเปล่า
  await expect(page.getByTestId("empty-list")).toBeEmpty();
});
```

## 🔰 Example 4.3: Locator Assertions - Text

```javascript
// tests/assertions-text.spec.js
const { test, expect } = require("@playwright/test");

test("Text assertions", async ({ page }) => {
  await page.goto("http://localhost:3000");

  // ตรวจสอบ text เป๊ะๆ
  await expect(page.getByTestId("title")).toHaveText("Welcome to Library");

  // ตรวจสอบ text แบบ partial
  await expect(page.getByTestId("title")).toContainText("Welcome");

  // ตรวจสอบด้วย regex
  await expect(page.getByTestId("title")).toHaveText(/Welcome/i);

  // ตรวจสอบ input value
  await expect(page.getByLabel("Username")).toHaveValue("john_doe");
  await expect(page.getByLabel("Search")).toHaveValue(/java/i);

  // ตรวจสอบหลาย elements
  await expect(page.getByTestId("book-title")).toHaveText([
    "JavaScript Guide",
    "Python Basics",
    "React Handbook",
  ]);
});
```

## 🔰 Example 4.4: Locator Assertions - State

```javascript
// tests/assertions-state.spec.js
const { test, expect } = require("@playwright/test");

test("State assertions", async ({ page }) => {
  await page.goto("http://localhost:3000/form");

  // Enabled/Disabled
  await expect(page.getByRole("button", { name: "Submit" })).toBeEnabled();
  await expect(page.getByRole("button", { name: "Delete" })).toBeDisabled();

  // Editable
  await expect(page.getByLabel("Username")).toBeEditable();
  await expect(page.getByLabel("User ID")).not.toBeEditable(); // readonly

  // Checked (checkbox, radio)
  await expect(page.getByLabel("Accept terms")).toBeChecked();
  await expect(page.getByLabel("Newsletter")).not.toBeChecked();

  // Focused
  await page.getByLabel("Email").focus();
  await expect(page.getByLabel("Email")).toBeFocused();
});
```

## 🔰 Example 4.5: Locator Assertions - Attributes & CSS

```javascript
// tests/assertions-attributes.spec.js
const { test, expect } = require("@playwright/test");

test("Attribute and CSS assertions", async ({ page }) => {
  await page.goto("http://localhost:3000");

  // ตรวจสอบ attribute
  await expect(page.getByTestId("link")).toHaveAttribute("href", "/about");
  await expect(page.getByTestId("image")).toHaveAttribute("alt", /logo/i);

  // ตรวจสอบ class
  await expect(page.getByTestId("button")).toHaveClass("btn btn-primary");
  await expect(page.getByTestId("button")).toHaveClass(/primary/);

  // ตรวจสอบ CSS property
  await expect(page.getByTestId("error")).toHaveCSS("color", "rgb(255, 0, 0)");
  await expect(page.getByTestId("header")).toHaveCSS("font-size", "24px");

  // ตรวจสอบ ID
  await expect(page.getByTestId("main")).toHaveId("main-content");
});
```

## 🔰 Example 4.6: Count & List Assertions

```javascript
// tests/assertions-count.spec.js
const { test, expect } = require("@playwright/test");

test("Count and list assertions", async ({ page }) => {
  await page.goto("http://localhost:3000/books");

  // นับจำนวน elements
  await expect(page.getByTestId("book-card")).toHaveCount(10);

  // ตรวจสอบว่ามีอย่างน้อย 1 element
  await expect(page.getByTestId("book-card").first()).toBeVisible();

  // ตรวจสอบ list ทั้งหมด
  const categories = page.getByTestId("category-item");
  await expect(categories).toHaveText([
    "Programming",
    "Science",
    "Literature",
    "History",
  ]);
});
```

## 🔰 Example 4.7: Soft Assertions

```javascript
// tests/assertions-soft.spec.js
const { test, expect } = require("@playwright/test");

test("Soft assertions - รัน assertions ทั้งหมดก่อน fail", async ({ page }) => {
  await page.goto("http://localhost:3000/profile");

  // Soft assertion - ไม่หยุดทันทีถ้า fail
  await expect.soft(page.getByTestId("name")).toHaveText("John Doe");
  await expect.soft(page.getByTestId("email")).toHaveText("john@example.com");
  await expect.soft(page.getByTestId("role")).toHaveText("Admin");
  await expect.soft(page.getByTestId("status")).toHaveText("Active");

  // ถ้ามี soft assertion fail, test จะ fail ตรงนี้
  // แต่จะรัน assertions ข้างบนทั้งหมดก่อน
});

test("Mixed assertions", async ({ page }) => {
  await page.goto("http://localhost:3000");

  // Hard assertion - หยุดทันทีถ้า fail
  await expect(page.getByTestId("header")).toBeVisible();

  // Soft assertions
  await expect.soft(page.getByTestId("item-1")).toHaveText("First");
  await expect.soft(page.getByTestId("item-2")).toHaveText("Second");

  // Hard assertion อีกครั้ง
  await expect(page.getByTestId("footer")).toBeVisible();
});
```

## 🔰 Example 4.8: Custom Timeout

```javascript
// tests/assertions-timeout.spec.js
const { test, expect } = require("@playwright/test");

test("Custom timeout", async ({ page }) => {
  await page.goto("http://localhost:3000");

  // รอนานขึ้น (default 5 วินาที)
  await expect(page.getByTestId("slow-element")).toBeVisible({
    timeout: 30000,
  });

  // Polling assertion (เช็คซ้ำๆ จนกว่าจะผ่าน)
  await expect(async () => {
    const count = await page.getByTestId("notification").count();
    expect(count).toBeGreaterThan(0);
  }).toPass({ timeout: 10000 });
});
```

## 📝 สรุป Level 4: Assertions

| ประเภท         | Assertion           | ตัวอย่าง                                       |
| -------------- | ------------------- | ---------------------------------------------- |
| **Page**       | `toHaveTitle()`     | `expect(page).toHaveTitle('Title')`            |
|                | `toHaveURL()`       | `expect(page).toHaveURL('/path')`              |
| **Visibility** | `toBeVisible()`     | `expect(locator).toBeVisible()`                |
|                | `toBeHidden()`      | `expect(locator).toBeHidden()`                 |
| **Text**       | `toHaveText()`      | `expect(locator).toHaveText('text')`           |
|                | `toContainText()`   | `expect(locator).toContainText('part')`        |
| **State**      | `toBeEnabled()`     | `expect(locator).toBeEnabled()`                |
|                | `toBeChecked()`     | `expect(locator).toBeChecked()`                |
| **Attribute**  | `toHaveAttribute()` | `expect(locator).toHaveAttribute('href', '/')` |
|                | `toHaveClass()`     | `expect(locator).toHaveClass('active')`        |
| **Count**      | `toHaveCount()`     | `expect(locator).toHaveCount(5)`               |

---

# Level 5: การจัดการ Test Structure

## 🎯 เป้าหมาย

- จัดกลุ่ม tests ด้วย describe
- Setup/Teardown ด้วย hooks
- Test annotations

## 🔰 Example 5.1: การจัดกลุ่มด้วย describe

```javascript
// tests/structure-describe.spec.js
const { test, expect } = require("@playwright/test");

test.describe("Login Feature", () => {
  test.describe("Valid Login", () => {
    test("should login with correct credentials", async ({ page }) => {
      await page.goto("http://localhost:3000/login");
      await page.getByLabel("Username").fill("admin");
      await page.getByLabel("Password").fill("admin123");
      await page.getByRole("button", { name: "Login" }).click();
      await expect(page).toHaveURL("/dashboard");
    });

    test("should remember user", async ({ page }) => {
      await page.goto("http://localhost:3000/login");
      await page.getByLabel("Remember me").check();
      // ...
    });
  });

  test.describe("Invalid Login", () => {
    test("should show error for wrong password", async ({ page }) => {
      await page.goto("http://localhost:3000/login");
      await page.getByLabel("Username").fill("admin");
      await page.getByLabel("Password").fill("wrong");
      await page.getByRole("button", { name: "Login" }).click();
      await expect(page.getByTestId("error")).toHaveText("Invalid credentials");
    });

    test("should show error for empty fields", async ({ page }) => {
      await page.goto("http://localhost:3000/login");
      await page.getByRole("button", { name: "Login" }).click();
      await expect(page.getByTestId("error")).toBeVisible();
    });
  });
});
```

## 🔰 Example 5.2: Hooks (beforeEach, afterEach, beforeAll, afterAll)

```javascript
// tests/structure-hooks.spec.js
const { test, expect } = require("@playwright/test");

test.describe("Book Management", () => {
  // รันก่อน test ทั้งหมดใน describe นี้ (ครั้งเดียว)
  test.beforeAll(async () => {
    console.log("🚀 Starting Book Management tests");
    // เช่น setup test data ใน database
  });

  // รันหลัง test ทั้งหมดใน describe นี้ (ครั้งเดียว)
  test.afterAll(async () => {
    console.log("✅ Finished Book Management tests");
    // เช่น cleanup test data
  });

  // รันก่อนทุก test
  test.beforeEach(async ({ page }) => {
    console.log("📖 Before each test");
    await page.goto("http://localhost:3000/books");
  });

  // รันหลังทุก test
  test.afterEach(async ({ page }) => {
    console.log("📝 After each test");
    // เช่น screenshot on failure
  });

  test("should display book list", async ({ page }) => {
    await expect(page.getByTestId("book-card")).toHaveCount(10);
  });

  test("should search books", async ({ page }) => {
    await page.getByPlaceholder("Search").fill("JavaScript");
    await page.keyboard.press("Enter");
    await expect(page.getByTestId("book-card").first()).toContainText(
      "JavaScript",
    );
  });
});
```

## 🔰 Example 5.3: Test Annotations

```javascript
// tests/structure-annotations.spec.js
const { test, expect } = require("@playwright/test");

test.describe("Feature Tests", () => {
  // ข้าม test นี้
  test.skip("should be implemented later", async ({ page }) => {
    // ...
  });

  // รันเฉพาะ test นี้
  test.only("debug this test", async ({ page }) => {
    await page.goto("http://localhost:3000");
    // ...
  });

  // Test ที่คาดว่าจะ fail (known bug)
  test.fail("known bug - issue #123", async ({ page }) => {
    await page.goto("http://localhost:3000/broken");
    await expect(page.getByTestId("feature")).toBeVisible();
  });

  // รันช้าลง (สำหรับ debug)
  test.slow("complex test that needs more time", async ({ page }) => {
    // timeout จะเพิ่มเป็น 3 เท่า
    await page.goto("http://localhost:3000");
    // ...
  });

  // ข้าม test ตาม condition
  test("only on webkit", async ({ page, browserName }) => {
    test.skip(browserName !== "webkit", "WebKit only test");
    // ...
  });
});

// ข้าม describe ทั้งหมด
test.describe.skip("Disabled Feature", () => {
  test("test 1", async ({ page }) => {});
  test("test 2", async ({ page }) => {});
});

// รันเฉพาะ describe นี้
test.describe.only("Focus on this", () => {
  test("important test", async ({ page }) => {});
});
```

## 🔰 Example 5.4: Parameterized Tests (Data-Driven)

```javascript
// tests/structure-parameterized.spec.js
const { test, expect } = require("@playwright/test");

// Test data
const loginTestData = [
  { username: "admin", password: "admin123", expected: "Admin Dashboard" },
  { username: "user", password: "user123", expected: "User Dashboard" },
  { username: "guest", password: "guest123", expected: "Guest Dashboard" },
];

// Method 1: for loop
for (const data of loginTestData) {
  test(`login as ${data.username}`, async ({ page }) => {
    await page.goto("http://localhost:3000/login");
    await page.getByLabel("Username").fill(data.username);
    await page.getByLabel("Password").fill(data.password);
    await page.getByRole("button", { name: "Login" }).click();
    await expect(page.getByTestId("title")).toHaveText(data.expected);
  });
}

// Method 2: forEach
["chrome", "firefox", "safari"].forEach((browser) => {
  test(`works on ${browser}`, async ({ page }) => {
    await page.goto("http://localhost:3000");
    await expect(page).toHaveTitle(/Library/);
  });
});

// Method 3: describe with data
test.describe("Search functionality", () => {
  const searchTerms = ["JavaScript", "Python", "React", "Node.js"];

  for (const term of searchTerms) {
    test(`search for "${term}"`, async ({ page }) => {
      await page.goto("http://localhost:3000/search");
      await page.getByPlaceholder("Search").fill(term);
      await page.keyboard.press("Enter");
      await expect(page.getByTestId("results")).toContainText(term);
    });
  }
});
```

## 🔰 Example 5.5: Test Configuration

```javascript
// playwright.config.js
const { defineConfig } = require("@playwright/test");

module.exports = defineConfig({
  // ที่เก็บ test files
  testDir: "./tests",

  // รัน tests แบบ parallel
  fullyParallel: true,

  // Fail build ถ้ามี test.only ค้างอยู่
  forbidOnly: !!process.env.CI,

  // Retry on failure
  retries: process.env.CI ? 2 : 0,

  // จำนวน workers
  workers: process.env.CI ? 1 : undefined,

  // Reporter
  reporter: [["html"], ["list"]],

  // Shared settings for all tests
  use: {
    // Base URL
    baseURL: "http://localhost:3000",

    // Trace on failure
    trace: "on-first-retry",

    // Screenshot on failure
    screenshot: "only-on-failure",

    // Video on failure
    video: "on-first-retry",
  },

  // Browser-specific settings
  projects: [
    {
      name: "chromium",
      use: { browserName: "chromium" },
    },
    {
      name: "firefox",
      use: { browserName: "firefox" },
    },
    {
      name: "webkit",
      use: { browserName: "webkit" },
    },
  ],

  // Run local server before tests
  webServer: {
    command: "npm run start",
    url: "http://localhost:3000",
    reuseExistingServer: !process.env.CI,
  },
});
```

## 📝 สรุป Level 5

| Feature     | คำสั่ง              | หน้าที่               |
| ----------- | ------------------- | --------------------- |
| Group       | `test.describe()`   | จัดกลุ่ม tests        |
| Before All  | `test.beforeAll()`  | รันก่อน tests ทั้งหมด |
| After All   | `test.afterAll()`   | รันหลัง tests ทั้งหมด |
| Before Each | `test.beforeEach()` | รันก่อนทุก test       |
| After Each  | `test.afterEach()`  | รันหลังทุก test       |
| Skip        | `test.skip()`       | ข้าม test             |
| Only        | `test.only()`       | รันเฉพาะ test นี้     |
| Fail        | `test.fail()`       | Expected to fail      |

---

# Level 6: Page Object Model

## 🎯 เป้าหมาย

- เข้าใจ POM Pattern
- สร้าง Page Classes
- ใช้งาน POM ใน Tests

## 🔰 Example 6.1: BasePage

```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 };
```

## 🔰 Example 6.2: LoginPage

```javascript
// pages/LoginPage.js
const { BasePage } = require("./BasePage");

class LoginPage extends BasePage {
  constructor(page) {
    super(page);

    // Locators - ประกาศไว้ที่เดียว
    this.usernameInput = page.getByLabel("Username");
    this.passwordInput = page.getByLabel("Password");
    this.loginButton = page.getByRole("button", { name: "Login" });
    this.rememberMeCheckbox = page.getByLabel("Remember me");
    this.errorMessage = page.getByTestId("error-message");
    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.login(username, password);
    await this.rememberMeCheckbox.check();
    await this.loginButton.click();
  }

  async getErrorMessage() {
    return await this.errorMessage.textContent();
  }

  async isErrorVisible() {
    return await this.errorMessage.isVisible();
  }
}

module.exports = { LoginPage };
```

## 🔰 Example 6.3: DashboardPage

```javascript
// pages/DashboardPage.js
const { BasePage } = require("./BasePage");

class DashboardPage extends BasePage {
  constructor(page) {
    super(page);

    // Header
    this.welcomeMessage = page.getByTestId("welcome-message");
    this.userMenu = page.getByTestId("user-menu");
    this.logoutButton = page.getByRole("button", { name: "Logout" });

    // Navigation
    this.booksLink = page.getByRole("link", { name: "Books" });
    this.profileLink = page.getByRole("link", { name: "Profile" });
    this.settingsLink = page.getByRole("link", { name: "Settings" });

    // Content
    this.recentBooks = page.getByTestId("recent-books");
    this.notifications = page.getByTestId("notifications");
  }

  async goto() {
    await this.navigate("/dashboard");
  }

  async getWelcomeText() {
    return await this.welcomeMessage.textContent();
  }

  async logout() {
    await this.userMenu.click();
    await this.logoutButton.click();
  }

  async goToBooks() {
    await this.booksLink.click();
  }

  async getNotificationCount() {
    const count = await this.notifications.count();
    return count;
  }
}

module.exports = { DashboardPage };
```

## 🔰 Example 6.4: BookSearchPage

```javascript
// pages/BookSearchPage.js
const { BasePage } = require("./BasePage");

class BookSearchPage extends BasePage {
  constructor(page) {
    super(page);

    // Search
    this.searchInput = page.getByPlaceholder("Search books...");
    this.searchButton = page.getByRole("button", { name: "Search" });
    this.categoryFilter = page.getByLabel("Category");
    this.sortSelect = page.getByLabel("Sort by");

    // Results
    this.bookCards = page.getByTestId("book-card");
    this.noResultsMessage = page.getByTestId("no-results");
    this.resultCount = page.getByTestId("result-count");
    this.loadingSpinner = page.getByTestId("loading");
  }

  async goto() {
    await this.navigate("/books");
  }

  async search(term) {
    await this.searchInput.fill(term);
    await this.searchButton.click();
    await this.waitForResults();
  }

  async searchWithFilter(term, category) {
    await this.searchInput.fill(term);
    await this.categoryFilter.selectOption(category);
    await this.searchButton.click();
    await this.waitForResults();
  }

  async waitForResults() {
    await this.loadingSpinner.waitFor({ state: "hidden" });
  }

  async getBookCount() {
    return await this.bookCards.count();
  }

  async getBookTitles() {
    const titles = [];
    const cards = await this.bookCards.all();
    for (const card of cards) {
      const title = await card.locator(".title").textContent();
      titles.push(title);
    }
    return titles;
  }

  async clickBook(index) {
    await this.bookCards.nth(index).click();
  }

  async sortBy(option) {
    await this.sortSelect.selectOption(option);
    await this.waitForResults();
  }
}

module.exports = { BookSearchPage };
```

## 🔰 Example 6.5: BookDetailPage (with Components)

```javascript
// pages/components/BookCard.js
class BookCard {
  constructor(locator) {
    this.locator = locator;
    this.title = locator.locator(".title");
    this.author = locator.locator(".author");
    this.borrowButton = locator.getByRole("button", { name: "Borrow" });
    this.reserveButton = locator.getByRole("button", { name: "Reserve" });
    this.availabilityBadge = locator.locator(".availability");
  }

  async getTitle() {
    return await this.title.textContent();
  }

  async getAuthor() {
    return await this.author.textContent();
  }

  async isAvailable() {
    const text = await this.availabilityBadge.textContent();
    return text === "Available";
  }

  async borrow() {
    await this.borrowButton.click();
  }

  async reserve() {
    await this.reserveButton.click();
  }
}

module.exports = { BookCard };
```

```javascript
// pages/BookDetailPage.js
const { BasePage } = require("./BasePage");

class BookDetailPage extends BasePage {
  constructor(page) {
    super(page);

    this.title = page.getByTestId("book-title");
    this.author = page.getByTestId("book-author");
    this.isbn = page.getByTestId("book-isbn");
    this.description = page.getByTestId("book-description");
    this.coverImage = page.getByTestId("book-cover");
    this.borrowButton = page.getByRole("button", { name: "Borrow" });
    this.reserveButton = page.getByRole("button", { name: "Reserve" });
    this.reviews = page.getByTestId("review-item");
    this.addReviewButton = page.getByRole("button", { name: "Add Review" });
  }

  async goto(bookId) {
    await this.navigate(`/books/${bookId}`);
  }

  async getBookInfo() {
    return {
      title: await this.title.textContent(),
      author: await this.author.textContent(),
      isbn: await this.isbn.textContent(),
      description: await this.description.textContent(),
    };
  }

  async borrowBook() {
    await this.borrowButton.click();
  }

  async getReviewCount() {
    return await this.reviews.count();
  }
}

module.exports = { BookDetailPage };
```

## 🔰 Example 6.6: ใช้ POM ใน Tests

```javascript
// tests/login.spec.js
const { test, expect } = require("@playwright/test");
const { LoginPage } = require("../pages/LoginPage");
const { DashboardPage } = require("../pages/DashboardPage");

test.describe("Login Tests with POM", () => {
  let loginPage;
  let dashboardPage;

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    dashboardPage = new DashboardPage(page);
  });

  test("should login successfully", async ({ page }) => {
    await loginPage.goto();
    await loginPage.login("admin", "admin123");

    await expect(page).toHaveURL("/dashboard");
    await expect(dashboardPage.welcomeMessage).toContainText("Welcome");
  });

  test("should show error for invalid credentials", async () => {
    await loginPage.goto();
    await loginPage.login("admin", "wrongpassword");

    await expect(loginPage.errorMessage).toBeVisible();
    const errorText = await loginPage.getErrorMessage();
    expect(errorText).toContain("Invalid");
  });

  test("should logout successfully", async ({ page }) => {
    await loginPage.goto();
    await loginPage.login("admin", "admin123");
    await dashboardPage.logout();

    await expect(page).toHaveURL("/login");
  });
});
```

```javascript
// tests/book-search.spec.js
const { test, expect } = require("@playwright/test");
const { LoginPage } = require("../pages/LoginPage");
const { BookSearchPage } = require("../pages/BookSearchPage");
const { BookDetailPage } = require("../pages/BookDetailPage");

test.describe("Book Search Tests", () => {
  let loginPage;
  let searchPage;
  let detailPage;

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    searchPage = new BookSearchPage(page);
    detailPage = new BookDetailPage(page);

    // Login before each test
    await loginPage.goto();
    await loginPage.login("user", "user123");
  });

  test("should search for books", async () => {
    await searchPage.goto();
    await searchPage.search("JavaScript");

    const count = await searchPage.getBookCount();
    expect(count).toBeGreaterThan(0);

    const titles = await searchPage.getBookTitles();
    titles.forEach((title) => {
      expect(title.toLowerCase()).toContain("javascript");
    });
  });

  test("should filter by category", async () => {
    await searchPage.goto();
    await searchPage.searchWithFilter("", "Programming");

    const count = await searchPage.getBookCount();
    expect(count).toBeGreaterThan(0);
  });

  test("should view book details", async () => {
    await searchPage.goto();
    await searchPage.search("JavaScript");
    await searchPage.clickBook(0);

    const bookInfo = await detailPage.getBookInfo();
    expect(bookInfo.title).toBeTruthy();
    expect(bookInfo.author).toBeTruthy();
  });
});
```

## 📝 สรุป Level 6: POM Structure

```
project/
├── pages/
│   ├── BasePage.js          # Base class
│   ├── LoginPage.js         # Login page
│   ├── DashboardPage.js     # Dashboard page
│   ├── BookSearchPage.js    # Book search page
│   ├── BookDetailPage.js    # Book detail page
│   └── components/
│       └── BookCard.js      # Reusable component
├── tests/
│   ├── login.spec.js
│   ├── book-search.spec.js
│   └── book-detail.spec.js
└── playwright.config.js
```

**ข้อดีของ POM:**

- ✅ Code reusable
- ✅ ง่ายต่อการ maintain
- ✅ Locators อยู่ที่เดียว
- ✅ Tests อ่านง่าย

---

# Level 7: Test Data & Fixtures

## 🎯 เป้าหมาย

- จัดการ Test Data
- สร้าง Custom Fixtures
- Authentication State

## 🔰 Example 7.1: Test Data Management

```javascript
// test-data/users.js
const users = {
  admin: {
    username: "admin",
    password: "admin123",
    email: "admin@library.com",
    role: "admin",
  },
  user: {
    username: "john_doe",
    password: "user123",
    email: "john@example.com",
    role: "user",
  },
  guest: {
    username: "guest",
    password: "guest123",
    email: "guest@example.com",
    role: "guest",
  },
};

module.exports = { users };
```

```javascript
// test-data/books.js
const books = {
  javascript: {
    id: 1,
    title: "JavaScript: The Good Parts",
    author: "Douglas Crockford",
    isbn: "978-0596517748",
    category: "Programming",
  },
  python: {
    id: 2,
    title: "Python Crash Course",
    author: "Eric Matthes",
    isbn: "978-1593279288",
    category: "Programming",
  },
  react: {
    id: 3,
    title: "Learning React",
    author: "Alex Banks",
    isbn: "978-1492051725",
    category: "Web Development",
  },
};

module.exports = { books };
```

## 🔰 Example 7.2: ใช้ Faker.js สร้าง Dynamic Data

```javascript
// test-data/generators.js
const { faker } = require("@faker-js/faker");

function generateUser() {
  return {
    username: faker.internet.userName(),
    email: faker.internet.email(),
    password: faker.internet.password({ length: 12 }),
    firstName: faker.person.firstName(),
    lastName: faker.person.lastName(),
    phone: faker.phone.number(),
  };
}

function generateBook() {
  return {
    title: faker.lorem.words(3),
    author: faker.person.fullName(),
    isbn: faker.string.numeric(13),
    description: faker.lorem.paragraph(),
    publishedYear: faker.date.past({ years: 20 }).getFullYear(),
    category: faker.helpers.arrayElement([
      "Programming",
      "Science",
      "Fiction",
      "History",
    ]),
  };
}

function generateReview() {
  return {
    rating: faker.number.int({ min: 1, max: 5 }),
    comment: faker.lorem.sentences(2),
    date: faker.date.recent(),
  };
}

// Generate multiple items
function generateUsers(count) {
  return Array.from({ length: count }, generateUser);
}

function generateBooks(count) {
  return Array.from({ length: count }, generateBook);
}

module.exports = {
  generateUser,
  generateBook,
  generateReview,
  generateUsers,
  generateBooks,
};
```

## 🔰 Example 7.3: ใช้ Test Data ใน Tests

```javascript
// tests/registration.spec.js
const { test, expect } = require("@playwright/test");
const { generateUser } = require("../test-data/generators");
const { users } = require("../test-data/users");

test.describe("User Registration", () => {
  test("should register with generated data", async ({ page }) => {
    const newUser = generateUser();

    await page.goto("/register");
    await page.getByLabel("Username").fill(newUser.username);
    await page.getByLabel("Email").fill(newUser.email);
    await page.getByLabel("Password").fill(newUser.password);
    await page.getByLabel("First Name").fill(newUser.firstName);
    await page.getByLabel("Last Name").fill(newUser.lastName);
    await page.getByRole("button", { name: "Register" }).click();

    await expect(page).toHaveURL("/login");
    await expect(page.getByTestId("success-message")).toBeVisible();
  });

  test("should reject duplicate email", async ({ page }) => {
    const existingUser = users.admin;

    await page.goto("/register");
    await page.getByLabel("Username").fill("newuser");
    await page.getByLabel("Email").fill(existingUser.email); // Duplicate!
    await page.getByLabel("Password").fill("password123");
    await page.getByRole("button", { name: "Register" }).click();

    await expect(page.getByTestId("error")).toContainText(
      "Email already exists",
    );
  });
});
```

## 🔰 Example 7.4: Custom Fixtures

```javascript
// fixtures/index.js
const { test as base, expect } = require('@playwright/test');
const { LoginPage } = require('../pages/LoginPage');
const { DashboardPage } = require('../pages/DashboardPage');
const { BookSearchPage } = require('../pages/BookSearchPage');
const { users } = require('../test-data/users');

// 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);
  },

  dashboardPage: async ({ page }, use) => {
    const dashboardPage = new DashboardPage(page);
    await use(dashboardPage);
  },

  bookSearchPage: async ({ page }, use) => {
    const bookSearchPage = new BookSearchPage(page);
    await use(bookSearchPage);
  },

  // Logged-in user fixture
  loggedInPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page);
    await loginPage.goto();
    await loginPage.login(users.user.username, users.user.password);
    await page.waitForURL('/dashboard');
    await use(page);
  },

  // Admin user fixture
  adminPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page);
    await loginPage.goto();
    await loginPage.login(users.admin.username, users.admin.password);
    await page.waitForURL('/admin');
    await use(page);
  },

  // API helper fixture
  apiHelper: async ({ request }, use) => {
    const helper = {
      async createBook(bookData) {
        const response = await request.post('/api/books', { data: bookData });
        return response.json();
      },
      async deleteBook(bookId) {
        await request.delete(`/api/books/${bookId}`);
      },
      async getBooks() {
        const response = await request.get('/api/books');
        return response.json();
      }
    };
    await use(helper);
  }
});

module.exports = { test, expect };
```

## 🔰 Example 7.5: ใช้ Custom Fixtures

```javascript
// tests/with-fixtures.spec.js
const { test, expect } = require("../fixtures");
const { generateBook } = require("../test-data/generators");

test.describe("Tests with Custom Fixtures", () => {
  // ใช้ Page Object fixtures
  test("login test with fixtures", async ({ loginPage, dashboardPage }) => {
    await loginPage.goto();
    await loginPage.login("admin", "admin123");

    const welcome = await dashboardPage.getWelcomeText();
    expect(welcome).toContain("Welcome");
  });

  // ใช้ logged-in fixture (ไม่ต้อง login เอง)
  test("search books as logged-in user", async ({
    loggedInPage,
    bookSearchPage,
  }) => {
    await bookSearchPage.goto();
    await bookSearchPage.search("JavaScript");

    const count = await bookSearchPage.getBookCount();
    expect(count).toBeGreaterThan(0);
  });

  // ใช้ admin fixture
  test("admin can create book", async ({ adminPage }) => {
    await adminPage.goto("/admin/books/new");
    // ...
  });

  // ใช้ API helper fixture
  test("create and verify book", async ({
    loggedInPage,
    bookSearchPage,
    apiHelper,
  }) => {
    // สร้าง book ผ่าน API
    const newBook = generateBook();
    const createdBook = await apiHelper.createBook(newBook);

    // ค้นหา book ที่สร้าง
    await bookSearchPage.goto();
    await bookSearchPage.search(newBook.title);

    const count = await bookSearchPage.getBookCount();
    expect(count).toBeGreaterThan(0);

    // Cleanup
    await apiHelper.deleteBook(createdBook.id);
  });
});
```

## 🔰 Example 7.6: Authentication State (Reuse Login)

```javascript
// auth.setup.js - สร้าง auth state ก่อนรัน tests
const { test as setup, expect } = require('@playwright/test');
const { users } = require('./test-data/users');

const authFile = 'playwright/.auth/user.json';

setup('authenticate', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Username').fill(users.user.username);
  await page.getByLabel('Password').fill(users.user.password);
  await page.getByRole('button', { name: 'Login' }).click();

  // รอจน login สำเร็จ
  await page.waitForURL('/dashboard');

  // บันทึก storage state (cookies, localStorage)
  await page.context().storageState({ path: authFile });
});
```

```javascript
// playwright.config.js
const { defineConfig } = require("@playwright/test");

module.exports = defineConfig({
  projects: [
    // Setup project - รัน authentication
    {
      name: "setup",
      testMatch: /.*\.setup\.js/,
    },

    // Tests ที่ต้อง login
    {
      name: "chromium",
      use: {
        browserName: "chromium",
        // ใช้ storage state จาก setup
        storageState: "playwright/.auth/user.json",
      },
      dependencies: ["setup"], // รัน setup ก่อน
    },

    // Tests ที่ไม่ต้อง login
    {
      name: "unauthenticated",
      use: {
        browserName: "chromium",
      },
      testMatch: "**/public/**/*.spec.js",
    },
  ],
});
```

## 📝 สรุป Level 7

| Feature          | ใช้เมื่อไหร่          |
| ---------------- | --------------------- |
| Static Test Data | ข้อมูลคงที่ที่ใช้บ่อย |
| Faker.js         | ข้อมูลแบบ random      |
| Custom Fixtures  | Reuse setup code      |
| Storage State    | Reuse authentication  |
| API Helper       | Setup/Cleanup data    |

---

# Level 8: Cross-Browser Testing

## 🎯 เป้าหมาย

- ทดสอบหลาย browsers
- ทดสอบ responsive
- ทดสอบ mobile

## 🔰 Example 8.1: Browser Configuration

```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"] },
    },

    // Tablet
    {
      name: "iPad",
      use: { ...devices["iPad Pro 11"] },
    },
  ],
});
```

## 🔰 Example 8.2: Browser-Specific Tests

```javascript
// tests/cross-browser.spec.js
const { test, expect } = require("@playwright/test");

test.describe("Cross-Browser Tests", () => {
  test("homepage works on all browsers", async ({ page }) => {
    await page.goto("/");
    await expect(page.getByTestId("header")).toBeVisible();
    await expect(page.getByTestId("main-content")).toBeVisible();
    await expect(page.getByTestId("footer")).toBeVisible();
  });

  test("login works on all browsers", async ({ page }) => {
    await page.goto("/login");
    await page.getByLabel("Username").fill("user");
    await page.getByLabel("Password").fill("password");
    await page.getByRole("button", { name: "Login" }).click();
    await expect(page).toHaveURL("/dashboard");
  });

  // Test เฉพาะ browser
  test("webkit-specific feature", async ({ page, browserName }) => {
    test.skip(browserName !== "webkit", "WebKit only");
    // test code...
  });

  test("skip on firefox", async ({ page, browserName }) => {
    test.skip(browserName === "firefox", "Known issue on Firefox");
    // test code...
  });
});
```

## 🔰 Example 8.3: Responsive Testing

```javascript
// tests/responsive.spec.js
const { test, expect } = require("@playwright/test");

const viewports = [
  { name: "mobile", width: 375, height: 667 }, // iPhone SE
  { name: "tablet", width: 768, height: 1024 }, // iPad
  { name: "desktop", width: 1920, height: 1080 }, // Full HD
];

test.describe("Responsive Design", () => {
  for (const viewport of viewports) {
    test(`layout on ${viewport.name}`, async ({ page }) => {
      await page.setViewportSize({
        width: viewport.width,
        height: viewport.height,
      });

      await page.goto("/");

      if (viewport.name === "mobile") {
        // Mobile: hamburger menu ต้องเห็น
        await expect(page.getByTestId("hamburger-menu")).toBeVisible();
        await expect(page.getByTestId("desktop-nav")).not.toBeVisible();
      } else {
        // Desktop/Tablet: desktop nav ต้องเห็น
        await expect(page.getByTestId("desktop-nav")).toBeVisible();
      }
    });
  }

  test("mobile menu toggle", async ({ page }) => {
    await page.setViewportSize({ width: 375, height: 667 });
    await page.goto("/");

    // Menu ปิดอยู่
    await expect(page.getByTestId("mobile-menu")).not.toBeVisible();

    // เปิด menu
    await page.getByTestId("hamburger-menu").click();
    await expect(page.getByTestId("mobile-menu")).toBeVisible();

    // ปิด menu
    await page.getByTestId("hamburger-menu").click();
    await expect(page.getByTestId("mobile-menu")).not.toBeVisible();
  });
});
```

## 🔰 Example 8.4: Device Emulation

```javascript
// tests/device-emulation.spec.js
const { test, expect, devices } = require("@playwright/test");

test.describe("Device Emulation", () => {
  test.use(devices["iPhone 13 Pro"]);

  test("mobile experience on iPhone", async ({ page }) => {
    await page.goto("/");

    // Touch events work
    await page.tap(".book-card");

    // Swipe gesture
    await page.touchscreen.tap(100, 100);
  });
});

// หรือกำหนดในแต่ละ test
test("tablet experience", async ({ browser }) => {
  const context = await browser.newContext({
    ...devices["iPad Pro 11"],
    locale: "th-TH",
    timezoneId: "Asia/Bangkok",
  });

  const page = await context.newPage();
  await page.goto("/");

  // Test tablet-specific behavior

  await context.close();
});
```

## 📝 สรุป Level 8

| รัน Command                                     | หน้าที่         |
| ----------------------------------------------- | --------------- |
| `npx playwright test`                           | รันทุก browsers |
| `npx playwright test --project=chromium`        | รันเฉพาะ Chrome |
| `npx playwright test --project="Mobile Safari"` | รันเฉพาะ iPhone |

---

# Level 9: Visual Regression Testing

## 🎯 เป้าหมาย

- Screenshot comparison
- จัดการ baselines
- Handle dynamic content

## 🔰 Example 9.1: Basic Screenshot Testing

```javascript
// tests/visual/basic.spec.js
const { test, expect } = require("@playwright/test");

test.describe("Visual Regression", () => {
  test("homepage screenshot", async ({ page }) => {
    await page.goto("/");

    // เปรียบเทียบ full page
    await expect(page).toHaveScreenshot("homepage.png");
  });

  test("login page screenshot", async ({ page }) => {
    await page.goto("/login");

    // ตั้งค่า tolerance
    await expect(page).toHaveScreenshot("login.png", {
      maxDiffPixels: 100, // ยอมให้ต่างได้ 100 pixels
    });
  });

  test("element screenshot", async ({ page }) => {
    await page.goto("/");

    // Screenshot เฉพาะ element
    const header = page.getByTestId("header");
    await expect(header).toHaveScreenshot("header.png");

    const footer = page.getByTestId("footer");
    await expect(footer).toHaveScreenshot("footer.png");
  });
});
```

## 🔰 Example 9.2: Handle Dynamic Content

```javascript
// tests/visual/dynamic.spec.js
const { test, expect } = require("@playwright/test");

test.describe("Visual - Dynamic Content", () => {
  test("mask dynamic elements", async ({ page }) => {
    await page.goto("/");

    await expect(page).toHaveScreenshot("homepage-masked.png", {
      // ซ่อน elements ที่เปลี่ยนแปลง
      mask: [
        page.getByTestId("timestamp"),
        page.getByTestId("random-ad"),
        page.getByTestId("user-avatar"),
      ],
    });
  });

  test("hide animations", async ({ page }) => {
    await page.goto("/");

    // หยุด animations และ CSS transitions
    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("no-animation.png");
  });

  test("wait for fonts", async ({ page }) => {
    await page.goto("/");

    // รอ fonts โหลดเสร็จ
    await page.waitForFunction(() => document.fonts.ready);

    await expect(page).toHaveScreenshot("with-fonts.png");
  });

  test("freeze date/time", async ({ page }) => {
    // Mock เวลา
    await page.addInitScript(() => {
      const fixedDate = new Date("2024-01-15T10:00:00");
      const OriginalDate = Date;

      window.Date = class extends OriginalDate {
        constructor(...args) {
          if (args.length === 0) {
            return fixedDate;
          }
          return new OriginalDate(...args);
        }
        static now() {
          return fixedDate.getTime();
        }
      };
    });

    await page.goto("/");
    await expect(page).toHaveScreenshot("frozen-time.png");
  });
});
```

## 🔰 Example 9.3: Responsive Visual Testing

```javascript
// tests/visual/responsive.spec.js
const { test, expect } = require("@playwright/test");

const viewports = {
  mobile: { width: 375, height: 667 },
  tablet: { width: 768, height: 1024 },
  desktop: { width: 1920, height: 1080 },
};

test.describe("Responsive Visual Tests", () => {
  for (const [name, size] of Object.entries(viewports)) {
    test(`homepage on ${name}`, async ({ page }) => {
      await page.setViewportSize(size);
      await page.goto("/");

      await expect(page).toHaveScreenshot(`homepage-${name}.png`);
    });

    test(`search results on ${name}`, async ({ page }) => {
      await page.setViewportSize(size);
      await page.goto("/books?search=javascript");

      await expect(page).toHaveScreenshot(`search-${name}.png`, {
        fullPage: true, // Capture full scrollable page
      });
    });
  }
});
```

## 🔰 Example 9.4: Theme Testing (Dark Mode)

```javascript
// tests/visual/themes.spec.js
const { test, expect } = require("@playwright/test");

test.describe("Theme Visual Tests", () => {
  test("light mode", async ({ page }) => {
    await page.goto("/");
    await expect(page).toHaveScreenshot("theme-light.png");
  });

  test("dark mode", async ({ page }) => {
    await page.goto("/");

    // Toggle dark mode
    await page.getByTestId("theme-toggle").click();

    await expect(page).toHaveScreenshot("theme-dark.png");
  });

  test("system preference - dark", async ({ browser }) => {
    const context = await browser.newContext({
      colorScheme: "dark",
    });
    const page = await context.newPage();

    await page.goto("/");
    await expect(page).toHaveScreenshot("system-dark.png");

    await context.close();
  });

  test("system preference - light", async ({ browser }) => {
    const context = await browser.newContext({
      colorScheme: "light",
    });
    const page = await context.newPage();

    await page.goto("/");
    await expect(page).toHaveScreenshot("system-light.png");

    await context.close();
  });
});
```

## 🔰 Example 9.5: Update Baseline Screenshots

```bash
# Update all snapshots
npx playwright test --update-snapshots

# Update specific test
npx playwright test visual.spec.js --update-snapshots

# Review changes
npx playwright show-report
```

## 📝 สรุป Level 9

| Option               | หน้าที่                       |
| -------------------- | ----------------------------- |
| `maxDiffPixels`      | จำนวน pixels ที่ยอมให้ต่าง    |
| `maxDiffPixelRatio`  | สัดส่วน pixels ที่ยอมให้ต่าง  |
| `mask`               | ซ่อน elements ที่เปลี่ยนแปลง  |
| `fullPage`           | Capture ทั้งหน้า (รวม scroll) |
| `--update-snapshots` | อัพเดท baseline               |

---

# Level 10: Advanced Techniques

## 🎯 เป้าหมาย

- Network interception
- Multiple tabs/windows
- Geolocation & permissions

## 🔰 Example 10.1: Network Interception (Mock API)

```javascript
// tests/advanced/network.spec.js
const { test, expect } = require("@playwright/test");

test.describe("Network Interception", () => {
  test("mock API response", async ({ page }) => {
    // Mock GET /api/books
    await page.route("**/api/books", (route) => {
      route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify([
          { id: 1, title: "Mocked Book 1" },
          { id: 2, title: "Mocked Book 2" },
        ]),
      });
    });

    await page.goto("/books");

    // ตรวจสอบว่าแสดง mocked data
    await expect(page.getByTestId("book-card")).toHaveCount(2);
    await expect(page.getByTestId("book-card").first()).toContainText(
      "Mocked Book 1",
    );
  });

  test("simulate API error", async ({ page }) => {
    // Mock error response
    await page.route("**/api/books", (route) => {
      route.fulfill({
        status: 500,
        body: JSON.stringify({ error: "Internal Server Error" }),
      });
    });

    await page.goto("/books");

    // ตรวจสอบ error handling
    await expect(page.getByTestId("error-message")).toBeVisible();
    await expect(page.getByTestId("error-message")).toContainText(
      "Error loading books",
    );
  });

  test("simulate slow network", async ({ page }) => {
    await page.route("**/api/books", async (route) => {
      await new Promise((resolve) => setTimeout(resolve, 3000)); // delay 3 seconds
      await route.continue();
    });

    await page.goto("/books");

    // Loading state ต้องเห็น
    await expect(page.getByTestId("loading")).toBeVisible();

    // รอจน load เสร็จ
    await expect(page.getByTestId("loading")).not.toBeVisible({
      timeout: 5000,
    });
  });

  test("modify request", async ({ page }) => {
    await page.route("**/api/books", (route) => {
      const headers = {
        ...route.request().headers(),
        "X-Custom-Header": "test-value",
      };
      route.continue({ headers });
    });

    await page.goto("/books");
  });

  test("intercept and modify response", async ({ page }) => {
    await page.route("**/api/user", async (route) => {
      const response = await route.fetch();
      const json = await response.json();

      // Modify response
      json.name = "Modified Name";
      json.role = "admin";

      await route.fulfill({
        response,
        body: JSON.stringify(json),
      });
    });

    await page.goto("/profile");
    await expect(page.getByTestId("user-name")).toHaveText("Modified Name");
  });
});
```

## 🔰 Example 10.2: Wait for API Responses

```javascript
// tests/advanced/wait-api.spec.js
const { test, expect } = require("@playwright/test");

test.describe("Wait for API", () => {
  test("wait for specific response", async ({ page }) => {
    // สร้าง promise รอ response
    const responsePromise = page.waitForResponse("**/api/books");

    await page.goto("/books");

    // รอ response และตรวจสอบ
    const response = await responsePromise;
    expect(response.status()).toBe(200);

    const data = await response.json();
    expect(data.length).toBeGreaterThan(0);
  });

  test("wait for multiple responses", async ({ page }) => {
    const [booksResponse, userResponse] = await Promise.all([
      page.waitForResponse("**/api/books"),
      page.waitForResponse("**/api/user"),
      page.goto("/dashboard"),
    ]);

    expect(booksResponse.status()).toBe(200);
    expect(userResponse.status()).toBe(200);
  });

  test("wait for request to be made", async ({ page }) => {
    await page.goto("/books");

    // รอจนมี request ไปยัง search endpoint
    const requestPromise = page.waitForRequest("**/api/books/search*");

    await page.getByPlaceholder("Search").fill("JavaScript");
    await page.getByRole("button", { name: "Search" }).click();

    const request = await requestPromise;
    expect(request.url()).toContain("q=JavaScript");
  });
});
```

## 🔰 Example 10.3: Multiple Tabs/Windows

```javascript
// tests/advanced/multi-tab.spec.js
const { test, expect } = require("@playwright/test");

test.describe("Multiple Tabs", () => {
  test("handle new tab", async ({ page, context }) => {
    await page.goto("/");

    // รอ popup (new tab)
    const pagePromise = context.waitForEvent("page");
    await page.getByRole("link", { name: "Open in new tab" }).click();
    const newPage = await pagePromise;

    // รอ new tab load
    await newPage.waitForLoadState();

    // ทำงานกับ new tab
    await expect(newPage).toHaveTitle(/New Page/);
    await expect(newPage.getByTestId("content")).toBeVisible();

    // กลับไป tab เดิม
    await page.bringToFront();
    await expect(page).toHaveTitle(/Home/);
  });

  test("work with multiple tabs", async ({ context }) => {
    // สร้างหลาย pages
    const page1 = await context.newPage();
    const page2 = await context.newPage();
    const page3 = await context.newPage();

    // ทำงานพร้อมกัน
    await Promise.all([
      page1.goto("/page1"),
      page2.goto("/page2"),
      page3.goto("/page3"),
    ]);

    // ตรวจสอบแต่ละ page
    await expect(page1.getByTestId("title")).toHaveText("Page 1");
    await expect(page2.getByTestId("title")).toHaveText("Page 2");
    await expect(page3.getByTestId("title")).toHaveText("Page 3");
  });

  test("handle popup window", async ({ page }) => {
    await page.goto("/");

    // Handle popup
    const [popup] = await Promise.all([
      page.waitForEvent("popup"),
      page.getByRole("button", { name: "Open Popup" }).click(),
    ]);

    await popup.waitForLoadState();
    await expect(popup).toHaveTitle(/Popup/);

    // ปิด popup
    await popup.close();
  });
});
```

## 🔰 Example 10.4: Geolocation & Permissions

```javascript
// tests/advanced/geolocation.spec.js
const { test, expect } = require("@playwright/test");

test.describe("Geolocation", () => {
  test("mock location - Bangkok", async ({ browser }) => {
    const context = await browser.newContext({
      geolocation: {
        latitude: 13.7563,
        longitude: 100.5018,
      },
      permissions: ["geolocation"],
    });

    const page = await context.newPage();
    await page.goto("/nearby-libraries");

    // ตรวจสอบว่าแสดง libraries ใกล้ Bangkok
    await expect(page.getByTestId("location")).toContainText("Bangkok");

    await context.close();
  });

  test("change location during test", async ({ page, context }) => {
    await context.grantPermissions(["geolocation"]);

    // เริ่มที่ Bangkok
    await context.setGeolocation({ latitude: 13.7563, longitude: 100.5018 });
    await page.goto("/nearby");
    await expect(page.getByTestId("city")).toHaveText("Bangkok");

    // เปลี่ยนไป Chiang Mai
    await context.setGeolocation({ latitude: 18.7883, longitude: 98.9853 });
    await page.reload();
    await expect(page.getByTestId("city")).toHaveText("Chiang Mai");
  });
});

test.describe("Permissions", () => {
  test("allow notifications", async ({ browser }) => {
    const context = await browser.newContext({
      permissions: ["notifications"],
    });

    const page = await context.newPage();
    await page.goto("/notifications");

    // No permission prompt should appear
    await expect(page.getByTestId("notification-status")).toHaveText("Enabled");

    await context.close();
  });

  test("deny camera", async ({ browser }) => {
    const context = await browser.newContext({
      permissions: [], // ไม่ให้ permissions ใดๆ
    });

    const page = await context.newPage();
    await page.goto("/video-chat");

    await expect(page.getByTestId("camera-error")).toBeVisible();

    await context.close();
  });
});
```

## 🔰 Example 10.5: Downloads & Uploads

```javascript
// tests/advanced/files.spec.js
const { test, expect } = require("@playwright/test");
const fs = require("fs");
const path = require("path");

test.describe("File Handling", () => {
  test("download file", async ({ page }) => {
    await page.goto("/reports");

    // รอ download
    const downloadPromise = page.waitForEvent("download");
    await page.getByRole("button", { name: "Download Report" }).click();
    const download = await downloadPromise;

    // ตรวจสอบ filename
    expect(download.suggestedFilename()).toBe("report.pdf");

    // บันทึกไฟล์
    const filePath = path.join("downloads", download.suggestedFilename());
    await download.saveAs(filePath);

    // ตรวจสอบว่าไฟล์มีอยู่
    expect(fs.existsSync(filePath)).toBeTruthy();

    // Cleanup
    fs.unlinkSync(filePath);
  });

  test("upload multiple files", async ({ page }) => {
    await page.goto("/upload");

    const files = [
      "test-files/doc1.pdf",
      "test-files/doc2.pdf",
      "test-files/image.png",
    ];

    await page.getByLabel("Files").setInputFiles(files);
    await page.getByRole("button", { name: "Upload" }).click();

    // ตรวจสอบว่า upload สำเร็จ
    await expect(page.getByTestId("upload-success")).toBeVisible();
    await expect(page.getByTestId("file-count")).toHaveText("3 files uploaded");
  });
});
```

## 📝 สรุป Level 10

| Feature                         | ใช้ทำอะไร                |
| ------------------------------- | ------------------------ |
| `page.route()`                  | Mock/modify API requests |
| `page.waitForResponse()`        | รอ API response          |
| `context.newPage()`             | สร้าง new tab            |
| `page.waitForEvent('popup')`    | Handle popup             |
| `context.setGeolocation()`      | Mock location            |
| `page.waitForEvent('download')` | Handle downloads         |

---

# Level 11: CI/CD Integration

## 🎯 เป้าหมาย

- รัน tests ใน GitHub Actions
- Generate reports
- Parallel execution

## 🔰 Example 11.1: GitHub Actions Workflow

```yaml
# .github/workflows/playwright.yml
name: Playwright Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 18

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright Browsers
        run: npx playwright install --with-deps

      - name: Run Playwright tests
        run: npx playwright test

      - name: Upload report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30
```

## 🔰 Example 11.2: Advanced CI Configuration

```yaml
# .github/workflows/playwright-full.yml
name: Full Test Suite

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    - cron: "0 0 * * *" # Run daily at midnight

jobs:
  # Lint and Type Check
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 18
      - run: npm ci
      - run: npm run lint

  # Unit Tests
  unit:
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 18
      - run: npm ci
      - run: npm run test:unit

  # E2E Tests - Sharded
  e2e:
    runs-on: ubuntu-latest
    needs: unit
    strategy:
      fail-fast: false
      matrix:
        shard: [1, 2, 3, 4] # 4 parallel shards

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 18

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright
        run: npx playwright install --with-deps

      - name: Run tests (Shard ${{ matrix.shard }}/4)
        run: npx playwright test --shard=${{ matrix.shard }}/4

      - name: Upload blob report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: blob-report-${{ matrix.shard }}
          path: blob-report/
          retention-days: 1

  # Merge reports
  merge-reports:
    needs: e2e
    runs-on: ubuntu-latest
    if: always()

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 18

      - run: npm ci

      - name: Download blob reports
        uses: actions/download-artifact@v4
        with:
          path: all-blob-reports
          pattern: blob-report-*
          merge-multiple: true

      - name: Merge reports
        run: npx playwright merge-reports --reporter html ./all-blob-reports

      - name: Upload HTML report
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30
```

## 🔰 Example 11.3: CI Configuration for Playwright

```javascript
// playwright.config.js - CI optimized
const { defineConfig } = require("@playwright/test");

module.exports = defineConfig({
  testDir: "./tests",

  // CI-specific settings
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 4 : undefined,

  // Reporters
  reporter: process.env.CI
    ? [
        ["blob"], // For sharding
        ["github"], // GitHub annotations
        ["html", { open: "never" }],
      ]
    : [["html"], ["list"]],

  use: {
    baseURL: process.env.BASE_URL || "http://localhost:3000",

    // Traces and screenshots for debugging
    trace: "on-first-retry",
    screenshot: "only-on-failure",
    video: "on-first-retry",
  },

  // Web server
  webServer: {
    command: "npm run start",
    url: "http://localhost:3000",
    reuseExistingServer: !process.env.CI,
    timeout: 120000,
  },

  projects: [
    {
      name: "chromium",
      use: { browserName: "chromium" },
    },
    // Add more browsers for full coverage
    ...(process.env.CI
      ? [
          {
            name: "firefox",
            use: { browserName: "firefox" },
          },
          {
            name: "webkit",
            use: { browserName: "webkit" },
          },
        ]
      : []),
  ],
});
```

## 🔰 Example 11.4: Docker Integration

```dockerfile
# Dockerfile.test
FROM mcr.microsoft.com/playwright:v1.40.0-jammy

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

CMD ["npx", "playwright", "test"]
```

```yaml
# docker-compose.test.yml
version: "3.8"
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=test

  tests:
    build:
      context: .
      dockerfile: Dockerfile.test
    depends_on:
      - app
    environment:
      - BASE_URL=http://app:3000
    volumes:
      - ./playwright-report:/app/playwright-report
      - ./test-results:/app/test-results
```

```bash
# Run tests in Docker
docker-compose -f docker-compose.test.yml up --build --abort-on-container-exit
```

## 📝 สรุป Level 11

| Command             | หน้าที่                  |
| ------------------- | ------------------------ |
| `--shard=1/4`       | แบ่งรัน tests (parallel) |
| `merge-reports`     | รวม reports จาก shards   |
| `--reporter=github` | GitHub annotations       |
| `--reporter=blob`   | สำหรับ sharding          |

---

# แบบฝึกหัดรวม: Library Management System

## 🎯 Project Overview

สร้าง complete test suite สำหรับ Library Management System

## 📁 Project Structure

```
library-testing/
├── pages/
│   ├── BasePage.js
│   ├── LoginPage.js
│   ├── DashboardPage.js
│   ├── BookSearchPage.js
│   ├── BookDetailPage.js
│   ├── MyBooksPage.js
│   └── components/
│       └── BookCard.js
├── test-data/
│   ├── users.js
│   ├── books.js
│   └── generators.js
├── fixtures/
│   └── index.js
├── tests/
│   ├── auth/
│   │   ├── login.spec.js
│   │   └── register.spec.js
│   ├── books/
│   │   ├── search.spec.js
│   │   ├── detail.spec.js
│   │   └── borrow.spec.js
│   ├── visual/
│   │   └── screenshots.spec.js
│   └── api/
│       └── books-api.spec.js
├── playwright.config.js
└── package.json
```

## 📝 แบบฝึกหัดที่ 1: Authentication Tests

```javascript
// tests/auth/login.spec.js
const { test, expect } = require("@playwright/test");
const { LoginPage } = require("../../pages/LoginPage");
const { DashboardPage } = require("../../pages/DashboardPage");
const { users } = require("../../test-data/users");

test.describe("Login Feature", () => {
  let loginPage;
  let dashboardPage;

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    dashboardPage = new DashboardPage(page);
    await loginPage.goto();
  });

  test("TC-001: Login with valid credentials", async ({ page }) => {
    await loginPage.login(users.user.username, users.user.password);

    await expect(page).toHaveURL("/dashboard");
    await expect(dashboardPage.welcomeMessage).toContainText("Welcome");
  });

  test("TC-002: Show error for invalid password", async () => {
    await loginPage.login(users.user.username, "wrongpassword");

    await expect(loginPage.errorMessage).toBeVisible();
    await expect(loginPage.errorMessage).toContainText("Invalid credentials");
  });

  test("TC-003: Show error for empty fields", async () => {
    await loginPage.loginButton.click();

    await expect(loginPage.usernameInput).toHaveAttribute(
      "aria-invalid",
      "true",
    );
  });

  test("TC-004: Remember me functionality", async ({ page }) => {
    await loginPage.usernameInput.fill(users.user.username);
    await loginPage.passwordInput.fill(users.user.password);
    await loginPage.rememberMeCheckbox.check();
    await loginPage.loginButton.click();

    await expect(page).toHaveURL("/dashboard");

    // Close and reopen browser would test remember me
    // For now, just verify checkbox was checked
    // In real test, would verify session persistence
  });
});
```

## 📝 แบบฝึกหัดที่ 2: Book Search Tests

```javascript
// tests/books/search.spec.js
const { test, expect } = require("../../fixtures");
const { books } = require("../../test-data/books");

test.describe("Book Search", () => {
  test.beforeEach(async ({ loggedInPage, bookSearchPage }) => {
    await bookSearchPage.goto();
  });

  test("TC-010: Search by title", async ({ bookSearchPage }) => {
    await bookSearchPage.search("JavaScript");

    const count = await bookSearchPage.getBookCount();
    expect(count).toBeGreaterThan(0);

    const titles = await bookSearchPage.getBookTitles();
    titles.forEach((title) => {
      expect(title.toLowerCase()).toContain("javascript");
    });
  });

  test("TC-011: Search with no results", async ({ bookSearchPage }) => {
    await bookSearchPage.search("xyznonexistentbook");

    await expect(bookSearchPage.noResultsMessage).toBeVisible();
    await expect(bookSearchPage.bookCards).toHaveCount(0);
  });

  test("TC-012: Filter by category", async ({ bookSearchPage }) => {
    await bookSearchPage.searchWithFilter("", "Programming");

    const count = await bookSearchPage.getBookCount();
    expect(count).toBeGreaterThan(0);
  });

  test("TC-013: Sort results", async ({ bookSearchPage }) => {
    await bookSearchPage.search("book");
    await bookSearchPage.sortBy("title-asc");

    const titles = await bookSearchPage.getBookTitles();
    const sortedTitles = [...titles].sort();
    expect(titles).toEqual(sortedTitles);
  });
});
```

## 📝 แบบฝึกหัดที่ 3: E2E User Journey

```javascript
// tests/e2e/complete-journey.spec.js
const { test, expect } = require("@playwright/test");
const { LoginPage } = require("../../pages/LoginPage");
const { BookSearchPage } = require("../../pages/BookSearchPage");
const { BookDetailPage } = require("../../pages/BookDetailPage");
const { MyBooksPage } = require("../../pages/MyBooksPage");
const { users } = require("../../test-data/users");

test.describe("Complete User Journey", () => {
  test("User can search, borrow, and return a book", async ({ page }) => {
    const loginPage = new LoginPage(page);
    const searchPage = new BookSearchPage(page);
    const detailPage = new BookDetailPage(page);
    const myBooksPage = new MyBooksPage(page);

    // Step 1: Login
    await test.step("Login", async () => {
      await loginPage.goto();
      await loginPage.login(users.user.username, users.user.password);
      await expect(page).toHaveURL("/dashboard");
    });

    // Step 2: Search for a book
    await test.step("Search for book", async () => {
      await searchPage.goto();
      await searchPage.search("JavaScript");
      const count = await searchPage.getBookCount();
      expect(count).toBeGreaterThan(0);
    });

    // Step 3: View book details
    await test.step("View book details", async () => {
      await searchPage.clickBook(0);
      const bookInfo = await detailPage.getBookInfo();
      expect(bookInfo.title).toBeTruthy();
    });

    // Step 4: Borrow the book
    await test.step("Borrow book", async () => {
      await detailPage.borrowBook();
      await expect(page.getByTestId("success-message")).toContainText(
        "borrowed",
      );
    });

    // Step 5: Verify in My Books
    await test.step("Verify in My Books", async () => {
      await myBooksPage.goto();
      const borrowedBooks = await myBooksPage.getBorrowedBooks();
      expect(borrowedBooks.length).toBeGreaterThan(0);
    });

    // Step 6: Return the book
    await test.step("Return book", async () => {
      await myBooksPage.returnBook(0);
      await expect(page.getByTestId("success-message")).toContainText(
        "returned",
      );
    });
  });
});
```

## 📝 แบบฝึกหัดที่ 4: Visual Regression Tests

```javascript
// tests/visual/library-visual.spec.js
const { test, expect } = require("@playwright/test");

test.describe("Visual Regression - Library System", () => {
  test("Homepage", async ({ page }) => {
    await page.goto("/");
    await expect(page).toHaveScreenshot("homepage.png");
  });

  test("Login Page", async ({ page }) => {
    await page.goto("/login");
    await expect(page).toHaveScreenshot("login-page.png");
  });

  test("Book Search Results", async ({ page }) => {
    await page.goto("/books?search=javascript");
    await page.waitForSelector('[data-testid="book-card"]');

    await expect(page).toHaveScreenshot("search-results.png", {
      mask: [page.getByTestId("timestamp")],
    });
  });

  test("Responsive - Mobile", async ({ page }) => {
    await page.setViewportSize({ width: 375, height: 667 });
    await page.goto("/");
    await expect(page).toHaveScreenshot("homepage-mobile.png");
  });

  test("Responsive - Tablet", async ({ page }) => {
    await page.setViewportSize({ width: 768, height: 1024 });
    await page.goto("/");
    await expect(page).toHaveScreenshot("homepage-tablet.png");
  });
});
```

## 📝 แบบฝึกหัดที่ 5: API Testing with UI

```javascript
// tests/api/books-api.spec.js
const { test, expect } = require("@playwright/test");

test.describe("Books API Integration", () => {
  test("Mock empty book list", async ({ page }) => {
    await page.route("**/api/books", (route) => {
      route.fulfill({
        status: 200,
        body: JSON.stringify([]),
      });
    });

    await page.goto("/books");
    await expect(page.getByTestId("empty-state")).toBeVisible();
  });

  test("Handle API error gracefully", async ({ page }) => {
    await page.route("**/api/books", (route) => {
      route.fulfill({
        status: 500,
        body: JSON.stringify({ error: "Server error" }),
      });
    });

    await page.goto("/books");
    await expect(page.getByTestId("error-message")).toBeVisible();
    await expect(page.getByRole("button", { name: "Retry" })).toBeVisible();
  });

  test("Loading state during slow API", async ({ page }) => {
    await page.route("**/api/books", async (route) => {
      await new Promise((r) => setTimeout(r, 2000));
      route.fulfill({
        status: 200,
        body: JSON.stringify([{ id: 1, title: "Test Book" }]),
      });
    });

    await page.goto("/books");
    await expect(page.getByTestId("loading-spinner")).toBeVisible();
    await expect(page.getByTestId("loading-spinner")).not.toBeVisible({
      timeout: 3000,
    });
    await expect(page.getByTestId("book-card")).toHaveCount(1);
  });
});
```

---

# 🎓 สรุปทักษะที่ได้เรียนรู้

## ✅ Basic Skills

- ติดตั้งและ configure Playwright
- เขียน test cases พื้นฐาน
- ใช้ locators หา elements
- ใช้ actions โต้ตอบกับหน้าเว็บ
- ใช้ assertions ตรวจสอบผลลัพธ์

## ✅ Intermediate Skills

- จัดโครงสร้าง tests ด้วย describe และ hooks
- ใช้ Page Object Model
- จัดการ test data
- Cross-browser testing
- Visual regression testing

## ✅ Advanced Skills

- Network interception และ API mocking
- Custom fixtures
- Authentication state reuse
- CI/CD integration
- Parallel test execution

---

# 📚 Resources

- [Playwright Documentation](https://playwright.dev/docs/intro)
- [Playwright API Reference](https://playwright.dev/docs/api/class-playwright)
- [Best Practices](https://playwright.dev/docs/best-practices)
- [GitHub: Playwright](https://github.com/microsoft/playwright)

---
