# Playwright By Example

## สารบัญ

0. [บทนำ & Setup](#0-บทนำ--setup)
1. [ตัวอย่างที่ 1: Simple Login Form](#1-ตัวอย่างที่-1-simple-login-form)
2. [ตัวอย่างที่ 2: Todo App](#2-ตัวอย่างที่-2-todo-app)
3. [ตัวอย่างที่ 3: E-Commerce Product Page](#3-ตัวอย่างที่-3-e-commerce-product-page)
4. [ตัวอย่างที่ 4: Form Validation](#4-ตัวอย่างที่-4-form-validation)
5. [ตัวอย่างที่ 5: API Integration (Search)](#5-ตัวอย่างที่-5-api-integration-search)
6. [ตัวอย่างที่ 6: Multi-Page Flow](#6-ตัวอย่างที่-6-multi-page-flow)
7. [ตัวอย่างที่ 7: Library Management (Real Project)](#7-ตัวอย่างที่-7-library-management-real-project)
8. [Common Patterns & Tips](#8-common-patterns--tips)

---

## 0. บทนำ & Setup

### ทำไมต้องดู "Playwright by Example"?

Playwright Documentation ครอบคลุม แต่ **ตัวอย่าง Real-World** ช่วยให้เข้าใจได้เร็วขึ้น 10 เท่า!

**Playwright vs Jest:**

| ลักษณะ           | Jest                    | Playwright                   |
| ---------------- | ----------------------- | ---------------------------- |
| **ทดสอบ**        | Logic (Functions)       | UI/UX (User Behavior)        |
| **ระดับ**        | Unit Tests              | End-to-End Tests             |
| **เปิด Browser** | ❌                      | ✅ Chromium, Firefox, Safari |
| **ทดสอบ**        | Calculation, Validation | User Journey, Cross-browser  |
| **Speed**        | ⚡ 0.1-1ms              | ⚠️ 1-5 seconds               |
| **Setup**        | npm install jest        | npm install @playwright/test |

### Project Setup

```bash
# สร้าง project folder
mkdir playwright-examples
cd playwright-examples

# สร้าง package.json
npm init -y

# ติดตั้ง Playwright
npm install --save-dev @playwright/test

# สร้าง folder structure
mkdir src tests e2e

# สร้างไฟล์ playwright.config.js
npx playwright install
```

### playwright.config.js พื้นฐาน

```javascript
const { defineConfig, devices } = require("@playwright/test");

module.exports = defineConfig({
  testDir: "./tests", // โฟลเดอร์ที่เก็บ tests
  fullyParallel: true, // รัน tests พร้อม
  forbidOnly: !!process.env.CI, // ห้าม test.only ใน CI
  retries: 0, // Retry ถ้า fail (0 = ไม่ retry)
  workers: process.env.CI ? 1 : undefined, // Parallel workers

  use: {
    baseURL: "http://localhost:3000", // Base URL
    trace: "on-first-retry", // บันทึก trace เมื่อ fail
    screenshot: "only-on-failure", // Screenshot เมื่อ fail
    video: "retain-on-failure", // Video เมื่อ fail
  },

  projects: [
    { name: "chromium", use: { ...devices["Desktop Chrome"] } },
    { name: "firefox", use: { ...devices["Desktop Firefox"] } },
    { name: "webkit", use: { ...devices["Desktop Safari"] } },
  ],

  webServer: {
    command: "npm run dev", // รัน server ก่อน tests
    url: "http://localhost:3000",
    reuseExistingServer: true,
  },
});
```

---

## 1. ตัวอย่างที่ 1: Simple Login Form

### 📝 โจทย์: Test Login Form ขั้นพื้นฐาน

#### 🌐 HTML Code (`src/login.html`):

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Login</title>
    <style>
      body {
        font-family: Arial;
        padding: 20px;
      }
      .login-form {
        max-width: 400px;
        margin: 0 auto;
      }
      input {
        width: 100%;
        padding: 8px;
        margin: 10px 0;
      }
      button {
        width: 100%;
        padding: 10px;
        background: #007bff;
        color: white;
        border: none;
        cursor: pointer;
      }
      button:hover {
        background: #0056b3;
      }
      .error {
        color: red;
        margin-top: 10px;
        display: none;
      }
      .success {
        color: green;
        margin-top: 10px;
        display: none;
      }
    </style>
  </head>
  <body>
    <div class="login-form">
      <h1>Login</h1>

      <form id="loginForm">
        <input type="email" id="email" placeholder="Enter email" required />

        <input
          type="password"
          id="password"
          placeholder="Enter password"
          required
        />

        <button type="submit" id="loginBtn">Login</button>
      </form>

      <div id="errorMsg" class="error">Invalid email or password</div>
      <div id="successMsg" class="success">Login successful!</div>
    </div>

    <script>
      const form = document.getElementById("loginForm");
      const errorMsg = document.getElementById("errorMsg");
      const successMsg = document.getElementById("successMsg");

      form.addEventListener("submit", async (e) => {
        e.preventDefault();

        const email = document.getElementById("email").value;
        const password = document.getElementById("password").value;

        // ตรวจสอบ
        if (email === "user@example.com" && password === "password123") {
          errorMsg.style.display = "none";
          successMsg.style.display = "block";
          setTimeout(() => {
            window.location.href = "/dashboard"; // Redirect
          }, 1000);
        } else {
          errorMsg.style.display = "block";
          successMsg.style.display = "none";
        }
      });
    </script>
  </body>
</html>
```

#### ✅ Test File (`tests/login.spec.js`):

```javascript
const { test, expect } = require("@playwright/test");

describe("Login Form", () => {
  // 🏗️ Setup: ไปหน้า login ก่อนแต่ละ test
  test.beforeEach(async ({ page }) => {
    await page.goto("/login.html");
  });

  // ✅ Test: Happy Path (Login สำเร็จ)
  test("should login successfully with correct credentials", async ({
    page,
  }) => {
    // 1️⃣ ARRANGE - ไม่ต้องใน Playwright (page already loaded)

    // 2️⃣ ACT - ผู้ใช้ input email
    await page.fill("#email", "user@example.com");

    // 3️⃣ ACT - ผู้ใช้ input password
    await page.fill("#password", "password123");

    // 4️⃣ ACT - ผู้ใช้ click Login button
    await page.click("#loginBtn");

    // 5️⃣ ASSERT - ตรวจสอบ Success message ปรากฏ
    const successMsg = page.locator("#successMsg");
    await expect(successMsg).toBeVisible();

    // 6️⃣ ASSERT - ตรวจสอบ Success message มี text
    await expect(successMsg).toContainText("Login successful");
  });

  // ❌ Test: Wrong Password
  test("should show error with wrong password", async ({ page }) => {
    await page.fill("#email", "user@example.com");
    await page.fill("#password", "wrongpassword");
    await page.click("#loginBtn");

    // ตรวจสอบ Error message
    const errorMsg = page.locator("#errorMsg");
    await expect(errorMsg).toBeVisible();
    await expect(errorMsg).toContainText("Invalid email or password");
  });

  // ❌ Test: Wrong Email
  test("should show error with wrong email", async ({ page }) => {
    await page.fill("#email", "wrong@example.com");
    await page.fill("#password", "password123");
    await page.click("#loginBtn");

    const errorMsg = page.locator("#errorMsg");
    await expect(errorMsg).toBeVisible();
  });

  // ❌ Test: Empty Email
  test("should show validation error for empty email", async ({ page }) => {
    // HTML input required attribute ป้องกัน submit
    await page.fill("#password", "password123");

    // ⚠️ Form validation บ่อบทำให้ submit ไม่ได้
    const emailInput = page.locator("#email");
    const isRequired = await emailInput.evaluate((el) =>
      el.hasAttribute("required"),
    );
    expect(isRequired).toBe(true);
  });

  // ⏱️ Test: Check if redirect happens
  test("should redirect to dashboard after login", async ({ page }) => {
    await page.fill("#email", "user@example.com");
    await page.fill("#password", "password123");

    // รอให้ Navigation เสร็จ
    const navigationPromise = page.waitForNavigation();
    await page.click("#loginBtn");
    await navigationPromise;

    // ตรวจสอบ URL เปลี่ยนไป
    expect(page.url()).toContain("/dashboard");
  });
});
```

#### 🏃 รัน Tests:

```bash
npm test login.spec.js
# ✅ PASS 5 tests
```

---

## 2. ตัวอย่างที่ 2: Todo App

### 📝 โจทย์: Test Todo List (Add, Complete, Delete)

#### 🌐 HTML Code (`src/todo.html`):

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Todo App</title>
    <style>
      body {
        font-family: Arial;
        padding: 20px;
        max-width: 500px;
        margin: 0 auto;
      }
      .todo-container {
        background: #f5f5f5;
        padding: 20px;
        border-radius: 8px;
      }
      input[type="text"] {
        width: 80%;
        padding: 8px;
      }
      button {
        padding: 8px 15px;
        background: #007bff;
        color: white;
        border: none;
        cursor: pointer;
      }
      .todo-list {
        margin-top: 20px;
      }
      .todo-item {
        background: white;
        padding: 10px;
        margin: 8px 0;
        border-radius: 4px;
        display: flex;
        justify-content: space-between;
      }
      .todo-item.completed {
        opacity: 0.5;
        text-decoration: line-through;
      }
      .todo-text {
        flex: 1;
      }
      .todo-actions button {
        margin-left: 5px;
        padding: 4px 8px;
        font-size: 12px;
      }
    </style>
  </head>
  <body>
    <div class="todo-container">
      <h1>My Todos</h1>

      <div class="input-group">
        <input type="text" id="todoInput" placeholder="Add a new todo..." />
        <button id="addBtn">Add</button>
      </div>

      <div class="todo-list" id="todoList">
        <!-- Todos จะถูก add ที่นี่ -->
      </div>
    </div>

    <script>
      let todos = [];
      let nextId = 1;

      const todoInput = document.getElementById("todoInput");
      const addBtn = document.getElementById("addBtn");
      const todoList = document.getElementById("todoList");

      function render() {
        todoList.innerHTML = "";
        todos.forEach((todo) => {
          const div = document.createElement("div");
          div.className = `todo-item ${todo.completed ? "completed" : ""}`;
          div.innerHTML = `
          <span class="todo-text">${todo.text}</span>
          <div class="todo-actions">
            <button 
              class="complete-btn" 
              data-id="${todo.id}"
              onclick="toggleComplete(${todo.id})"
            >
              ${todo.completed ? "Undo" : "Done"}
            </button>
            <button 
              class="delete-btn" 
              data-id="${todo.id}"
              onclick="deleteTodo(${todo.id})"
            >
              Delete
            </button>
          </div>
        `;
          todoList.appendChild(div);
        });
      }

      function addTodo() {
        const text = todoInput.value.trim();
        if (text === "") return;

        todos.push({
          id: nextId++,
          text: text,
          completed: false,
        });

        todoInput.value = "";
        render();
      }

      function toggleComplete(id) {
        const todo = todos.find((t) => t.id === id);
        if (todo) todo.completed = !todo.completed;
        render();
      }

      function deleteTodo(id) {
        todos = todos.filter((t) => t.id !== id);
        render();
      }

      addBtn.addEventListener("click", addTodo);
      todoInput.addEventListener("keypress", (e) => {
        if (e.key === "Enter") addTodo();
      });
    </script>
  </body>
</html>
```

#### ✅ Test File (`tests/todo.spec.js`):

```javascript
const { test, expect } = require("@playwright/test");

describe("Todo App", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/todo.html");
  });

  // ✅ Test: Add Todo
  test("should add a new todo", async ({ page }) => {
    // 📝 Input todo text
    await page.fill("#todoInput", "Buy groceries");

    // 🖱️ Click Add button
    await page.click("#addBtn");

    // ✅ ตรวจสอบ Todo ปรากฏในรายการ
    const todoText = page.locator(".todo-text");
    await expect(todoText).toContainText("Buy groceries");

    // ✅ ตรวจสอบ Input ถูก Clear
    const input = page.locator("#todoInput");
    await expect(input).toHaveValue("");
  });

  // ✅ Test: Add Multiple Todos
  test("should add multiple todos", async ({ page }) => {
    const todos = ["Task 1", "Task 2", "Task 3"];

    for (const todo of todos) {
      await page.fill("#todoInput", todo);
      await page.click("#addBtn");
    }

    // ✅ ตรวจสอบ 3 todos ปรากฏ
    const todoItems = page.locator(".todo-item");
    await expect(todoItems).toHaveCount(3);
  });

  // ✅ Test: Complete Todo
  test("should mark todo as completed", async ({ page }) => {
    // Setup: Add todo
    await page.fill("#todoInput", "Test task");
    await page.click("#addBtn");

    // 🎯 Click Complete button
    const completeBtn = page.locator(".complete-btn").first();
    await completeBtn.click();

    // ✅ ตรวจสอบ Todo มี class "completed"
    const todoItem = page.locator(".todo-item").first();
    await expect(todoItem).toHaveClass(/completed/);
  });

  // ✅ Test: Undo Completed Todo
  test("should undo completed todo", async ({ page }) => {
    // Setup: Add and complete
    await page.fill("#todoInput", "Test task");
    await page.click("#addBtn");
    await page.locator(".complete-btn").first().click();

    // 🔄 Click Undo
    const completeBtn = page.locator(".complete-btn").first();
    await expect(completeBtn).toContainText("Undo");
    await completeBtn.click();

    // ✅ ตรวจสอบ class completed หายไป
    const todoItem = page.locator(".todo-item").first();
    const classList = await todoItem.evaluate((el) => Array.from(el.classList));
    expect(classList).not.toContain("completed");
  });

  // ✅ Test: Delete Todo
  test("should delete a todo", async ({ page }) => {
    // Setup: Add 2 todos
    await page.fill("#todoInput", "Todo 1");
    await page.click("#addBtn");
    await page.fill("#todoInput", "Todo 2");
    await page.click("#addBtn");

    // 🗑️ Delete first todo
    const deleteBtn = page.locator(".delete-btn").first();
    await deleteBtn.click();

    // ✅ ตรวจสอบ มี 1 todo เหลือ
    const todoItems = page.locator(".todo-item");
    await expect(todoItems).toHaveCount(1);

    // ✅ ตรวจสอบ Todo 2 ยังอยู่
    await expect(todoItems).toContainText("Todo 2");
  });

  // ✅ Test: Enter Key to Add
  test("should add todo with Enter key", async ({ page }) => {
    const input = page.locator("#todoInput");
    await input.fill("Enter key test");
    await input.press("Enter");

    // ✅ ตรวจสอบ Todo ถูก Add
    const todoText = page.locator(".todo-text");
    await expect(todoText).toContainText("Enter key test");
  });

  // ❌ Test: Empty Input (ไม่ add)
  test("should not add empty todo", async ({ page }) => {
    // ✅ ลองกด Add โดยไม่มี input
    await page.click("#addBtn");

    // ✅ ตรวจสอบ ไม่มี todo เพิ่ม
    const todoItems = page.locator(".todo-item");
    await expect(todoItems).toHaveCount(0);
  });
});
```

#### 🏃 รัน Tests:

```bash
npm test todo.spec.js
# ✅ PASS 8 tests
```

---

## 3. ตัวอย่างที่ 3: E-Commerce Product Page

### 📝 โจทย์: Test Product Page (View, Add to Cart, Checkout)

#### 🌐 HTML Code (`src/product.html`):

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Product Page</title>
    <style>
      body {
        font-family: Arial;
        padding: 20px;
      }
      .container {
        max-width: 1200px;
        margin: 0 auto;
      }
      .product-grid {
        display: grid;
        grid-template-columns: repeat(3, 1fr);
        gap: 20px;
      }
      .product-card {
        border: 1px solid #ddd;
        padding: 15px;
        border-radius: 8px;
      }
      .product-name {
        font-weight: bold;
        font-size: 16px;
      }
      .product-price {
        color: #d9534f;
        font-size: 18px;
        margin: 10px 0;
      }
      button {
        padding: 10px 15px;
        background: #5cb85c;
        color: white;
        border: none;
        cursor: pointer;
      }
      button:disabled {
        background: #999;
        cursor: not-allowed;
      }
      .cart-count {
        background: #333;
        color: white;
        padding: 5px 10px;
        border-radius: 50%;
        float: right;
        margin-bottom: 10px;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h1>
        Products
        <span class="cart-count" id="cartCount">0</span>
      </h1>

      <div class="product-grid" id="productGrid">
        <!-- Products จะถูก render ที่นี่ -->
      </div>
    </div>

    <script>
      const products = [
        { id: 1, name: "Laptop", price: 999 },
        { id: 2, name: "Phone", price: 599 },
        { id: 3, name: "Tablet", price: 399 },
      ];

      let cart = [];

      function renderProducts() {
        const grid = document.getElementById("productGrid");
        grid.innerHTML = products
          .map(
            (p) => `
        <div class="product-card" data-product-id="${p.id}">
          <div class="product-name">${p.name}</div>
          <div class="product-price">$${p.price}</div>
          <button class="add-to-cart" data-id="${p.id}" onclick="addToCart(${p.id})">
            Add to Cart
          </button>
        </div>
      `,
          )
          .join("");
      }

      function addToCart(productId) {
        const product = products.find((p) => p.id === productId);
        cart.push(product);
        updateCartCount();
      }

      function updateCartCount() {
        document.getElementById("cartCount").textContent = cart.length;
      }

      renderProducts();
    </script>
  </body>
</html>
```

#### ✅ Test File (`tests/product.spec.js`):

```javascript
const { test, expect } = require("@playwright/test");

describe("Product Page", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/product.html");
  });

  // ✅ Test: Products Display
  test("should display all products", async ({ page }) => {
    // ตรวจสอบ 3 products ปรากฏ
    const productCards = page.locator(".product-card");
    await expect(productCards).toHaveCount(3);

    // ตรวจสอบ Product names
    await expect(page.locator(".product-name")).toContainText("Laptop");
    await expect(page.locator(".product-name")).toContainText("Phone");
    await expect(page.locator(".product-name")).toContainText("Tablet");
  });

  // ✅ Test: Product Prices Displayed
  test("should display correct prices", async ({ page }) => {
    await expect(page.locator(".product-price")).toContainText("$999");
    await expect(page.locator(".product-price")).toContainText("$599");
    await expect(page.locator(".product-price")).toContainText("$399");
  });

  // ✅ Test: Add to Cart
  test("should add product to cart", async ({ page }) => {
    // 🛒 Click Add to Cart (first product)
    const addButtons = page.locator(".add-to-cart");
    await addButtons.first().click();

    // ✅ ตรวจสอบ Cart count = 1
    const cartCount = page.locator("#cartCount");
    await expect(cartCount).toContainText("1");
  });

  // ✅ Test: Add Multiple Products
  test("should add multiple products to cart", async ({ page }) => {
    const addButtons = page.locator(".add-to-cart");

    // 🛒 Click Add to Cart 3 times
    await addButtons.nth(0).click(); // Laptop
    await addButtons.nth(1).click(); // Phone
    await addButtons.nth(2).click(); // Tablet

    // ✅ ตรวจสอบ Cart count = 3
    const cartCount = page.locator("#cartCount");
    await expect(cartCount).toContainText("3");
  });

  // ✅ Test: Add Same Product Multiple Times
  test("should allow adding same product multiple times", async ({ page }) => {
    const addButton = page.locator(".add-to-cart").first();

    // 🛒 Add Laptop 3 times
    await addButton.click();
    await addButton.click();
    await addButton.click();

    // ✅ ตรวจสอบ Cart count = 3
    const cartCount = page.locator("#cartCount");
    await expect(cartCount).toContainText("3");
  });
});
```

---

## 4. ตัวอย่างที่ 4: Form Validation

### 📝 โจทย์: Test Register Form (Multiple Validations)

#### 🌐 HTML Code (`src/register.html`):

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Register</title>
    <style>
      body {
        font-family: Arial;
        max-width: 500px;
        margin: 50px auto;
      }
      .form-group {
        margin: 15px 0;
      }
      label {
        display: block;
        margin-bottom: 5px;
        font-weight: bold;
      }
      input,
      select {
        width: 100%;
        padding: 8px;
        box-sizing: border-box;
      }
      .error-message {
        color: #d9534f;
        font-size: 12px;
        margin-top: 5px;
        display: none;
      }
      button {
        width: 100%;
        padding: 10px;
        background: #5cb85c;
        color: white;
        border: none;
        cursor: pointer;
        margin-top: 10px;
      }
      .success {
        background: #dff0d8;
        color: #3c763d;
        padding: 10px;
        border-radius: 4px;
        display: none;
      }
    </style>
  </head>
  <body>
    <h1>Register</h1>

    <form id="registerForm">
      <div class="form-group">
        <label for="username">Username:</label>
        <input
          type="text"
          id="username"
          placeholder="Min 3 characters"
          minlength="3"
          required
        />
        <div class="error-message" id="usernameError"></div>
      </div>

      <div class="form-group">
        <label for="email">Email:</label>
        <input type="email" id="email" required />
        <div class="error-message" id="emailError"></div>
      </div>

      <div class="form-group">
        <label for="password">Password:</label>
        <input
          type="password"
          id="password"
          placeholder="Min 8 characters"
          minlength="8"
          required
        />
        <div class="error-message" id="passwordError"></div>
      </div>

      <div class="form-group">
        <label for="country">Country:</label>
        <select id="country" required>
          <option value="">Select country</option>
          <option value="thailand">Thailand</option>
          <option value="usa">USA</option>
          <option value="uk">UK</option>
        </select>
      </div>

      <div class="form-group">
        <label>
          <input type="checkbox" id="terms" required />
          I agree to terms and conditions
        </label>
      </div>

      <button type="submit">Register</button>
    </form>

    <div id="successMsg" class="success">Account created successfully!</div>

    <script>
      const form = document.getElementById("registerForm");

      form.addEventListener("submit", (e) => {
        e.preventDefault();

        const username = document.getElementById("username").value;
        const email = document.getElementById("email").value;
        const password = document.getElementById("password").value;
        const country = document.getElementById("country").value;
        const terms = document.getElementById("terms").checked;

        let valid = true;

        // Validate username
        if (username.length < 3) {
          document.getElementById("usernameError").textContent =
            "Username must be at least 3 characters";
          document.getElementById("usernameError").style.display = "block";
          valid = false;
        }

        // Validate email
        if (!email.includes("@")) {
          document.getElementById("emailError").textContent = "Invalid email";
          document.getElementById("emailError").style.display = "block";
          valid = false;
        }

        // Validate password
        if (password.length < 8) {
          document.getElementById("passwordError").textContent =
            "Password must be at least 8 characters";
          document.getElementById("passwordError").style.display = "block";
          valid = false;
        }

        if (valid && country && terms) {
          document.getElementById("successMsg").style.display = "block";
          form.reset();
        }
      });
    </script>
  </body>
</html>
```

#### ✅ Test File (`tests/register.spec.js`):

```javascript
const { test, expect } = require("@playwright/test");

describe("Register Form", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/register.html");
  });

  // ✅ Test: Valid Registration
  test("should register successfully with valid data", async ({ page }) => {
    await page.fill("#username", "john_doe");
    await page.fill("#email", "john@example.com");
    await page.fill("#password", "SecurePassword123");
    await page.selectOption("#country", "thailand");
    await page.check("#terms");

    await page.click('button[type="submit"]');

    // ✅ ตรวจสอบ Success message
    const successMsg = page.locator("#successMsg");
    await expect(successMsg).toBeVisible();
  });

  // ❌ Test: Username Too Short
  test("should show error for short username", async ({ page }) => {
    await page.fill("#username", "ab");
    await page.fill("#email", "test@example.com");
    await page.fill("#password", "SecurePassword123");
    await page.selectOption("#country", "thailand");
    await page.check("#terms");

    await page.click('button[type="submit"]');

    // ✅ ตรวจสอบ Error message
    const usernameError = page.locator("#usernameError");
    await expect(usernameError).toBeVisible();
    await expect(usernameError).toContainText("at least 3 characters");
  });

  // ❌ Test: Invalid Email
  test("should show error for invalid email", async ({ page }) => {
    await page.fill("#username", "valid_user");
    await page.fill("#email", "invalid-email"); // ไม่มี @
    await page.fill("#password", "SecurePassword123");
    await page.selectOption("#country", "thailand");
    await page.check("#terms");

    await page.click('button[type="submit"]');

    const emailError = page.locator("#emailError");
    await expect(emailError).toBeVisible();
    await expect(emailError).toContainText("Invalid email");
  });

  // ❌ Test: Password Too Short
  test("should show error for short password", async ({ page }) => {
    await page.fill("#username", "valid_user");
    await page.fill("#email", "test@example.com");
    await page.fill("#password", "short"); // < 8 chars
    await page.selectOption("#country", "thailand");
    await page.check("#terms");

    await page.click('button[type="submit"]');

    const passwordError = page.locator("#passwordError");
    await expect(passwordError).toBeVisible();
    await expect(passwordError).toContainText("at least 8 characters");
  });

  // ❌ Test: Country Not Selected
  test("should require country selection", async ({ page }) => {
    await page.fill("#username", "valid_user");
    await page.fill("#email", "test@example.com");
    await page.fill("#password", "SecurePassword123");
    // ไม่เลือก country
    await page.check("#terms");

    // Browser validation ป้องกัน submit
    const countrySelect = page.locator("#country");
    const isValid = await countrySelect.evaluate((el) => el.checkValidity());
    expect(isValid).toBe(false);
  });

  // ❌ Test: Terms Not Agreed
  test("should require terms agreement", async ({ page }) => {
    await page.fill("#username", "valid_user");
    await page.fill("#email", "test@example.com");
    await page.fill("#password", "SecurePassword123");
    await page.selectOption("#country", "thailand");
    // ไม่ check terms

    const termsCheckbox = page.locator("#terms");
    const isChecked = await termsCheckbox.isChecked();
    expect(isChecked).toBe(false);
  });

  // ✅ Test: Reset Form After Success
  test("should reset form after successful registration", async ({ page }) => {
    await page.fill("#username", "john_doe");
    await page.fill("#email", "john@example.com");
    await page.fill("#password", "SecurePassword123");
    await page.selectOption("#country", "thailand");
    await page.check("#terms");

    await page.click('button[type="submit"]');
    await expect(page.locator("#successMsg")).toBeVisible();

    // ✅ ตรวจสอบ Form ถูก Clear
    await expect(page.locator("#username")).toHaveValue("");
    await expect(page.locator("#email")).toHaveValue("");
    await expect(page.locator("#password")).toHaveValue("");
  });
});
```

---

## 5. ตัวอย่างที่ 5: API Integration (Search)

### 📝 โจทย์: Test Search Feature (API Call)

#### 🌐 HTML Code (`src/search.html`):

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Search</title>
    <style>
      body {
        font-family: Arial;
        max-width: 600px;
        margin: 20px auto;
      }
      .search-box {
        margin: 20px 0;
      }
      input {
        width: 70%;
        padding: 10px;
      }
      button {
        padding: 10px 20px;
        background: #007bff;
        color: white;
        border: none;
        cursor: pointer;
      }
      .loading {
        color: #666;
        margin-top: 10px;
        display: none;
      }
      .results {
        margin-top: 20px;
      }
      .result-item {
        border: 1px solid #ddd;
        padding: 10px;
        margin: 10px 0;
        border-radius: 4px;
      }
      .error {
        color: #d9534f;
        margin-top: 10px;
        display: none;
      }
    </style>
  </head>
  <body>
    <h1>Book Search</h1>

    <div class="search-box">
      <input type="text" id="searchInput" placeholder="Search books..." />
      <button id="searchBtn">Search</button>
    </div>

    <div class="loading" id="loading">Searching...</div>
    <div class="error" id="error"></div>
    <div class="results" id="results"></div>

    <script>
      const searchInput = document.getElementById("searchInput");
      const searchBtn = document.getElementById("searchBtn");
      const loading = document.getElementById("loading");
      const error = document.getElementById("error");
      const results = document.getElementById("results");

      // Mock API data
      const mockBooks = {
        javascript: [
          { id: 1, title: "JavaScript Guide", author: "John Doe" },
          { id: 2, title: "Advanced JS", author: "Jane Smith" },
        ],
        python: [
          { id: 3, title: "Python Basics", author: "Bob Wilson" },
          { id: 4, title: "Data Science with Python", author: "Alice Johnson" },
        ],
      };

      async function search() {
        const query = searchInput.value.trim().toLowerCase();

        if (!query) {
          error.textContent = "Please enter a search term";
          error.style.display = "block";
          results.innerHTML = "";
          return;
        }

        loading.style.display = "block";
        error.style.display = "none";
        results.innerHTML = "";

        // Simulate API call
        setTimeout(() => {
          loading.style.display = "none";

          const books = mockBooks[query] || [];

          if (books.length === 0) {
            error.textContent = "No books found";
            error.style.display = "block";
          } else {
            results.innerHTML = books
              .map(
                (book) => `
            <div class="result-item">
              <h3>${book.title}</h3>
              <p>by ${book.author}</p>
            </div>
          `,
              )
              .join("");
          }
        }, 500);
      }

      searchBtn.addEventListener("click", search);
      searchInput.addEventListener("keypress", (e) => {
        if (e.key === "Enter") search();
      });
    </script>
  </body>
</html>
```

#### ✅ Test File (`tests/search.spec.js`):

```javascript
const { test, expect } = require("@playwright/test");

describe("Search Feature", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/search.html");
  });

  // ✅ Test: Successful Search
  test("should find books by search query", async ({ page }) => {
    await page.fill("#searchInput", "javascript");
    await page.click("#searchBtn");

    // ⏳ รอให้ API response (500ms)
    await page.waitForSelector(".result-item");

    // ✅ ตรวจสอบ Results ปรากฏ
    const results = page.locator(".result-item");
    await expect(results).toHaveCount(2);

    // ✅ ตรวจสอบ Content
    await expect(page.locator("text=JavaScript Guide")).toBeVisible();
    await expect(page.locator("text=Advanced JS")).toBeVisible();
  });

  // ✅ Test: Search with Enter Key
  test("should search with Enter key", async ({ page }) => {
    const input = page.locator("#searchInput");
    await input.fill("python");
    await input.press("Enter");

    // ⏳ รอให้ Results ปรากฏ
    await page.waitForSelector(".result-item");

    // ✅ ตรวจสอบ Python Books
    await expect(page.locator("text=Python Basics")).toBeVisible();
    await expect(page.locator("text=Data Science with Python")).toBeVisible();
  });

  // ❌ Test: No Results Found
  test("should show error when no books found", async ({ page }) => {
    await page.fill("#searchInput", "ruby"); // ไม่มี ruby books
    await page.click("#searchBtn");

    // ⏳ รอให้ Search เสร็จ
    await page.waitForSelector("#error");

    // ✅ ตรวจสอบ Error message
    const error = page.locator("#error");
    await expect(error).toBeVisible();
    await expect(error).toContainText("No books found");
  });

  // ❌ Test: Empty Search
  test("should show error for empty search", async ({ page }) => {
    // ไม่กรอก text
    await page.click("#searchBtn");

    // ✅ ตรวจสอบ Error message
    const error = page.locator("#error");
    await expect(error).toBeVisible();
    await expect(error).toContainText("Please enter a search term");
  });

  // ✅ Test: Loading State
  test("should show loading indicator during search", async ({ page }) => {
    await page.fill("#searchInput", "javascript");

    // 🔍 Click search (ไม่ await waitForSelector)
    const searchPromise = page.click("#searchBtn");

    // ✅ ตรวจสอบ Loading ปรากฏ (อยู่ 500ms)
    const loading = page.locator("#loading");
    await expect(loading).toBeVisible();

    // ⏳ รอให้ Search เสร็จ
    await searchPromise;
    await page.waitForSelector(".result-item");

    // ✅ ตรวจสอบ Loading หายไป
    await expect(loading).not.toBeVisible();
  });

  // ✅ Test: Case Insensitive Search
  test("should search case-insensitively", async ({ page }) => {
    await page.fill("#searchInput", "JAVASCRIPT"); // Uppercase
    await page.click("#searchBtn");

    await page.waitForSelector(".result-item");

    // ✅ ยังควร Find JavaScript Books
    const results = page.locator(".result-item");
    await expect(results).toHaveCount(2);
  });

  // ✅ Test: Whitespace Trim
  test("should trim whitespace from search query", async ({ page }) => {
    await page.fill("#searchInput", "  javascript  "); // Whitespace
    await page.click("#searchBtn");

    await page.waitForSelector(".result-item");

    // ✅ ยังควร Find Books
    const results = page.locator(".result-item");
    await expect(results).toHaveCount(2);
  });
});
```

---

## 6. ตัวอย่างที่ 6: Multi-Page Flow

### 📝 โจทย์: Test User Journey (Home → Product → Checkout)

#### 🌐 HTML Files:

**home.html:**

```html
<h1>Welcome</h1>
<a href="/products.html" id="shopLink">Shop Now</a>
```

**products.html:**

```html
<h1>Products</h1>
<button class="add-to-cart" data-id="1">Add Laptop</button>
<a href="/checkout.html" id="checkoutLink">Go to Checkout</a>
```

**checkout.html:**

```html
<h1>Checkout</h1>
<div id="cartTotal">Total: $999</div>
<button id="confirmBtn">Confirm Order</button>
<div id="orderConfirm" style="display:none;">Order confirmed!</div>
```

#### ✅ Test File (`tests/multi-page.spec.js`):

```javascript
const { test, expect } = require("@playwright/test");

describe("Multi-Page User Journey", () => {
  // ✅ Test: Complete Purchase Flow
  test("should complete purchase flow from home to checkout", async ({
    page,
  }) => {
    // 1️⃣ User starts at home page
    await page.goto("/home.html");
    await expect(page).toHaveTitle(/Welcome/);

    // 2️⃣ User clicks "Shop Now"
    await page.click("#shopLink");
    await page.waitForURL("**/products.html");

    // ✅ ตรวจสอบ อยู่ Products Page แล้ว
    await expect(page.locator("h1")).toContainText("Products");

    // 3️⃣ User adds product to cart
    await page.click(".add-to-cart");

    // 4️⃣ User goes to checkout
    await page.click("#checkoutLink");
    await page.waitForURL("**/checkout.html");

    // ✅ ตรวจสอบ Checkout page
    const total = page.locator("#cartTotal");
    await expect(total).toContainText("$999");

    // 5️⃣ User confirms order
    await page.click("#confirmBtn");

    // ✅ ตรวจสอบ Order confirmation
    const confirm = page.locator("#orderConfirm");
    await expect(confirm).toBeVisible();
  });

  // ✅ Test: Navigation Back
  test("should navigate between pages", async ({ page }) => {
    await page.goto("/checkout.html");

    // ⬅️ Go back
    await page.goBack();

    // ✅ ตรวจสอบ อยู่ Products page
    await expect(page).toHaveURL("**/products.html");
  });
});
```

---

## 7. ตัวอย่างที่ 7: Library Management (Real Project)

### 📝 โจทย์: Test Complete Library Management System

#### 🌐 HTML Code (`src/library.html`):

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Library Management</title>
    <style>
      body {
        font-family: Arial;
        max-width: 1000px;
        margin: 20px auto;
      }
      .section {
        margin: 30px 0;
        border: 1px solid #ddd;
        padding: 20px;
        border-radius: 8px;
      }
      input,
      button,
      select {
        padding: 8px;
        margin: 5px;
      }
      .book-list {
        margin-top: 15px;
      }
      .book-item {
        background: #f9f9f9;
        padding: 10px;
        margin: 8px 0;
        border-radius: 4px;
        display: flex;
        justify-content: space-between;
      }
      .available {
        color: green;
      }
      .unavailable {
        color: red;
      }
    </style>
  </head>
  <body>
    <h1>📚 Library Management System</h1>

    <!-- Add Book Section -->
    <div class="section">
      <h2>Add Book</h2>
      <div>
        <input type="text" id="bookTitle" placeholder="Book Title" />
        <input type="text" id="bookAuthor" placeholder="Author" />
        <input type="number" id="bookCopies" placeholder="Copies" value="1" />
        <button id="addBookBtn">Add Book</button>
      </div>
      <div id="addBookMsg" style="margin-top: 10px;"></div>
    </div>

    <!-- Borrow Book Section -->
    <div class="section">
      <h2>Borrow Book</h2>
      <div>
        <select id="bookSelect">
          <option value="">Select a book</option>
        </select>
        <input type="text" id="memberName" placeholder="Member Name" />
        <button id="borrowBtn">Borrow</button>
      </div>
      <div id="borrowMsg" style="margin-top: 10px;"></div>
    </div>

    <!-- Books List -->
    <div class="section">
      <h2>Books Inventory</h2>
      <div class="book-list" id="bookList">
        <p>No books yet</p>
      </div>
    </div>

    <script>
      let books = [];
      let nextId = 1;

      const bookTitleInput = document.getElementById("bookTitle");
      const bookAuthorInput = document.getElementById("bookAuthor");
      const bookCopiesInput = document.getElementById("bookCopies");
      const addBookBtn = document.getElementById("addBookBtn");
      const addBookMsg = document.getElementById("addBookMsg");

      const bookSelect = document.getElementById("bookSelect");
      const memberNameInput = document.getElementById("memberName");
      const borrowBtn = document.getElementById("borrowBtn");
      const borrowMsg = document.getElementById("borrowMsg");

      const bookList = document.getElementById("bookList");

      function renderBooks() {
        if (books.length === 0) {
          bookList.innerHTML = "<p>No books yet</p>";
          bookSelect.innerHTML = '<option value="">Select a book</option>';
          return;
        }

        bookList.innerHTML = books
          .map(
            (book) => `
        <div class="book-item">
          <div>
            <strong>${book.title}</strong> by ${book.author}
            <br>
            <span class="${book.available > 0 ? "available" : "unavailable"}">
              Available: ${book.available}/${book.total}
            </span>
          </div>
          <div>
            <span>${book.total} copies</span>
          </div>
        </div>
      `,
          )
          .join("");

        bookSelect.innerHTML =
          '<option value="">Select a book</option>' +
          books
            .map((book) => `<option value="${book.id}">${book.title}</option>`)
            .join("");
      }

      addBookBtn.addEventListener("click", () => {
        const title = bookTitleInput.value.trim();
        const author = bookAuthorInput.value.trim();
        const copies = parseInt(bookCopiesInput.value) || 1;

        if (!title || !author) {
          addBookMsg.textContent = "❌ Please fill in all fields";
          addBookMsg.style.color = "red";
          return;
        }

        if (copies <= 0) {
          addBookMsg.textContent = "❌ Copies must be positive";
          addBookMsg.style.color = "red";
          return;
        }

        books.push({
          id: nextId++,
          title,
          author,
          total: copies,
          available: copies,
        });

        addBookMsg.textContent = `✅ Book "${title}" added successfully`;
        addBookMsg.style.color = "green";

        bookTitleInput.value = "";
        bookAuthorInput.value = "";
        bookCopiesInput.value = "1";

        renderBooks();
      });

      borrowBtn.addEventListener("click", () => {
        const bookId = parseInt(bookSelect.value);
        const memberName = memberNameInput.value.trim();

        if (!bookId) {
          borrowMsg.textContent = "❌ Please select a book";
          borrowMsg.style.color = "red";
          return;
        }

        if (!memberName) {
          borrowMsg.textContent = "❌ Please enter member name";
          borrowMsg.style.color = "red";
          return;
        }

        const book = books.find((b) => b.id === bookId);
        if (!book) {
          borrowMsg.textContent = "❌ Book not found";
          borrowMsg.style.color = "red";
          return;
        }

        if (book.available === 0) {
          borrowMsg.textContent = `❌ ${book.title} is not available`;
          borrowMsg.style.color = "red";
          return;
        }

        book.available--;

        borrowMsg.textContent = `✅ ${memberName} borrowed "${book.title}" successfully`;
        borrowMsg.style.color = "green";

        memberNameInput.value = "";
        renderBooks();
      });

      renderBooks();
    </script>
  </body>
</html>
```

#### ✅ Test File (`tests/library.spec.js`):

```javascript
const { test, expect } = require("@playwright/test");

describe("Library Management System", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/library.html");
  });

  // ✅ Test: Add Book
  test("should add a new book", async ({ page }) => {
    await page.fill("#bookTitle", "JavaScript Guide");
    await page.fill("#bookAuthor", "John Doe");
    await page.fill("#bookCopies", "5");

    await page.click("#addBookBtn");

    // ✅ ตรวจสอบ Success message
    const msg = page.locator("#addBookMsg");
    await expect(msg).toContainText("added successfully");
    await expect(msg).toContainText("JavaScript Guide");

    // ✅ ตรวจสอบ Book ปรากฏในรายการ
    const bookList = page.locator(".book-item");
    await expect(bookList).toContainText("JavaScript Guide");
    await expect(bookList).toContainText("Available: 5/5");
  });

  // ✅ Test: Add Multiple Books
  test("should add multiple books", async ({ page }) => {
    const books = [
      { title: "Book 1", author: "Author 1", copies: "3" },
      { title: "Book 2", author: "Author 2", copies: "2" },
      { title: "Book 3", author: "Author 3", copies: "4" },
    ];

    for (const book of books) {
      await page.fill("#bookTitle", book.title);
      await page.fill("#bookAuthor", book.author);
      await page.fill("#bookCopies", book.copies);
      await page.click("#addBookBtn");
    }

    // ✅ ตรวจสอบ 3 books ปรากฏ
    const bookItems = page.locator(".book-item");
    await expect(bookItems).toHaveCount(3);
  });

  // ❌ Test: Validation - Missing Title
  test("should show error when title is missing", async ({ page }) => {
    await page.fill("#bookAuthor", "John Doe");
    await page.fill("#bookCopies", "5");
    await page.click("#addBookBtn");

    const msg = page.locator("#addBookMsg");
    await expect(msg).toContainText("Please fill in all fields");
  });

  // ❌ Test: Validation - Invalid Copies
  test("should show error for invalid copies", async ({ page }) => {
    await page.fill("#bookTitle", "Book");
    await page.fill("#bookAuthor", "Author");
    await page.fill("#bookCopies", "0");
    await page.click("#addBookBtn");

    const msg = page.locator("#addBookMsg");
    await expect(msg).toContainText("positive");
  });

  // ✅ Test: Borrow Book
  test("should borrow an available book", async ({ page }) => {
    // Setup: Add book
    await page.fill("#bookTitle", "Test Book");
    await page.fill("#bookAuthor", "Test Author");
    await page.fill("#bookCopies", "3");
    await page.click("#addBookBtn");

    // Borrow
    await page.selectOption("#bookSelect", { label: "Test Book" });
    await page.fill("#memberName", "John");
    await page.click("#borrowBtn");

    // ✅ ตรวจสอบ Success message
    const msg = page.locator("#borrowMsg");
    await expect(msg).toContainText("John borrowed");

    // ✅ ตรวจสอบ Available count ลดลง
    const bookItem = page.locator(".book-item");
    await expect(bookItem).toContainText("Available: 2/3");
  });

  // ❌ Test: Borrow Unavailable Book
  test("should show error when book unavailable", async ({ page }) => {
    // Setup: Add book with 1 copy
    await page.fill("#bookTitle", "Limited Book");
    await page.fill("#bookAuthor", "Author");
    await page.fill("#bookCopies", "1");
    await page.click("#addBookBtn");

    // Borrow once
    await page.selectOption("#bookSelect", { label: "Limited Book" });
    await page.fill("#memberName", "John");
    await page.click("#borrowBtn");

    // Try to borrow again
    await page.fill("#memberName", "Jane");
    await page.click("#borrowBtn");

    // ✅ ตรวจสอบ Error message
    const msg = page.locator("#borrowMsg");
    await expect(msg).toContainText("not available");
  });

  // ❌ Test: Borrow Without Selection
  test("should show error when book not selected", async ({ page }) => {
    // Setup: Add book
    await page.fill("#bookTitle", "Book");
    await page.fill("#bookAuthor", "Author");
    await page.click("#addBookBtn");

    // Try to borrow without selecting
    await page.fill("#memberName", "John");
    await page.click("#borrowBtn");

    const msg = page.locator("#borrowMsg");
    await expect(msg).toContainText("Please select a book");
  });

  // ✅ Test: Complete Scenario
  test("should complete library workflow", async ({ page }) => {
    // Add 2 books
    await page.fill("#bookTitle", "JavaScript");
    await page.fill("#bookAuthor", "John Doe");
    await page.fill("#bookCopies", "2");
    await page.click("#addBookBtn");

    await page.fill("#bookTitle", "Python");
    await page.fill("#bookAuthor", "Jane Smith");
    await page.fill("#bookCopies", "3");
    await page.click("#addBookBtn");

    // User 1 borrows JavaScript
    await page.selectOption("#bookSelect", { label: "JavaScript" });
    await page.fill("#memberName", "Member1");
    await page.click("#borrowBtn");

    // User 2 borrows JavaScript
    await page.fill("#memberName", "Member2");
    await page.click("#borrowBtn");

    // User 3 borrows Python
    await page.selectOption("#bookSelect", { label: "Python" });
    await page.fill("#memberName", "Member3");
    await page.click("#borrowBtn");

    // Verify counts
    const bookItems = page.locator(".book-item");
    const items = await bookItems.allTextContents();

    // JavaScript: 0/2 available (2 borrowed)
    expect(items[0]).toContain("Available: 0/2");

    // Python: 2/3 available (1 borrowed)
    expect(items[1]).toContain("Available: 2/3");
  });
});
```

---

## 8. Common Patterns & Tips

### 🎯 Pattern 1: Page Object Model (POM)

```javascript
// ❌ ไม่ดี: Selectors เกาะไปทั่ว
test("login", async ({ page }) => {
  await page.fill("#email", "user@example.com");
  await page.fill("#password", "password");
  await page.click(".btn-login");
  await expect(page.locator(".success-msg")).toBeVisible();
});

test("logout", async ({ page }) => {
  await page.click("#user-menu");
  await page.click("#logout-btn");
});

// ✅ ดี: POM Pattern
class LoginPage {
  constructor(page) {
    this.page = page;
    this.emailInput = page.locator("#email");
    this.passwordInput = page.locator("#password");
    this.loginButton = page.locator(".btn-login");
    this.successMsg = page.locator(".success-msg");
  }

  async login(email, password) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
  }

  async isLoggedIn() {
    return await this.successMsg.isVisible();
  }
}

test("should login successfully", async ({ page }) => {
  await page.goto("/login");
  const loginPage = new LoginPage(page);

  await loginPage.login("user@example.com", "password");
  expect(await loginPage.isLoggedIn()).toBe(true);
});
```

**ข้อดี:**

- ✅ Selectors อยู่ที่เดียว
- ✅ ถ้า UI เปลี่ยน แก้ที่เดียว
- ✅ Tests อ่านง่ายขึ้น

---

### 🎯 Pattern 2: Reusable Fixtures

```javascript
const { test, expect } = require("@playwright/test");

test.describe("with logged-in user", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/login");
    await page.fill("#email", "user@example.com");
    await page.fill("#password", "password");
    await page.click("#loginBtn");

    // ⏳ รอให้ Login เสร็จ
    await page.waitForURL("/dashboard");
  });

  test("should show dashboard", async ({ page }) => {
    await expect(page.locator("h1")).toContainText("Dashboard");
  });

  test("should show user profile", async ({ page }) => {
    await page.click("#profileBtn");
    await expect(page.locator(".profile-name")).toBeVisible();
  });

  // ทั้งหมด tests ทำงาน with logged-in user!
});
```

---

### 🎯 Pattern 3: Network Mocking & Interception

```javascript
test("should handle API errors gracefully", async ({ page }) => {
  // 🔴 Mock API to return error
  await page.route("**/api/books", (route) => {
    route.abort("failed");
  });

  await page.goto("/search");
  await page.fill("#searchInput", "test");
  await page.click("#searchBtn");

  // ✅ ตรวจสอบ Error message
  await expect(page.locator(".error")).toContainText("Error loading");
});

test("should handle slow API", async ({ page }) => {
  // 🐢 Mock slow API
  await page.route("**/api/books", (route) => {
    setTimeout(() => route.continue(), 3000); // 3s delay
  });

  await page.goto("/search");
  await page.fill("#searchInput", "test");

  // ⏳ ตรวจสอบ Loading state
  await expect(page.locator(".loading")).toBeVisible();

  // ⏳ รอให้ Response เก่า
  await page.waitForSelector(".result-item");
  await expect(page.locator(".loading")).not.toBeVisible();
});
```

---

### 💡 Best Practices

1. **Use Semantic Locators**

```javascript
// ❌ Fragile
await page.click("button.btn.btn-lg.btn-primary");

// ✅ Robust
await page.getByRole("button", { name: "Submit" }).click();
```

2. **Wait Properly**

```javascript
// ❌ Bad: Hardcoded wait
await page.waitForTimeout(2000);

// ✅ Good: Wait for element
await page.waitForSelector(".result-item");

// ✅ Better: Wait for condition
await expect(page.locator(".loading")).not.toBeVisible();
```

3. **Handle Async Properly**

```javascript
// ❌ Bad: Forgot await
test("should...", async ({ page }) => {
  page.goto("/"); // Missing await!
  await expect(page).toHaveTitle("Home");
});

// ✅ Good: await everything
test("should...", async ({ page }) => {
  await page.goto("/");
  await expect(page).toHaveTitle("Home");
});
```

4. **Test Real User Behavior**

```javascript
// ❌ Bad: Technical detail
test("should call fetch API", async ({ page }) => {
  // Testing implementation, not behavior
});

// ✅ Good: User behavior
test("should display search results", async ({ page }) => {
  await page.fill("#search", "javascript");
  await page.press("Enter");

  await expect(page.locator(".result")).toHaveCount(5);
});
```

---

## สรุป

**ตัวอย่าง 7 นี้แสดงให้เห็น:**

1. ✅ Simple Forms → Complex Multi-page Apps
2. ✅ User Input → API Integration
3. ✅ Happy Paths → Error Handling
4. ✅ Single Page → Real Project

**ท้ายสุด:** Playwright E2E Testing = **ทำให้ Product ปลอดภัย ก่อน Release** 🎉
