## Week 11: Test Automation

### วัตถุประสงค์

- เขียน Unit tests ด้วย Jest
- เขียน UI/E2E tests โดยใช้ Playwright (Page Object Model)
- เขียน Integration tests สำหรับ API ด้วย supertest
- ติดตั้ง test coverage reporting
- ประยุกต์ใช้ best practices ในการเขียน automated tests
- ทำความเข้าใจกับ Library-Management-v2 test structure

### ความแตกต่างระหว่าง Testing Types

**Unit Testing (Unit tests)**

- ตรวจสอบ "ชิ้นเล็กๆ" ของโปรแกรม เช่น function หรือ method
- ใช้: Jest
- ตัวอย่าง: test validation function ที่ input "admin@example.com" ได้ result true
- ล้อม isolate - ไม่ต้องมี database หรือ server วิ่ง
- เร็วที่สุด

**UI/E2E Testing (End-to-End tests)**

- ทดสอบผ่านเบราว์เซอร์จริง - ผู้ใช้ใช้งานแบบไหน?
- ใช้: Playwright (หรือ Selenium, Cypress)
- ตัวอย่าง: เปิด login page → พิมพ์ username → พิมพ์ password → กด login → เช็คว่า redirect ไปหน้า dashboard
- ช้า (ต้องรอบราว์เซอร์ render + network response)
- ใกล้เคียงกับการใช้งานจริงมากที่สุด

**Integration Testing**

- ตรวจสอบการทำงาน "หลายๆ ส่วนร่วมกัน" เช่น API กับ database
- ใช้: Jest + Supertest
- ตัวอย่าง: fetch `/api/books` → ตรวจว่า database return books list ถูกต้อง
- Moderate speed - ต้อง server วิ่งแต่ไม่ต้องเบราว์เซอร์

### สรุป: เมื่อไหร่ใช้ test แบบไหน

| Test Type       | ใช้เมื่อ                                          | ความเร็ว  |
| --------------- | ------------------------------------------------- | --------- |
| Unit (Jest)     | ทดสอบ function เดี่ยว, logic ของ helper functions | ไว ที่สุด |
| UI (Playwright) | ทดสอบสิ่งที่ผู้ใช้ทำได้, user journey             | ช้าที่สุด |
| API (Supertest) | ทดสอบ API endpoints + database                    | ปานกลาง   |

**Best Practice**: ใช้ Unit tests มาก, UI tests น้อย (พีระมิด test)

---

## Part 1: Unit Testing & Basic API Tests ด้วย Jest

### Setup Jest - ติดตั้ง Jest

Jest ได้ติดตั้งแล้วใน Library-Management-v2:

```bash
# ติดตั้ง dependencies (ถ้ายังไม่ได้)
npm install

# ดูว่า jest.config.js มี setup ครบหรือไม่
cat jest.config.js

# รัน tests
npm test

# รัน tests พร้อมดู coverage
npm test -- --coverage

# รัน tests ใน watch mode
npm run test:watch
```

### Exercise 1.1: เข้าใจ Test Structure ใน Library-Management-v2

ตรวจสอบโครงสร้าง test files:

```
src/
  __tests__/
    Auth.test.js        # Authentication tests
    Books.test.js       # Books API tests
  app.js
  database.js
  controllers/
  routes/
  models/
```

### Exercise 1.2: สร้าง Unit Test สำหรับ Helper Functions

สร้างไฟล์ `src/__tests__/helpers.test.js` เพื่อ test utility functions (ฟังก์ชันช่วยเหลือ):

```javascript
// ตัวอย่าง test สำหรับ helper functions
describe("Helper Functions", () => {
  test("should validate email format", () => {
    const validEmail = "user@example.com";
    const invalidEmail = "invalid.email";

    // หากมี validator function ในโปรแกรม ให้เขียน test ตั้งแต่นี้
    // expect(isValidEmail(validEmail)).toBe(true);
    // expect(isValidEmail(invalidEmail)).toBe(false);
  });

  test("should format date correctly", () => {
    // ทดสอบ date formatting utility
  });
});
```

---

## Part 2: UI Automation ด้วย Playwright

### Exercise 2.1: ติดตั้ง Playwright (Setup Playwright)

**ติดตั้ง Playwright:**

```bash
# 1. Navigate to Library-Management-v2 folder
cd Library-Management-v2

# 2. Install Playwright
npm install --save-dev @playwright/test

# 3. Download browsers
npx playwright install

# 4. Create test directory
mkdir -p tests/e2e/{pages,specs,fixtures}
```

### Exercise 2.2: สร้าง Page Object Model (POM) - Pattern Design

สร้างไฟล์ต่อไปนี้:

**`tests/e2e/pages/BasePage.js` - Base class สำหรับทุก page:**

```javascript
class BasePage {
  constructor(page) {
    this.page = page;
    this.baseURL = "http://localhost:3000";
  }

  async goto(path = "/") {
    await this.page.goto(`${this.baseURL}${path}`);
  }

  async waitForPageLoad() {
    await this.page.waitForLoadState("networkidle");
  }

  async takeScreenshot(filename) {
    const dir = "tests/screenshots";
    if (!require("fs").existsSync(dir)) {
      require("fs").mkdirSync(dir, { recursive: true });
    }
    await this.page.screenshot({ path: `${dir}/${filename}.png` });
  }

  async getSuccessMessage() {
    const element = this.page.locator('[data-testid="success-message"]');
    if (await element.isVisible()) {
      return await element.textContent();
    }
    return null;
  }

  async getErrorMessage() {
    const element = this.page.locator('[data-testid="error-message"]');
    if (await element.isVisible()) {
      return await element.textContent();
    }
    return null;
  }
}

module.exports = { BasePage };
```

**`tests/e2e/pages/LoginPage.js` - Page Object สำหรับ login:**

```javascript
const { BasePage } = require("./BasePage");

class LoginPage extends BasePage {
  constructor(page) {
    super(page);
    this.usernameInput = page.locator('[data-testid="username"]');
    this.passwordInput = page.locator('[data-testid="password"]');
    this.loginButton = page.locator('[data-testid="login-button"]');
    this.registerLink = page.locator('[data-testid="register-link"]');
  }

  async goto() {
    await super.goto("/login");
    await this.waitForPageLoad();
  }

  async login(username, password) {
    await this.usernameInput.fill(username);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
    await this.waitForPageLoad();
  }

  async isLoginButtonEnabled() {
    return await this.loginButton.isEnabled();
  }

  async getLoginButtonStatus() {
    return (await this.loginButton.isEnabled()) ? "enabled" : "disabled";
  }

  async clickRegisterLink() {
    await this.registerLink.click();
  }
}

module.exports = { LoginPage };
```

**`tests/e2e/pages/BookPage.js` - Page Object สำหรับ books:**

```javascript
const { BasePage } = require("./BasePage");

class BookPage extends BasePage {
  constructor(page) {
    super(page);
    this.searchInput = page.locator('[data-testid="search-input"]');
    this.searchButton = page.locator('[data-testid="search-button"]');
    this.bookItems = page.locator(".book-item");
  }

  async goto() {
    await super.goto("/books");
    await this.waitForPageLoad();
  }

  async searchBooks(query) {
    await this.searchInput.fill(query);
    await this.searchButton.click();
    await this.waitForPageLoad();
  }

  async getBookCount() {
    return await this.bookItems.count();
  }

  async getFirstBook() {
    return await this.bookItems.first().textContent();
  }

  async borrowBook(index = 0) {
    const book = this.bookItems.nth(index);
    await book.locator('[data-testid^="borrow-"]').click();
    await this.waitForPageLoad();
  }
}

module.exports = { BookPage };
```

### Exercise 2.3: เขียน E2E Tests

**`tests/e2e/specs/login.spec.js` - Test login functionality:**

**บันทึก:**

- `test.describe()` = จัดกลุ่ม tests ที่เกี่ยวกับเรื่องเดียวกัน
- `test.beforeEach()` = ทำก่อนทุก test ใน describe นี้ (ใช้ลง setup เหมือนกันทุก test)
- `async/await` = รอให้งานเสร็จก่อนที่จะไปต่อ (เพราะ test บ้างทำช้า เช่นรอ page load)

```javascript
const { test, expect } = require("@playwright/test");
const { LoginPage } = require("../pages/LoginPage");

test.describe("Login Functionality", () => {
  let loginPage;

  test.beforeEach(async ({ page }) => {
    // ทำก่อนทุก test ใน describe นี้ - setup ข้อมูล/login
    loginPage = new LoginPage(page);
    await loginPage.goto();
  });

  test("should display login page correctly", async ({ page }) => {
    await expect(page).toHaveTitle(/Login|เข้าสู่ระบบ/i);
    await expect(loginPage.usernameInput).toBeVisible();
    await expect(loginPage.passwordInput).toBeVisible();
    await expect(loginPage.loginButton).toBeVisible();
  });

  test("should login successfully with valid credentials", async ({ page }) => {
    await loginPage.login("admin", "admin123");

    // ตรวจสอบว่า URL เปลี่ยนไปหน้า dashboard/home
    await expect(page).toHaveURL(/dashboard|home/i);

    // ตรวจสอบว่าแสดง success message
    const successMsg = await loginPage.getSuccessMessage();
    expect(successMsg).toBeTruthy();
  });

  test("should show error with invalid credentials", async ({ page }) => {
    await loginPage.login("wronguser", "wrongpass");

    // ยังอยู่ที่หน้า login
    await expect(page).toHaveURL(/login/i);

    // เห็น error message
    const errorMsg = await loginPage.getErrorMessage();
    expect(errorMsg).toBeTruthy();
    expect(errorMsg).toContain("Invalid");
  });

  test("login button should be disabled when fields empty", async () => {
    const buttonStatus = await loginPage.getLoginButtonStatus();
    expect(buttonStatus).toBe("disabled");

    // กรอก username ลงในช่อง input
    await loginPage.usernameInput.fill("testuser");
    const statusAfterUsername = await loginPage.getLoginButtonStatus();
    expect(statusAfterUsername).toBe("disabled");

    // กรอก password ลงในช่อง input
    await loginPage.passwordInput.fill("password123");
    const statusAfterPassword = await loginPage.getLoginButtonStatus();
    expect(statusAfterPassword).toBe("enabled");
  });

  test("should navigate to register page", async ({ page }) => {
    await loginPage.clickRegisterLink();
    await expect(page).toHaveURL(/register|signup/i);
  });
});
```

**`tests/e2e/specs/books.spec.js` - Test books functionality:**

```javascript
const { test, expect } = require("@playwright/test");
const { LoginPage } = require("../pages/LoginPage");
const { BookPage } = require("../pages/BookPage");

test.describe("Books Page", () => {
  let bookPage;

  test.beforeEach(async ({ page }) => {
    // Login first
    const loginPage = new LoginPage(page);
    await loginPage.goto();
    await loginPage.login("admin", "admin123");

    // Go to books
    bookPage = new BookPage(page);
    await bookPage.goto();
  });

  test("should display list of books", async () => {
    const bookCount = await bookPage.getBookCount();
    expect(bookCount).toBeGreaterThan(0);
  });

  test("should search books", async ({ page }) => {
    await bookPage.searchBooks("JavaScript");

    const bookCount = await bookPage.getBookCount();
    expect(bookCount).toBeGreaterThan(0);

    // ตรวจสอบว่า text มีคำค้นหา 'JavaScript'
    const firstBook = await bookPage.getFirstBook();
    expect(firstBook.toLowerCase()).toContain("javascript");
  });

  test("should borrow book successfully", async () => {
    await bookPage.borrowBook(0);

    // ตรวจสอบว่าแสดง success message
    const successMsg = await bookPage.getSuccessMessage();
    expect(successMsg).toBeTruthy();
    expect(successMsg).toContain("success");
  });

  test("complete user journey: search and borrow", async ({ page }) => {
    // ค้นหา
    await bookPage.searchBooks("Python");
    expect(await bookPage.getBookCount()).toBeGreaterThan(0);

    // ยืม
    await bookPage.borrowBook(0);
    const successMsg = await bookPage.getSuccessMessage();
    expect(successMsg).toBeTruthy();

    // เช็ค screenshot
    await bookPage.takeScreenshot("complete-journey");
  });
});
```

### Exercise 2.4: เขียน Playwright Configuration

**`playwright.config.js`:**

```javascript
const { defineConfig, devices } = require("@playwright/test");

module.exports = defineConfig({
  testDir: "./tests/e2e/specs",
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: "html",
  use: {
    baseURL: "http://localhost:3000",
    trace: "on-first-retry",
    screenshot: "only-on-failure",
    video: "retain-on-failure",
  },

  projects: [
    {
      name: "chromium",
      use: { ...devices["Desktop Chrome"] },
    },
    {
      name: "firefox",
      use: { ...devices["Desktop Firefox"] },
    },
    {
      name: "webkit",
      use: { ...devices["Desktop Safari"] },
    },
  ],

  webServer: {
    // ตั้งค่า webServer: บอกให้ Playwright รู้ว่าจะรันเซิร์ฟเวอร์ไหน (npm start จะรัน node server)
    command: "npm start",
    url: "http://localhost:3000", // ที่อยู่ของ server ที่รัน
    reuseExistingServer: !process.env.CI, // ในการทดสอบท้องถิ่น ให้ใช้ server เดิม ไม่สร้างใหม่เต่า run
  },
});
```

### Running Playwright Tests:

```bash
# Install dependencies first
npm install --save-dev @playwright/test

# Run all tests
npx playwright test

# Run specific test file
npx playwright test tests/e2e/specs/login.spec.js

# Run in UI mode (recommended)
npx playwright test --ui

# Run in headed mode (see browser)
npx playwright test --headed

# Generate HTML report
npx playwright show-report
```

---

## Part 3: Integration API Testing ด้วย Supertest

**คำอธิบาย:** Integration tests คือการทดสอบการทำงานของหลาย ๆ ส่วนร่วมกัน เช่น การสื่อสารกับ database ผ่าน API

- **Session Cookie:** เก็บข้อมูลการ login ของผู้ใช้ ใช้แทน token
- **Supertest:** ไลบรารี่สำหรับทดสอบ HTTP API
- **CRUD:** Create, Read, Update, Delete - การดำเนินการพื้นฐาน

### Exercise 3.1: Basic CRUD API Tests

สร้างไฟล์ `src/__tests__/Books.test.js`:

**บันทึก:**

- `beforeAll()` = ทำ 1 ครั้งตอนเริ่มทดสอบ (ใช้สำหรับ setup ครั้งเดียว เช่น login, สร้าง test data)
- `describe()` = จัดกลุ่ม tests ที่เกี่ยวกับเรื่องเดียวกัน
- `test()` = 1 test case (ทดสอบอย่างไร 1 อย่าง)
- `expect()` = พูดว่า "ฉันคาดหวัง..." (เช่น expect result คือ 201, expect object มี property)

```javascript
const request = require("supertest");
const app = require("../app");

describe("Book API - CRUD Operations", () => {
  let sessionCookie;
  let createdBookId;

  beforeAll(async () => {
    // ขั้นเตรียมการ: ทำการเข้าระบบเพื่อรับ session cookie
    const response = await request(app)
      .post("/api/auth/login")
      .send({ username: "admin", password: "admin123" });

    const cookies = response.headers["set-cookie"];
    // เก็บ cookie ไว้ใช้ในการทดสอบส่วนอื่น
    sessionCookie = cookies ? cookies[0].split(";")[0] : "";
  });

  describe("POST /api/books", () => {
    test("should create a new book with valid data", async () => {
      // ทดสอบการสร้างหนังสือใหม่ด้วยข้อมูลที่ถูกต้อง
      const newBook = {
        title: "Advanced JavaScript",
        author: "Kyle Simpson",
        isbn: "978-1491954622",
        category: "Programming",
        total_copies: 5,
      };

      const response = await request(app)
        .post("/api/books")
        .set("Cookie", sessionCookie)
        .send(newBook);

      expect(response.status).toBe(201);
      expect(response.body).toHaveProperty("book_id");
      expect(response.body.title).toBe(newBook.title);
      expect(response.body.author).toBe(newBook.author);

      createdBookId = response.body.book_id;
    });

    test("should return 400 with missing required fields", async () => {
      const invalidBook = {
        title: "Test Book",
        // Missing author and other required fields
      };

      const response = await request(app)
        .post("/api/books")
        .set("Cookie", sessionCookie)
        .send(invalidBook);

      expect(response.status).toBe(400);
      expect(response.body).toHaveProperty("error");
    });

    test("should return 401 without authentication", async () => {
      const newBook = {
        title: "Test Book",
        author: "Test Author",
        total_copies: 2,
      };

      const response = await request(app).post("/api/books").send(newBook);

      expect(response.status).toBe(401);
    });
  });

  describe("GET /api/books", () => {
    test("should return list of all books", async () => {
      const response = await request(app)
        .get("/api/books")
        .set("Cookie", sessionCookie);

      expect(response.status).toBe(200);
      expect(Array.isArray(response.body)).toBe(true);
      expect(response.body.length).toBeGreaterThan(0);
    });

    test("should return books with correct fields", async () => {
      const response = await request(app)
        .get("/api/books")
        .set("Cookie", sessionCookie);

      expect(response.status).toBe(200);
      const book = response.body[0];
      expect(book).toHaveProperty("book_id");
      expect(book).toHaveProperty("title");
      expect(book).toHaveProperty("author");
      expect(book).toHaveProperty("available_copies");
    });

    test("should search books by title", async () => {
      const response = await request(app)
        .get("/api/books/search")
        .query({ q: "JavaScript" })
        .set("Cookie", sessionCookie);

      expect(response.status).toBe(200);
      expect(Array.isArray(response.body)).toBe(true);
      if (response.body.length > 0) {
        response.body.forEach((book) => {
          const titleLower = book.title.toLowerCase();
          expect(titleLower).toContain("javascript");
        });
      }
    });
  });

  describe("GET /api/books/:id", () => {
    test("should return book details by ID", async () => {
      const response = await request(app)
        .get(`/api/books/${createdBookId}`)
        .set("Cookie", sessionCookie);

      expect(response.status).toBe(200);
      expect(response.body.book_id).toBe(createdBookId);
      expect(response.body).toHaveProperty("title");
      expect(response.body).toHaveProperty("author");
    });

    test("should return 404 for non-existent book", async () => {
      const response = await request(app)
        .get("/api/books/99999")
        .set("Cookie", sessionCookie);

      expect(response.status).toBe(404);
      expect(response.body).toHaveProperty("error");
    });
  });

  describe("PUT /api/books/:id", () => {
    test("should update book with valid data", async () => {
      const updates = {
        title: "Updated Advanced JavaScript",
        total_copies: 10,
      };

      const response = await request(app)
        .put(`/api/books/${createdBookId}`)
        .set("Cookie", sessionCookie)
        .send(updates);

      expect(response.status).toBe(200);
      expect(response.body.title).toBe(updates.title);
      expect(response.body.total_copies).toBe(10);
    });

    test("should return 404 for non-existent book", async () => {
      const response = await request(app)
        .put("/api/books/99999")
        .set("Cookie", sessionCookie)
        .send({ title: "Test" });

      expect(response.status).toBe(404);
    });
  });

  describe("DELETE /api/books/:id", () => {
    test("should delete book successfully", async () => {
      const response = await request(app)
        .delete(`/api/books/${createdBookId}`)
        .set("Cookie", sessionCookie);

      expect(response.status).toBe(200);

      // Verify deletion
      const getResponse = await request(app)
        .get(`/api/books/${createdBookId}`)
        .set("Cookie", sessionCookie);
      expect(getResponse.status).toBe(404);
    });

    test("should return 404 for already deleted book", async () => {
      const response = await request(app)
        .delete(`/api/books/${createdBookId}`)
        .set("Cookie", sessionCookie);

      expect(response.status).toBe(404);
    });
  });
});
```

### Exercise 3.2: Advanced API Tests - Borrowing

สร้างไฟล์ `src/__tests__/Borrowing.test.js`:

```javascript
const request = require("supertest");
const app = require("../app");

describe("Book Borrowing API", () => {
  let sessionCookie;
  let adminCookie;
  let bookId;
  let memberId;
  let borrowId;

  beforeAll(async () => {
    // Setup: Login as admin to create test data
    const adminLoginResponse = await request(app)
      .post("/api/auth/login")
      .send({ username: "admin", password: "admin123" });

    const adminCookies = adminLoginResponse.headers["set-cookie"];
    adminCookie = adminCookies ? adminCookies[0].split(";")[0] : "";

    // Create a test book with multiple copies
    const bookResponse = await request(app)
      .post("/api/books")
      .set("Cookie", adminCookie)
      .send({
        title: "Test Book for Borrowing",
        author: "Test Author",
        isbn: "TEST-BORROW-001",
        category: "Testing",
        total_copies: 3,
      });

    bookId = bookResponse.body.book_id;

    // Login as regular user for borrowing tests
    const userLoginResponse = await request(app)
      .post("/api/auth/login")
      .send({ username: "librarian", password: "lib123" });

    const userCookies = userLoginResponse.headers["set-cookie"];
    sessionCookie = userCookies ? userCookies[0].split(";")[0] : "";

    // Get member ID from logged-in user
    const meResponse = await request(app)
      .get("/api/auth/me")
      .set("Cookie", sessionCookie);

    memberId = meResponse.body.user_id || meResponse.body.member_id;
  });

  describe("POST /api/borrowing - Borrow Book", () => {
    test("should borrow book successfully", async () => {
      const response = await request(app)
        .post("/api/borrowing")
        .set("Cookie", sessionCookie)
        .send({ book_id: bookId });

      expect(response.status).toBe(201);
      expect(response.body).toHaveProperty("borrow_id");
      expect(response.body).toHaveProperty("book_id", bookId);
      expect(response.body).toHaveProperty("member_id");
      expect(response.body).toHaveProperty("borrowed_date");
      expect(response.body).toHaveProperty("due_date");

      borrowId = response.body.borrow_id;
    });

    test("should not allow borrowing same book twice", async () => {
      const response = await request(app)
        .post("/api/borrowing")
        .set("Cookie", sessionCookie)
        .send({ book_id: bookId });

      expect(response.status).toBe(409);
      expect(response.body).toHaveProperty("error");
    });
  });

  describe("GET /api/borrowing/borrowed - Get Borrowed Books", () => {
    test("should return list of books borrowed by user", async () => {
      const response = await request(app)
        .get("/api/borrowing/borrowed")
        .set("Cookie", sessionCookie);

      expect(response.status).toBe(200);
      expect(Array.isArray(response.body)).toBe(true);
      expect(response.body.length).toBeGreaterThan(0);

      const borrowed = response.body.find((b) => b.borrow_id === borrowId);
      expect(borrowed).toBeDefined();
      expect(borrowed.book_id).toBe(bookId);
    });
  });

  describe("PUT /api/borrowing/:borrowId/return - Return Book", () => {
    test("should return book successfully", async () => {
      const response = await request(app)
        .put(`/api/borrowing/${borrowId}/return`)
        .set("Cookie", sessionCookie)
        .send({});

      expect(response.status).toBe(200);
      expect(response.body).toHaveProperty("borrow_id", borrowId);
      expect(response.body).toHaveProperty("returned_date");
    });

    test("should return 404 for non-existent borrow record", async () => {
      const response = await request(app)
        .put("/api/borrowing/99999/return")
        .set("Cookie", sessionCookie)
        .send({});

      expect(response.status).toBe(404);
      expect(response.body).toHaveProperty("error");
    });
  });

  describe("GET /api/borrowing/overdue - Get Overdue Books", () => {
    test("should return overdue borrowing records", async () => {
      const response = await request(app)
        .get("/api/borrowing/overdue")
        .set("Cookie", sessionCookie);

      expect(response.status).toBe(200);
      expect(Array.isArray(response.body)).toBe(true);
    });
  });

  describe("PUT /api/borrowing/:borrowId/extend - Extend Due Date", () => {
    test("should extend due date for borrowed book", async () => {
      // First borrow a book again
      const borrowResponse = await request(app)
        .post("/api/borrowing")
        .set("Cookie", sessionCookie)
        .send({ book_id: bookId });

      const newBorrowId = borrowResponse.body.borrow_id;

      // Then extend it
      const response = await request(app)
        .put(`/api/borrowing/${newBorrowId}/extend`)
        .set("Cookie", sessionCookie)
        .send({});

      expect(response.status).toBe(200);
      expect(response.body).toHaveProperty("borrow_id", newBorrowId);
      expect(response.body).toHaveProperty("due_date");
    });
  });

  describe("GET /api/borrowing/member/:memberId - Get Member Borrows", () => {
    test("should return borrowing records for specific member", async () => {
      const response = await request(app)
        .get(`/api/borrowing/member/${memberId}`)
        .set("Cookie", adminCookie);

      expect(response.status).toBe(200);
      expect(Array.isArray(response.body)).toBe(true);
    });
  });
});
```

### Running Tests:

**ก่อนรัน tests** ให้แน่ใจว่า:

1. อยู่ในโฟลเดอร์ `Library-Management-v2`
2. ติดตั้ง dependencies: `npm install`
3. เริ่มต้น database ตามขั้นตอน

ไฟล์ package.json ได้ setup scripts สำหรับ testing (ใช้สำหรับทั้ง Jest tests และ Supertest):

```bash
# รัน Jest tests ทั้งหมด (ทั้ง Part 1 Unit tests และ Part 3 API tests)
npm test

# รัน test แบบ watch (เมื่อไฟล์เปลี่ยน จะรันใหม่อัตโนมัติ)
npm run test:watch

# รัน test พร้อมดู coverage (เปอร์เซนต์การครอบคลุม - แสดงว่า code ไหนถูก test แล้ว)
npm test -- --coverage

# รัน test ไฟล์เดียว (เช่น Books.test.js)
npm test -- src/__tests__/Books.test.js
```

## 📊 Lab Deliverables - สิ่งที่ต้องส่ง

ส่งเอกสารต่อไปนี้:

### 1. Source Code (60%)

**Part 1: Unit Testing with Jest:**

- Unit test examples (helpers.test.js)
- Jest configuration (jest.config.js)

**Part 2: UI Automation (Playwright):**

- Page Object classes (BasePage.js, LoginPage.js, BookPage.js)
- E2E test specs (login.spec.js, books.spec.js)
- Playwright configuration (playwright.config.js)

**Part 3: API Integration Tests (Supertest):**

- Integration API tests (Books.test.js, Borrowing.test.js)

### 2. Test Execution Report (20%)

**Playwright Reports:**

- HTML test report (screenshot)
- Video recordings of failed tests
- Screenshots of test execution

**Jest Reports:**

- Jest test output (screenshot)
- Coverage report (screenshot)

**Summary of test results:**

- Total Playwright tests: X, Passed: X, Failed: X
- Total Jest tests: X, Passed: X, Failed: X
- Code coverage: X%
- Execution time

### 3. Documentation (20%)

สร้างไฟล์ `LAB11_REPORT.md` ที่มี:

```markdown
# Lab 11 Report: Test Automation

## Student Information

- Name: [Your Name]
- ID: [Your ID]
- Date: [Date]

## Part 2: UI Automation with Playwright

### Setup

- Describe Playwright installation steps
- Explain POM (Page Object Model) design

### Test Cases Implemented

1. Login Tests (login.spec.js):
   - [test case name]
   - [test case name]

2. Books Tests (books.spec.js):
   - [test case name]
   - [test case name]

### Challenges & Solutions

- Challenge 1: [describe] - Solution: [describe]

### Screenshots/Videos

- Include screenshots of test execution
- Include video of failed tests (if any)

## Part 3: API Integration Testing

### Test Setup

- Describe Jest configuration
- Explain test data setup

### Test Cases Implemented

1. Books CRUD (Books.test.js): [list test cases]
2. Borrowing API (Borrowing.test.js): [list test cases]

### Test Results

- Total tests: X
- Passed: X, Failed: X
- Coverage: X%

## Comparison & Analysis

- Which is faster: Playwright or Jest?
- Which is easier to maintain?
- Recommendations for your project

## Lessons Learned

1. [Learning 1]
2. [Learning 2]
3. [Learning 3]
```

---

## 🎯 Grading Rubric - เกณฑ์การประเมิน

| Criteria                  | Points  | Description                                      |
| ------------------------- | ------- | ------------------------------------------------ |
| **Jest Unit Tests**       | 15      | Helper function tests, test setup                |
| **Playwright UI Tests**   | 15      | POM design, test coverage, assertions            |
| **API Integration Tests** | 15      | CRUD operations, error handling, authentication  |
| **Test Coverage**         | 10      | Code coverage > 70%, all main functions tested   |
| **Code Quality**          | 15      | Clean code, DRY principle, best practices        |
| **Test Results**          | 15      | All tests pass, no flaky tests, stable execution |
| **Documentation**         | 10      | Complete report, clear explanations, screenshots |
| **Total**                 | **100** |                                                  |

---

## 💡 Tips & Best Practices - เคล็ดลับและแนวปฏิบัติ

**คำศัพท์สำคัญ:**

- **expect()** = คาด (เช่น expect(5) = 5)
- **toHaveProperty()** = มีคุณสมบัติ (เช่น object มี field \"name\")
- **toBe()** = เป็น/เท่ากับ (ตรงตัว)
- **beforeAll()** = ทำ 1 ครั้งแบบเริ่มต้น (ก่อนทั้งหมด)
- **afterAll()** = ทำ 1 ครั้งแบบปิดท้าย (หลังทั้งหมด - cleanup/ลบข้อมูล)

### API Testing Tips:

1. **Test Coverage**: ทดสอบทั้งกรณีสำเร็จ (happy path) และกรณีล้มเหลว (error cases)
   - ตัวอย่าง: POST book สำเร็จ vs POST book ไม่เข้าระบบ
2. **Setup & Teardown**: เก็บความสะอาด - ลบข้อมูล test หลังจาก test จบ (cleanup)
3. **Mock Data**: ใช้ข้อมูลจริงที่เหมือนข้อมูลในระบบจริง (ไม่ใช่ข้อมูลแปลกๆ)
4. **Authentication**: อย่าลืม set cookie ตั้งแต่เริ่ม test
5. **Response Validation**: ตรวจสอบทั้ง status code และ ข้อมูลที่ส่งกลับ
6. **Isolation**: Test แต่ละตัวต้องเป็นอิสระ ไม่ขึ้นตรงกับ test อื่น
7. **Assertions**: ใช้ assertion ที่ชัดเจน เช่น `toHaveProperty()` ไม่ใช่ `toBeTruthy()`

### Common Mistakes to Avoid - ความผิดพลาดทั่วไป:

1. ไม่ลบข้อมูล test → ทำให้ test ครั้งที่ 2 ล้มเหลว
   - แก้: ใช้ `afterAll()` ลบข้อมูล
2. test ขึ้นอยู่กับลำดับการรัน → ยากแก้บग
   - แก้: ทุก test ต้องอิสระ เรียงตามลำดับก็ได้ ลำดับไม่ได้ก็ได้
3. assertion เกินไปนิ่ม → ไม่จับบังเอิญ
   - แก้: `expect(isValid).toBe(true)` ดีกว่า `expect(isValid).toBeTruthy()`
4. ไม่ test error cases → ไม่รู้ปัญหาที่ซ่อนอยู่
   - แก้: test failure case ด้วย
5. hard-code ค่า → ยากแก้ไข
   - แก้: ใช้ variable แล้วเปลี่ยนที่จุดเดียว
6. ลืม set cookie → test ครั้งต่อจะล้มเหลว
   - แก้: ตั้ง cookie ใน `beforeAll()`

---

## 🚀 Challenges (Optional) - ท้าทายเพิ่มเติม

### Bonus 1: Test Coverage Analysis

- อัพโหลด coverage report ที่ติดตั้ง `--coverage` flag
- ระบุส่วนไหนของ code ที่ยังไม่ได้ test
- เขียน test เพิ่มเติมเพื่อให้ coverage ≥ 80%

### Bonus 2: Error Case Testing

- เพิ่ม test cases สำหรับ edge cases (ข้อมูลไม่ครบ, ค่าผิดพลาด เช่น negative number, empty string)
- Test all HTTP status codes (400 = bad request, 401 = unauthorized, 404 = not found, 409 = conflict, 500 = server error)
- ตรวจสอบว่า error messages อธิบายปัญหาชัดเจน

### Bonus 3: Performance Testing

- Measure API response times
- Test with concurrent requests
- Document performance metrics

---

## � Troubleshooting - แก้ไขปัญหาทั่วไป

### ปัญหา: "Cannot find module '@playwright/test'"

**สาเหตุ**: ยังไม่ติดตั้ง Playwright

**แก้ไข**:

```bash
npm install --save-dev @playwright/test
npx playwright install
```

### ปัญหา: "ECONNREFUSED - Connection refused at 127.0.0.1:3000"

**สาเหตุ**: Server ไม่วิ่ง (test ต้องการ server)

**แก้ไข**:

```bash
# Terminal 1: รัน server
npm start

# Terminal 2: รัน tests
npm test
```

หรือใช้ Playwright's webServer config (มี playwright.config.js อยู่แล้ว)

### ปัญหา: "Test failed: element not found"

**สาเหตุ**: Locator ไม่ถูกต้อง หรือ element ยังไม่ load

**แก้ไข**:

1. ใช้ `--headed` flag เพื่อเห็นสิ่งที่ Playwright ทำ: `npx playwright test --headed`
2. ใช้ `--debug` เพื่อ pause และ step through: `npx playwright test --debug`
3. ตรวจสอบ HTML จริงว่า element มี `data-testid` ตรงกับ locator ไหม

### ปัญหา: "beforeAll failed - tests skipped"

**สาเหตุ**: setUp ล้มเหลว (เช่น login ล้มเหลว) ทำให้ test อื่นข้ามไป

**แก้ไข**:

1. ตรวจดู console output ว่า error คืออะไร
2. ลอง run test เดี่ยว: `npm test -- src/__tests__/Books.test.js`
3. ตรวจว่า test data พร้อม (database มีข้อมูล test หรือหลัก)

### ปัญหา: "Tests passed locally but failed in CI"

**สาเหตุ**: ทำให้ test "flaky" (บังเอิญ pass/fail) - มักเป็นเรื่อง timing

**แก้ไข**:

1. เพิ่ม timeout: `jest.setTimeout(15000)` หรือ `npx playwright test --timeout=15000`
2. เพิ่ม wait: `await page.waitForLoadState('networkidle')`
3. ลบข้อมูล test ทั้งหมด (cleanup) ก่อน run ใหม่

---

- [Jest Documentation](https://jestjs.io/)
- [Supertest Documentation](https://github.com/ladjs/supertest)
- [Testing Best Practices](https://testingjavascript.com/)
- [Library-Management-v2 README](../Library-Management-v2/README.md)
- [Library-Management-v2 TESTING_GUIDE](../Library-Management-v2/TESTING_GUIDE.md)

---
