# Week 10: System Testing และ Cross-Browser Testing

## 🎯 วัตถุประสงค์ Lab

เมื่อจบ Lab นี้ นิสิตจะสามารถ:

1. เขียน End-to-End test scenarios ด้วย Playwright
2. ทดสอบ complete user journeys
3. ทำ cross-browser testing บน Chromium, Firefox, WebKit
4. เตรียม UAT test cases และ sign-off documents

---

## 📌 Lab Duration: 1 ชั่วโมง

### Part 1: System Testing - E2E Scenarios (30 นาที)

### Part 2: Cross-Browser Testing (15 นาที)

### Part 3: UAT Preparation (15 นาที)

---

## 📋 Prerequisites

- Node.js และ npm ติดตั้งแล้ว
- Library-Management-v2 project พร้อมใช้งาน
- พื้นฐาน JavaScript และ async/await
- ความเข้าใจ test fundamentals จากคำบรรยาย

---

## ⚡ Library-Management-v2 Quick Reference

**Test Credentials:**

- Username: `admin` | Password: `admin123`
- Alternative: `librarian` | Password: `lib123`

**Application Base URL:** `http://localhost:3000`

**Main Pages:**

- `/login` - Login page
- `/dashboard` - Dashboard (statistics, overview)
- `/books` - Browse all books
- `/borrowing` - Manage borrowed books (borrow/return)
- `/members` - Member management
- `/reports` - Reports page

**Key API Endpoints (require authentication):**

- `POST /api/auth/login` - Login
- `GET /api/books` - List all books
- `POST /api/borrowing` - Borrow a book
- `PUT /api/borrowing/:borrowId/return` - Return a book

**Important Notes:**
⚠️ Library-Management-v2 **ไม่มี user registration endpoint** - ใช้ test account ที่มีอยู่แล้ว (admin/admin123)
⚠️ ทุก page ยกเว้น `/login` ต้องการ authentication

---

## Part 1: System Testing - E2E Scenarios (30 นาที)

### Setup

1. **ติดตั้ง Playwright (หากยังไม่ได้ติดตั้ง):**

```bash
npm install --save-dev @playwright/test
npx playwright install
```

2. **สร้าง test directory:**

```bash
mkdir -p tests/e2e
```

3. **ตั้งค่า Playwright:**

```javascript
// playwright.config.js
module.exports = {
  testDir: "./tests/e2e",
  timeout: 30000,
  retries: 1,
  use: {
    baseURL: "http://localhost:3000",
    screenshot: "only-on-failure",
    video: "retain-on-failure",
  },
  projects: [
    {
      name: "chromium",
      use: { browserName: "chromium" },
    },
  ],
};
```

---

### Exercise 1: Complete User Journey Test (20 นาที)

**Scenario: User Login → Browse Books → Borrow → Return → Logout**

```javascript
// tests/e2e/complete-user-journey.spec.js
const { test, expect } = require("@playwright/test");

test.describe("Complete User Journey", () => {
  let testUser;

  test.beforeEach(() => {
    // ใช้ test account ที่มีอยู่แล้วในระบบ
    // (Library-Management-v2 ไม่มี registration endpoint)
    testUser = {
      username: "admin",
      password: "admin123",
    };
  });

  test("user can login, view books, borrow, and return book", async ({
    page,
  }) => {
    // ═══════════════════════════════════════════════════════════════
    // Step 1: ล็อกอิน ด้วย test account ที่มีอยู่แล้ว
    // ═══════════════════════════════════════════════════════════════
    await page.goto("/login");
    await page.fill('[data-testid="username-input"]', testUser.username);
    await page.fill('[data-testid="password-input"]', testUser.password);
    await page.click('[data-testid="login-button"]');

    // ยืนยันการล็อกอินสำเร็จ - redirect ไปหน้า dashboard
    await expect(page).toHaveURL(/dashboard/);
    await page.waitForSelector('[data-testid="dashboard-header"]');

    // ═══════════════════════════════════════════════════════════════
    // Step 2: ไปยัง Books page เพื่อดูหนังสือที่มีอยู่
    // ═══════════════════════════════════════════════════════════════
    await page.goto("/books");
    await page.waitForSelector('[data-testid="book-list"]');

    // ยืนยันว่ามีหนังสือแสดง
    const bookCount = await page.locator('[data-testid="book-row"]').count();
    expect(bookCount).toBeGreaterThan(0);

    // ═══════════════════════════════════════════════════════════════
    // Step 3: เลือกหนังสือและตรวจสอบสถานะความพร้อม
    // ═══════════════════════════════════════════════════════════════
    const firstBook = page.locator('[data-testid="book-row"]').first();
    const bookTitle = await firstBook
      .locator('[data-testid="book-title"]')
      .textContent();

    // ตรวจสอบว่าหนังสือพร้อมให้ยืม (available สถานะ)
    const availabilityStatus = await firstBook
      .locator('[data-testid="book-status"]')
      .textContent();
    expect(availabilityStatus).toContain("Available");

    // ═══════════════════════════════════════════════════════════════
    // Step 4: ยืมหนังสือโดยคลิก borrow button
    // ═══════════════════════════════════════════════════════════════
    await firstBook.locator('[data-testid="borrow-btn"]').click();

    // รอให้ dialog ยืนยันการยืมปรากฏ
    await page
      .locator('[data-testid="borrow-confirm-btn"]')
      .waitFor({ state: "visible" });
    await page.locator('[data-testid="borrow-confirm-btn"]').click();

    // ยืนยันการยืมสำเร็จ - ตรวจสอบ success message หรือ API response
    await page.waitForResponse(
      (response) =>
        response.url().includes("/api/borrowing") && response.status() === 200,
    );

    // ═══════════════════════════════════════════════════════════════
    // Step 5: ไปยัง Borrowing page เพื่อดูหนังสือที่เคยยืม
    // ═══════════════════════════════════════════════════════════════
    await page.goto("/borrowing");
    await page.waitForSelector('[data-testid="borrowing-list"]');

    // ตรวจสอบว่าหนังสือที่เพิ่งยืมปรากฏในรายการ
    const borrowedBooks = page.locator('[data-testid="borrowed-book-row"]');
    const borrowedBookCount = await borrowedBooks.count();
    expect(borrowedBookCount).toBeGreaterThan(0);

    // ยืนยันว่าหนังสือที่ยืมปรากฏพร้อมกับวันครบกำหนด
    const borrowedBookTitles = await borrowedBooks
      .locator('[data-testid="book-title"]')
      .allTextContents();
    expect(
      borrowedBookTitles.some((title) => title.includes(bookTitle)),
    ).toBeTruthy();

    // ═══════════════════════════════════════════════════════════════
    // Step 6: คืนหนังสือจาก Borrowing page
    // ═══════════════════════════════════════════════════════════════
    // เลือกแถวแรกของหนังสือที่ยืมจาก Borrowing list
    const firstBorrowedBook = page
      .locator('[data-testid="borrowed-book-row"]')
      .first();

    // คลิกปุ่ม return-btn
    await firstBorrowedBook.locator('[data-testid="return-btn"]').click();

    // รอให้ dialog ยืนยันการคืนปรากฏ
    const confirmReturnBtn = page.locator('[data-testid="return-confirm-btn"]');
    await confirmReturnBtn.waitFor({ state: "visible" });
    await confirmReturnBtn.click();

    // ยืนยันการคืนสำเร็จ - ตรวจสอบ API response
    await page.waitForResponse(
      (response) =>
        response.url().includes("/api/borrowing") && response.status() === 200,
    );

    // ═══════════════════════════════════════════════════════════════
    // Step 7: ตรวจสอบว่าหนังสือไม่อยู่ในรายการ Borrowed อีกต่อไป
    // ═══════════════════════════════════════════════════════════════
    // Refresh เพื่อโหลดข้อมูลล่าสุด
    await page.reload();
    await page.waitForSelector('[data-testid="borrowing-list"]');

    // เปรียบเทียบจำนวนหนังสือที่ยืม - ควรลดลง และหนังสือที่คืนไม่ควรปรากฏ
    const borrowedBooksAfterReturn = page.locator(
      '[data-testid="borrowed-book-row"]',
    );
    const borrowedBookCountAfterReturn = await borrowedBooksAfterReturn.count();
    expect(borrowedBookCountAfterReturn).toBeLessThanOrEqual(
      borrowedBookCount - 1,
    );

    // ═══════════════════════════════════════════════════════════════
    // Step 8: ออกจากระบบ (Logout)
    // ═══════════════════════════════════════════════════════════════
    // คลิก user menu หรือ logout link
    await page.click('[data-testid="user-menu-btn"]');
    await page.click('[data-testid="logout-link"]');

    // ยืนยันการออกจากระบบสำเร็จ - redirect ไปหน้า login
    await expect(page).toHaveURL("/login");
  });
});
```

---

### Exercise 2: Error Handling E2E Test (10 นาที)

```javascript
// tests/e2e/error-handling.spec.js
const { test, expect } = require("@playwright/test");

test.describe("การจัดการข้อผิดพลาดของระบบ", () => {
  test("จัดการการล็อกอินที่ไม่ถูกต้องอย่างสุภาพ", async ({ page }) => {
    await page.goto("/login");

    await page.fill('[data-testid="username-input"]', "nonexistent");
    await page.fill('[data-testid="password-input"]', "wrongpassword");
    await page.click('[data-testid="login-button"]');

    // ควรแสดง ข้อความแสดงข้อผิดพลาด
    await expect(page.locator('[data-testid="error-message"]')).toContainText(
      "Invalid username or password",
    );

    // ควรคงอยู่บนหน้าล็อกอิน
    await expect(page).toHaveURL(/login/);
  });

  test("จัดการการพยายามยืมหนังสือที่ไม่พร้อม", async ({ page }) => {
    // ล็อกอินก่อน
    await page.goto("/login");
    await page.fill('[data-testid="username-input"]', "admin");
    await page.fill('[data-testid="password-input"]', "admin123");
    await page.click('[data-testid="login-button"]');

    await expect(page).toHaveURL(/dashboard/);

    // ไปยังหนังสือและตรวจสอบหนังสือที่ไม่มี (unavailable)
    await page.goto("/books");
    await page.waitForSelector('[data-testid="book-list"]');

    // หา book ที่มี status "Unavailable"
    const unavailableBooks = page.locator(
      '[data-testid="book-row"] >> filter([has-text="Unavailable"])',
    );
    const unavailableCount = await unavailableBooks.count();

    if (unavailableCount > 0) {
      // พยายามที่จะยืม
      await unavailableBooks
        .first()
        .locator('[data-testid="borrow-btn"]')
        .click();

      // ควรแสดงข้อผิดพลาดหรือถูก disabled
      const borrowBtn = unavailableBooks
        .first()
        .locator('[data-testid="borrow-btn"]');
      const isDisabled = await borrowBtn.isDisabled();
      expect(
        isDisabled ||
          (await page.locator('[data-testid="error-message"]').isVisible()),
      ).toBeTruthy();
    }
  });

  test("ป้องกันการเข้าถึง protected pages ถ้า logout แล้ว", async ({
    page,
  }) => {
    // ล็อกอิน
    await page.goto("/login");
    await page.fill('[data-testid="username-input"]', "admin");
    await page.fill('[data-testid="password-input"]', "admin123");
    await page.click('[data-testid="login-button"]');

    await expect(page).toHaveURL(/dashboard/);

    // ออกจากระบบ
    await page.click('[data-testid="user-menu-btn"]');
    await page.click('[data-testid="logout-link"]');

    await expect(page).toHaveURL(/login/);

    // พยายามเข้าถึง protected page โดยตรง
    await page.goto("/books");

    // ควร redirect กลับมา login
    await expect(page).toHaveURL(/login/);
  });
});
```

---

## Part 2: Cross-Browser Testing (15 นาที)

### Exercise 3: Configure Multi-Browser Testing

1. **Update playwright.config.js:**

```javascript
// playwright.config.js
const { devices } = require("@playwright/test");

module.exports = {
  testDir: "./tests/e2e",
  timeout: 30000,
  retries: 1,

  use: {
    baseURL: "http://localhost:3000",
    screenshot: "only-on-failure",
    video: "retain-on-failure",
    trace: "on-first-retry",
  },

  projects: [
    // Desktop Browsers
    {
      name: "chromium",
      use: {
        ...devices["Desktop Chrome"],
      },
    },
    {
      name: "firefox",
      use: {
        ...devices["Desktop Firefox"],
      },
    },
    {
      name: "webkit",
      use: {
        ...devices["Desktop Safari"],
      },
    },

    // Mobile Devices
    {
      name: "Mobile Chrome",
      use: {
        ...devices["Pixel 5"],
      },
    },
    {
      name: "Mobile Safari",
      use: {
        ...devices["iPhone 12"],
      },
    },

    // Tablet
    {
      name: "iPad",
      use: {
        ...devices["iPad Pro"],
      },
    },
  ],
};
```

2. **Create Cross-Browser Test:**

```javascript
// tests/e2e/cross-browser.spec.js
const { test, expect } = require("@playwright/test");

test.describe("Cross-Browser Compatibility", () => {
  test("ล็อกอินทำงานบน browsers ทั้งหมด", async ({ page, browserName }) => {
    console.log(`ทำการทดสอบบน: ${browserName}`);

    await page.goto("/login");

    // กรอกแบบฟอร์มล็อกอิน
    await page.fill('[data-testid="username-input"]', "admin");
    await page.fill('[data-testid="password-input"]', "admin123");
    await page.click('[data-testid="login-button"]');

    // ควร redirect ไปยัง dashboard
    await expect(page).toHaveURL(/dashboard/);
    await page.waitForSelector('[data-testid="dashboard-header"]');
  });

  test("การเข้าถึง Books page ทำงานบน browsers ทั้งหมด", async ({ page }) => {
    // ล็อกอินก่อน
    await page.goto("/login");
    await page.fill('[data-testid="username-input"]', "admin");
    await page.fill('[data-testid="password-input"]', "admin123");
    await page.click('[data-testid="login-button"]');

    // ไปที่ Books page
    await page.goto("/books");
    await page.waitForSelector('[data-testid="book-list"]');

    // ควร มีหนังสือแสดง
    const bookCount = await page.locator('[data-testid="book-row"]').count();
    expect(bookCount).toBeGreaterThan(0);
  });

  test("responsive layout บน mobile และ desktop", async ({
    page,
    viewport,
  }) => {
    // ล็อกอินก่อน
    await page.goto("/login");
    await page.fill('[data-testid="username-input"]', "admin");
    await page.fill('[data-testid="password-input"]', "admin123");
    await page.click('[data-testid="login-button"]');

    // ไปที่ Books page
    await page.goto("/books");
    await page.waitForSelector('[data-testid="book-list"]');

    // ตรวจสอบ responsive behavior ตามขนาด viewport
    if (viewport && viewport.width < 768) {
      // บน mobile: ควร show mobile menu
      const mobileMenu = page.locator('[data-testid="mobile-menu"]');
      const hasVisibleMobileMenu = await mobileMenu
        .isVisible()
        .catch(() => false);
      expect(
        hasVisibleMobileMenu ||
          (await page.locator('[data-testid="hamburger-btn"]').isVisible()),
      ).toBeTruthy();
    } else {
      // บน desktop: ควร show desktop navigation
      const desktopNav = page.locator('[data-testid="desktop-nav"]');
      expect(await desktopNav.isVisible().catch(() => true)).toBeTruthy();
    }

    // หมายเหตุสำคัญ: ทั้ง mobile/desktop ต้องสามารถเข้าถึง content ได้
    const bookList = page.locator('[data-testid="book-list"]');
    expect(await bookList.isVisible()).toBeTruthy();
  });
});
```

3. **Run Tests:**

```bash
# Run on all browsers
npx playwright test

# Run on specific browser
npx playwright test --project=chromium
npx playwright test --project=firefox
npx playwright test --project=webkit

# Run mobile tests only
npx playwright test --project="Mobile Chrome"
npx playwright test --project="Mobile Safari"

# Show browser while testing (headed mode)
npx playwright test --headed

# Show detailed test UI
npx playwright test --ui
```

---

## Part 3: UAT Preparation (15 นาที)

### Exercise 4: Create UAT Test Scenarios

**Task 1: Write UAT Test Cases (10 นาที)**

สร้างไฟล์: `docs/uat-test-cases.md`

ตัวอย่าง UAT Test Case:

```markdown
# UAT Test Cases - Library Management System

## Test Session Information

- **Project**: Library Management System
- **UAT Phase**: Phase 1
- **Duration**: 2 weeks
- **Participants**: Library Staff (3 persons)
- **Environment**: UAT Server (http://uat.library.com)

---

## ตัวอย่าง UAT Test Cases

**หมายเหตุ:** Library-Management-v2 ไม่มี user registration endpoint ดังนั้น UAT test cases ควรทดสอบ features ที่มีจริงในแอปพลิเคชัน เช่น:

### ตัวอย่าง UAT Cases ที่ควรจะมี:

1. **UAT-01: User Login and Dashboard Access** (ตัวอย่างแล้ว ด้านบน)
2. **UAT-02: Browse Books and View Details** - User สามารถเปิด Books page, ค้นหาหนังสือ, ดูรายละเอียด
3. **UAT-03: Borrow Book** - User สามารถยืมหนังสือที่พร้อม (Available status)
4. **UAT-04: View Borrowed Books** - User สามารถเห็นรายการหนังสือที่ยืมและ due date
5. **UAT-05: Return Borrowed Book** - User สามารถคืนหนังสือและสถานะจะเปลี่ยนเป็น Available

**เพิ่มเติม:** หากต้องการเขียน UAT test case เพิ่มเติม ให้ทำตามแผนต่อไปนี้:

- เลือก feature ที่มีจริงในแอป
- เขียนจากมุมมองผู้ใช้ (ไม่ใช่ technical terms)
- รวม real test data ที่สามารถใช้ได้จริง
- ให้ Acceptance Criteria ชัดเจนและวัดได้
```

---

**Task 2: UAT Sign-off Template (5 นาที)**

สร้างไฟล์: `docs/uat-signoff.md`

ตัวอย่าง UAT Sign-off Template:

```markdown
# UAT Sign-off Document

## Project Information

- **Project Name**: Library Management System
- **UAT Phase**: Phase 1
- **UAT Period**: [Start Date] to [End Date]
- **UAT Environment**: http://uat.library.com

## UAT Summary

### Test Execution Statistics

| Metric           | Count |
| ---------------- | ----- |
| Total Test Cases | 25    |
| Executed         | 25    |
| Passed           | 23    |
| Failed           | 2     |
| Blocked          | 0     |
| Pass Rate        | 92%   |

### Defects Summary

| Severity | Count | Status   |
| -------- | ----- | -------- |
| Critical | 0     | -        |
| High     | 1     | Fixed    |
| Medium   | 3     | Fixed    |
| Low      | 5     | Accepted |

## Sign-off

### Business Stakeholders

| Name   | Role             | Signature  | Date           |
| ------ | ---------------- | ---------- | -------------- |
| [ชื่อ] | Library Director | **\_\_\_** | **_/_**/\_\_\_ |
| [ชื่อ] | Head Librarian   | **\_\_\_** | **_/_**/\_\_\_ |

### Project Team

| Name   | Role             | Signature  | Date           |
| ------ | ---------------- | ---------- | -------------- |
| [ชื่อ] | Project Manager  | **\_\_\_** | **_/_**/\_\_\_ |
| [ชื่อ] | QA Lead          | **\_\_\_** | **_/_**/\_\_\_ |
| [ชื่อ] | Development Lead | **\_\_\_** | **_/_**/\_\_\_ |

---
```

---

## 📝 Lab Deliverables - ต้องส่งอะไรบ้าง

### **รวมคะแนน: 10 คะแนน**

### 📌 **1. E2E Test Suite (4 คะแนน)**

**ไฟล์ที่ต้องสร้าง:**

- ✅ `tests/e2e/complete-user-journey.spec.js`
  - อย่างน้อย 2 complete user journeys ที่สมบูรณ์ (เช่น login → browse books → borrow → return และ login → view dashboard → logout)
  - Step ต้องชัดเจนและมี assertion ที่ถูกต้อง
  - (2 คะแนน)

- ✅ `tests/e2e/error-handling.spec.js`
  - อย่างน้อย 3 test cases ทดสอบจัดการข้อผิดพลาด
  - ตัวอย่าง: invalid login (wrong username/password), unavailable book (try to borrow when status is Unavailable), unauthorized access (try to access protected pages after logout)
  - (2 คะแนน)

### 📌 **2. Cross-Browser Testing (3 คะแนน)**

**ไฟล์ที่ต้องสร้าง:**

- ✅ `tests/e2e/cross-browser.spec.js`
  - ทดสอบอย่างน้อย 3 features บน 3 browsers (Chromium, Firefox, WebKit)
  - (1.5 คะแนน)

**ผลการทดสอบ:**

- ✅ `screenshots/` folder มีภาพ test results จาก:
  - `chromium-test-results.png`
  - `firefox-test-results.png`
  - `webkit-test-results.png`
  - (1.5 คะแนน)

**วิธีสร้างไฟล์ screenshot:**

Playwright สร้าง screenshots โดยอัตโนมัติเมื่อทดสอบ:

```bash
# รัน tests และจะสร้าง screenshots อัตโนมัติ
npx playwright test

# ตรวจสอบ test results
npx playwright show-report
```

หลังจากทดสอบแต่ละ browser ให้ทำสำเนา screenshots แล้ว rename:

```bash
# เมื่อรันบน Chromium
cp test-results/screenshot-chromium.png screenshots/chromium-test-results.png

# เมื่อรันบน Firefox
cp test-results/screenshot-firefox.png screenshots/firefox-test-results.png

# เมื่อรันบน WebKit
cp test-results/screenshot-webkit.png screenshots/webkit-test-results.png
```

### 📌 **3. UAT Documentation (3 คะแนน)**

**ไฟล์ที่ต้องสร้าง:**

- ✅ `docs/uat-test-cases.md`
  - อย่างน้อย 5 UAT Test Cases ที่เขียนจากมุมมองผู้ใช้
  - แต่ละ test case ต้องมี: Objective, Preconditions, Test Steps, Expected Results, Acceptance Criteria
  - (1.5 คะแนน)

**ตัวอย่าง UAT Test Case ที่เขียนเต็มไปแล้ว:**

```markdown
### UAT-01: User Login and Dashboard Access

**Objective**: ยืนยันว่าผู้ใช้สามารถเข้าสู่ระบบและเข้าถึง Dashboard ได้สำเร็จ

**Preconditions**:

- ระบบ Library Management ทำงานปกติ
- User account "admin" มีอยู่ในระบบ (admin / admin123)
- Network connection ดีปกติ

**Test Steps**:

1. เปิด browser และไปที่ http://localhost:3000
2. ระบบ redirect ไปหน้า Login โดยอัตโนมัติ
3. ใส่ username: "admin"
4. ใส่ password: "admin123"
5. คลิกปุ่ม "Login"
6. รอให้หน้า Dashboard โหลด
7. ตรวจสอบว่า Dashboard แสดง statistics ต่างๆ (Total Books, Available Books, etc.)

**Expected Results**:

1. Login page แสดงชัดเจนด้วย username/password fields
2. หลังกด Login: ระบบ redirect ไปหน้า Dashboard
3. Dashboard แสดง:
   - ยินดีต้อนรับผู้ใช้ด้วยชื่อหรือ role
   - สถิติของระบบ (Total Books, Available Books, Total Members, Borrowed Books)
   - Menu นำทาง (Books, Members, Borrowing, Reports)
4. URL เปลี่ยนจาก /login เป็น /dashboard
5. Time to load page ≤ 3 วินาที

**Acceptance Criteria**:

- ✅ Login สำเร็จด้วยข้อมูล admin/admin123
- ✅ Dashboard แสดง welcome message
- ✅ Navigation menu ทำงานปกติ
- ✅ สามารถคลิกเข้าไปยัง Books, Members, Borrowing pages ได้
- ✅ ไม่มี JavaScript errors ใน browser console
```

- ✅ `docs/uat-signoff.md`
  - Sign-off template พร้อมที่ให้ผู้ใช้เซ็นชื่อ
  - (1.5 คะแนน)

### 📌 **Configuration Files**

- ✅ `playwright.config.js` - ตั้งค่า Playwright สำหรับทดสอบ

### 📌 **Documentation**

- ✅ `README.md` - อธิบายวิธี run tests

### 📂 **โครงสร้างโฟลเดอร์ที่ต้องส่ง:**

```
lab10-submission/
├── tests/
│   └── e2e/
│       ├── complete-user-journey.spec.js   ✅ ต้องส่ง
│       ├── error-handling.spec.js          ✅ ต้องส่ง
│       └── cross-browser.spec.js           ✅ ต้องส่ง
├── docs/
│   ├── uat-test-cases.md                   ✅ ต้องส่ง
│   └── uat-signoff.md                      ✅ ต้องส่ง
├── screenshots/                            ✅ ต้องส่ง
│   ├── chromium-test-results.png
│   ├── firefox-test-results.png
│   └── webkit-test-results.png
├── playwright.config.js                    ✅ ต้องส่ง
└── README.md                               ✅ ต้องส่ง
```

### ✅ **Checklist ก่อนส่ง:**

- [ ] ทุก test รันได้และผ่าน `npx playwright test`
- [ ] Playwright config มี 3 browsers (chromium, firefox, webkit)
- [ ] ภาพหน้าจอ test results บน browsers ทั้ง 3 มี
- [ ] UAT test cases เขียนจากมุมมองผู้ใช้ (ไม่ใช่ technical)
- [ ] ทุกไฟล์และโฟลเดอร์ตามรายการข้างต้นมี

---

## 💡 Tips for Success

### E2E Testing Tips:

1. **ใช้ data-testid attributes** - ทำให้ tests stable และไม่เหลื่อมล้ำ
   - เหตุผล: `data-testid` เป็น attribute ที่สร้างเพื่อการทดสอบโดยเฉพาะ ไม่เปลี่ยนแปลงไปเมื่อ UI มีการเปลี่ยนแปลง
   - ตัวอย่าง:

     ```javascript
     // ❌ ไม่ดี - class อาจเปลี่ยนเมื่อ UI มีการเปลี่ยนแปลง
     await page.click(".btn.btn-primary.mt-2");

     // ✅ ดี - data-testid ทีมสร้างมาแล้ว
     await page.click('[data-testid="submit-button"]');
     ```

2. **รอให้ elements โหลด** - ใช้ `waitForSelector()` หรือ `waitForLoadState()`
3. **สร้าง unique test data** - ป้องกัน conflicts ระหว่าง test runs
4. **ทำความสะอาด (cleanup)** - ลบ test data หลังทดสอบ
   - ตัวอย่าง cleanup code:

     ```javascript
     test.afterEach(async ({ page }) => {
       // ลบ test user
       await page.goto("/admin/users");
       await page.click('[data-testid="delete-test-user"]');
       await page.click('[data-testid="confirm-delete"]');

       // ลบ test books
       await fetch("/api/books/test-*", { method: "DELETE" });
     });
     ```

5. **ทดสอบทั้ง happy path และ error cases**

### Cross-Browser Testing Tips:

1. **ไม่ใช้ browser-specific features** - ใช้ standard CSS/JavaScript
   - ตัวอย่าง browser-specific features ที่ต้องหลีกเลี่ยง:

     ```javascript
     // ❌ ไม่ดี - Safari specific
     element.style.webkitTransform = "scale(2)";

     // ✅ ดี - standard CSS
     element.style.transform = "scale(2)";

     // ❌ ไม่ดี - IE specific method
     document.attachEvent("onload", handler);

     // ✅ ดี - standard method
     document.addEventListener("load", handler);
     ```

2. **ทดสอบ responsive design** - Desktop, Tablet, Mobile ทีละตัว
   - วิธี: ใช้ device emulation ใน Playwright โดยอัตโนมัติ
   - playwright.config.js จะ run tests บน devices ที่กำหนดให้:
     ```javascript
     projects: [
       {
         name: "Desktop",
         use: { ...devices["Desktop Chrome"] },
       },
       {
         name: "Mobile",
         use: { ...devices["iPhone 12"] },
       },
       {
         name: "Tablet",
         use: { ...devices["iPad Pro"] },
       },
     ];
     ```
   - เขียน test แค่ครั้งเดียว Playwright จะรันอัตโนมัติบน devices ทั้งหมด
3. **ตรวจสอบ form handling** - แต่ละ browser render ต่างกัน
4. **ตรวจสอบ JavaScript compatibility** - บาง features อาจไม่รองรับ

### UAT Tips:

1. **เขียนจากมุมมองผู้ใช้** - ไม่ใช่ technical terms
2. **รวม real test data** - ให้ user ใช้ข้อมูลจริง
3. **Acceptance Criteria ต้องชัดเจน** - ไม่คลุมเครือ
   - ตัวอย่าง Acceptance Criteria ที่ดี:

     ```
     ❌ ไม่ดี: "ระบบต้อง search ได้เร็ว"

     ✅ ดี:
     - ผลลัพธ์ search ต้องแสดงภายใน 2 วินาที
     - ต้องแสดง book ทั้งหมดที่ชื่อ title หรือ author ตรงกับ search term
     - ถ้าไม่เจอ ต้องแสดงข้อความ "No books found" แทน
     - สามารถ filter ผลลัพธ์ได้ (by category, by availability)
     ```

4. **บันทึก Issues ให้ชัดเจน** - ให้ developers เข้าใจแน่ชัด

---

## 🔗 เอกสารอ้างอิง

- [Playwright Documentation](https://playwright.dev/)
- [Playwright Cross-Browser Testing](https://playwright.dev/docs/test-runners)
- [Device Emulation](https://playwright.dev/docs/emulation)
- [UAT Best Practices](https://www.softwaretestinghelp.com/user-acceptance-testing-uat/)
- [Writing Acceptance Criteria](https://www.atlassian.com/agile/project-management/user-stories)

---

## ❓ Common Issues & Solutions

### Issue 1: Tests timeout (Test รันนานเกินไป)

```javascript
test("workflow ที่ใช้เวลา", async ({ page }) => {
  test.setTimeout(60000); // 60 วินาที
  await page.waitForLoadState("networkidle");
});
```

### Issue 2: Element not found (ไม่พบ element)

**สาเหตุ:** Element ยังไม่ load หรือ selector ใช้ผิด

```javascript
// ❌ ปัญหา: selector ผิด
await page.click(".btn-submit"); // อาจมี button หลายตัว

// ✅ วิธีแก้ 1: ใช้ data-testid (แนะนำ)
await page.click('[data-testid="submit-button"]');

// ✅ วิธีแก้ 2: รอให้ element แสดง
await page
  .locator('[data-testid="submit-button"]')
  .waitFor({ state: "visible" });
await page.click('[data-testid="submit-button"]');

// ✅ วิธีแก้ 3: ใช้ more specific selector
await page.click('button:has-text("Submit")');
```

### Issue 3: Flaky tests (บางครั้งผ่านบางครั้งไม่ผ่าน)

**ความหมาย:** Flaky tests คือ test ที่บางครั้งผ่านบางครั้งไม่ผ่านแม้ว่า code ไม่มีการเปลี่ยนแปลง เกิดจากการ timing issues หรือ element ยังไม่พร้อม

**วิธีแก้:**

```javascript
// playwright.config.js
module.exports = {
  retries: 2, // พยายาม run ใหม่ 2 ครั้ง ถ้าไร้
  use: {
    actionTimeout: 10000, // รอ action สูงสุด 10 วินาที (default 5000ms)
    navigationTimeout: 30000, // รอ page load สูงสุด 30 วินาที (default 30000ms)
  },
};
```

**สาเหตุทั่วไป และวิธีแก้:**

1. Element ยังไม่เห็น

   ```javascript
   // ❌ ไม่ดี - อาจเร็วเกินไป
   await page.click('[data-testid="submit"]');

   // ✅ ดี - รอให้ element พร้อม
   await page.waitForSelector('[data-testid="submit"]');
   await page.click('[data-testid="submit"]');
   ```

2. Network ยังค้างอยู่
   ```javascript
   // ✅ รอให้ network เสร็จ
   await page.waitForLoadState("networkidle");
   ```

### Issue 4: Cross-browser failures

```javascript
// ใช้ selectors ที่ universal
// ❌ ไม่ดี: -webkit- CSS specific
// ✅ ดี: standard CSS
```

---
