ข้ามไปที่เนื้อหา

บทที่ 3: Playwright Architecture — Browser, BrowserContext, Page

ก่อนอ่านบทนี้ ลองตอบ:

  1. ทำไม Playwright ถึงต้องใช้ TypeScript แทน JavaScript ธรรมดา? ข้อได้เปรียบหลักคืออะไร?
  2. ใน playwright.config.ts ถ้าคุณต้องการให้ test รันบน Chromium เท่านั้น คุณต้องแก้ไข section ใด?

เฉลย:

  1. TypeScript ให้ autocomplete, type checking ตอน compile-time (ไม่ใช่ runtime), และ refactoring ที่ปลอดภัยกว่า — @playwright/test รวม type definitions ไว้ครบในตัว ทำให้ IDE รู้ว่า page.goto() รับ argument อะไร และเตือนถ้าพิมพ์ผิดก่อนรัน test
  2. แก้ใน projects array ภายใน playwright.config.ts — เหลือแค่ { name: 'chromium', use: { ...devices['Desktop Chrome'] } } แล้วลบ project อื่นออก

1. วัตถุประสงค์

หลังอ่านบทนี้คุณจะ:

  • อธิบายความสัมพันธ์ระหว่าง Browser, BrowserContext, และ Page ได้ถูกต้อง
  • เข้าใจว่าทำไม test ของ Playwright ถึง isolated กันโดยอัตโนมัติโดยไม่ต้องเขียน setup/teardown เอง
  • รู้ว่าเมื่อไหร่ควรใช้ page fixture, context fixture, และ browser fixture — และความแตกต่างระหว่างกัน
  • เขียน test ที่ simulate 2 users login พร้อมกันได้ โดยใช้ BrowserContext
  • วิเคราะห์ได้ว่า test ที่ "บางครั้งผ่านบางครั้งพัง" เกิดจาก state leak ระหว่าง context หรือไม่
  • อธิบายได้ว่าทำไม Playwright รัน test ได้เร็วกว่า Selenium ทั้งที่ test จำนวนมากขึ้น

2. ทำไมต้องรู้? (Why)

สมมติคุณเขียน test นี้:

test('login as admin', async ({ page }) => {
  await page.goto('http://localhost:3000/login');
  await page.fill('[data-testid="input-username"]', 'admin');
  await page.fill('[data-testid="input-password"]', 'admin123');
  await page.click('[data-testid="btn-login"]');
  await expect(page.locator('[data-testid="session-badge"]')).toContainText('admin');
});

test('homepage shows guest view', async ({ page }) => {
  await page.goto('http://localhost:3000');
  await expect(page.locator('[data-testid="session-badge"]')).toContainText('Not logged in');
});

คำถามคือ: test ที่ 2 จะผ่านหรือเปล่า? ถ้ามาจาก Selenium คุณอาจกลัวว่า session จาก test แรก (login admin) จะ "รั่ว" มา test ที่สอง แล้วทำให้ session badge แสดง "admin"

คำตอบคือ: ผ่านเสมอ เพราะ Playwright สร้าง BrowserContext ใหม่ก่อนทุก test โดยอัตโนมัติ — cookies, localStorage, sessionStorage ทุกอย่างเริ่มใหม่จากศูนย์ badge จึงแสดง "Not logged in" แทนที่จะเป็น "admin"

แต่ถ้าคุณไม่เข้าใจว่า BrowserContext คืออะไร คุณจะไม่รู้ว่า isolation นี้เกิดขึ้นที่ระดับไหน หรือจะทำลายมันได้อย่างไร (โดยไม่ตั้งใจ) และเมื่อไหร่ต้องจัดการ context เอง

นี่คือเหตุผลที่ต้องเข้าใจ architecture 3 layer นี้ก่อนเขียน test จริง


3. เนื้อหาหลัก

3.1 Browser — เปิดครั้งเดียว ใช้ได้ทั้ง suite

Browser ใน Playwright คือโปรแกรม Chromium, Firefox, หรือ WebKit ที่รันอยู่จริงบนเครื่อง การเปิด browser ขึ้นมาแต่ละครั้งใช้เวลาหลายร้อย millisecond เพราะ OS ต้องโหลดโปรแกรมขนาดใหญ่และจัดสรร memory

Playwright แก้ปัญหานี้ด้วยการ เปิด browser ครั้งเดียว แล้วใช้ร่วมกันทุก test — test 100 ตัวใช้ browser เดิม ไม่ได้เปิด/ปิด 100 รอบแบบ Selenium

"Browsers are shared across tests to optimize resources." — playwright.dev/docs/test-fixtures

3.2 BrowserContext — Isolation Engine

BrowserContext คือหัวใจของความ isolated ใน Playwright

"BrowserContexts which are equivalent to incognito-like profiles. They are fast and cheap to create and are completely isolated, even when running in a single browser." — playwright.dev/docs/browser-contexts

แต่ละ BrowserContext มีของของตัวเองแยกกันสมบูรณ์: - Cookies — login session, tracking cookies - localStorage และ sessionStorage — state ที่ JavaScript เก็บไว้ - Auth state — HTTP authentication headers - Cache — resources ที่ browser เก็บไว้เพื่อเร็วขึ้น

"each test has its own local storage, session storage, cookies etc." — playwright.dev/docs/browser-contexts

และที่สำคัญมาก: BrowserContext สร้างเร็วและเบา เพราะ Playwright ไม่ต้องเปิด browser process ใหม่ แค่สร้าง isolated profile ใหม่บน browser เดิม

3.3 Page — หนึ่ง Tab ใน Context

"A Page refers to a single tab or a popup window within a browser context." — playwright.dev/docs/pages

Page คือสิ่งที่คุณ interact ด้วยมากที่สุด — นี่คือ object ที่คุณใช้ page.goto(), page.click(), page.fill() ทุกอย่าง

Page ผูกอยู่กับ BrowserContext ที่สร้างมัน และ share state กับ page อื่นใน context เดียวกัน:

"Pages inside a context respect context-level emulation, like viewport sizes, custom network routes or browser locale." — playwright.dev/docs/pages

3.4 Fixture คืออะไร และ Default Fixtures ที่ Playwright เตรียมให้

Fixture คืออะไร?

Fixture คือ resource ที่ Playwright เตรียมให้ก่อน test เริ่ม และเก็บกวาดให้เองหลัง test จบ โดยที่คุณไม่ต้องเขียน setup/teardown เอง

ลองดูโค้ด test ทั่วไป:

test('my test', async ({ page }) => {
  //                    ↑
  //            ตรงนี้แหละคือ fixture
  //            Playwright inject page มาให้อัตโนมัติ
  await page.goto('http://localhost:3000');
});

{ page } ในวงเล็บหลัง async คือการบอก Playwright ว่า "ฉันต้องการ page — ช่วยเตรียมมาให้หน่อย" Playwright จะสร้าง BrowserContext ใหม่ เปิด Page ใน context นั้น ส่งให้คุณใช้ใน test แล้วปิดทุกอย่างให้หลัง test จบ

เทียบกับ Robot Framework ที่ต้องเขียน:

*** Test Cases ***
My Test
    [Setup]    Open Browser    http://localhost:3000    Chrome
    ...
    [Teardown]    Close Browser

Playwright ทำส่วน Setup และ Teardown ให้ผ่าน fixture อัตโนมัติ


Default Fixtures ที่ใช้บ่อย:

page fixture — ใช้ 90% ของเวลา

test('example', async ({ page }) => {
  // page คือ 1 browser tab ที่สะอาดใหม่
  // Playwright สร้างมาให้ก่อน test เริ่ม
  // และ close ให้หลัง test จบ — ไม่ต้องเขียนอะไรเพิ่ม
  await page.goto('http://localhost:3000');
});

"Playwright creates a context for each test, and provides a default Page in that context." — playwright.dev/docs/browser-contexts

context fixture — ใช้เมื่อต้องการเปิดหลาย tab ที่ share session กัน

test('multi-tab', async ({ page, context }) => {
  // page ตัวแรกมีอยู่แล้ว
  const secondTab = await context.newPage(); // เปิด tab ที่ 2 ใน session เดียวกัน
  // ทั้ง page และ secondTab เห็น cookies เดียวกัน
});

"Isolated context for this test run. The page fixture belongs to this context as well." — playwright.dev/docs/test-fixtures

browser fixture — ใช้เมื่อต้องการสร้าง session แยกกันหลายอัน (multi-user scenario)

test('multi-user', async ({ browser }) => {
  // สร้าง 2 sessions แยกกันสมบูรณ์
  const adminCtx = await browser.newContext();  // session ของ admin
  const userCtx  = await browser.newContext();  // session ของ user ทั่วไป
  // ทั้งสองไม่ share cookies กัน
});

"Browsers are shared across tests to optimize resources." — playwright.dev/docs/test-fixtures

สรุปการเลือก fixture:

ต้องการ ใช้ fixture
test ทั่วไป 1 user page
เปิดหลาย tab ที่ login ด้วย user เดียวกัน context
ทดสอบหลาย user พร้อมกัน browser

3.5 Multi-User Scenario

Playwright รองรับ multi-user testing โดยธรรมชาติ:

"Playwright can create multiple browser contexts within a single scenario. This is useful when you want to test for multi-user functionality, like a chat." — playwright.dev/docs/browser-contexts

แต่ละ context = session ของ user คนละคน บน browser เดียวกัน — ทำงานพร้อมกันได้โดยไม่ต้อง Selenium Grid

3.6 เปรียบเทียบ RF/Selenium vs Playwright

Robot Framework + Selenium Playwright
Browser startup เปิด process ใหม่ทุก test case (ช้า) Reuse browser ข้าม tests (เร็ว)
Session isolation ต้องเขียน setup/teardown เอง Built-in ผ่าน BrowserContext อัตโนมัติ
Multi-user parallel ต้องใช้ Selenium Grid (infrastructure แยก) ใช้หลาย context บน browser เดียวกัน
Auth state sharing copy cookie/sessionStorage ด้วย script เอง storageState export/import (Ch13)
State cleanup ต้องเขียน teardown keyword context ถูก close อัตโนมัติหลังแต่ละ test

4. ตัวอย่าง

Beginner: Default Page Fixture Isolation

สถานการณ์: ต้องการยืนยันว่า test แต่ละตัวไม่ได้ share session กัน

อ่าน syntax นี้ก่อน:

test('ชื่อ test', async ({ page }) => {
//                          ↑
//                   นี่คือ fixture — Playwright inject page มาให้
//                   ไม่ต้องสร้างเอง ไม่ต้องปิดเอง

  await page.goto('...');
//  ↑
//  await = รอให้เสร็จก่อนบรรทัดถัดไป
//  ทุก action ใน Playwright ต้องมี await เสมอ
});
// tested: Playwright v1.50+, Node.js 20+ (requires demo app at localhost:3000)
import { test, expect } from '@playwright/test';

// Test 1: login เป็น admin แล้วปล่อยให้ Playwright จัดการ cleanup
test('test 1 — login as admin', async ({ page }) => {
  // Playwright สร้าง context ใหม่สะอาดให้ก่อน test นี้เสมอ
  await page.goto('http://localhost:3000');
  await expect(page.locator('[data-testid="session-badge"]')).toContainText('Not logged in');

  // login เป็น admin
  await page.goto('http://localhost:3000/login');
  await page.fill('[data-testid="input-username"]', 'admin');
  await page.fill('[data-testid="input-password"]', 'admin123');
  await page.click('[data-testid="btn-login"]');
  await expect(page.locator('[data-testid="session-badge"]')).toContainText('admin');

  // หลัง test นี้จบ Playwright จะ close context (และ session ที่ login ไว้) ทิ้งอัตโนมัติ
  // ไม่ต้องเขียน logout หรือ cleanup ใดๆ
});

// Test 2: ได้ context ใหม่สะอาด — ไม่มีร่องรอยจาก Test 1 เลย
test('test 2 — fresh context, no session from test 1', async ({ page }) => {
  await page.goto('http://localhost:3000');
  // ถ้า Playwright ไม่มี isolation จริง ตรงนี้จะเห็น "admin" จาก test 1
  // แต่เพราะ context ใหม่ทุกครั้ง — badge แสดง "Not logged in" เสมอ
  await expect(page.locator('[data-testid="session-badge"]')).toContainText('Not logged in');
});

สังเกตว่า: ไม่มี beforeEach, ไม่มี afterEach, ไม่มี cleanup code เลย — Playwright จัดการให้ทั้งหมดผ่าน fixture


Intermediate: ทดสอบ Role-Based Access — admin กับ testuser เห็น Admin Dashboard ต่างกัน

สถานการณ์: ระบบมีหน้า Admin Dashboard ที่จำกัดสิทธิ์ — admin ต้องเห็น welcome message แต่ testuser (role ปกติ) ต้องเห็น "Access denied" ต้องการทดสอบทั้งสองพร้อมกันบน browser เดียว เพื่อยืนยันว่า context isolation ทำให้ role ของแต่ละ session แยกกันสมบูรณ์

// tested: Playwright v1.50+, Node.js 20+ (requires demo app at localhost:3000)
import { test, expect } from '@playwright/test';

test('admin sees dashboard, testuser sees access denied — same browser, different contexts', async ({ browser }) => {
  // สร้าง 2 contexts — แต่ละตัวมี session และ role ของตัวเอง
  const adminCtx = await browser.newContext();
  const userCtx = await browser.newContext();

  const adminPage = await adminCtx.newPage();
  const userPage = await userCtx.newPage();

  try {
    // Login เป็น admin ใน context แรก
    await adminPage.goto('http://localhost:3000/login');
    await adminPage.fill('[data-testid="input-username"]', 'admin');
    await adminPage.fill('[data-testid="input-password"]', 'admin123');
    await adminPage.click('[data-testid="btn-login"]');

    // Login เป็น testuser ใน context ที่สอง
    await userPage.goto('http://localhost:3000/login');
    await userPage.fill('[data-testid="input-username"]', 'testuser');
    await userPage.fill('[data-testid="input-password"]', 'test123');
    await userPage.click('[data-testid="btn-login"]');

    // ทั้งสอง navigate ไปหน้าเดียวกัน — /admin
    await adminPage.goto('http://localhost:3000/admin');
    await userPage.goto('http://localhost:3000/admin');

    // Admin เห็น welcome message (มีสิทธิ์)
    await expect(adminPage.locator('[data-testid="admin-welcome"]')).toBeVisible();

    // testuser เห็น access denied (ไม่มีสิทธิ์) — หน้าเดียวกัน แต่ต่างกันเพราะ session ต่างกัน
    await expect(userPage.locator('[data-testid="access-denied"]')).toBeVisible();

  } finally {
    // ต้อง close contexts ที่สร้างเองด้วยมือเสมอ
    await adminCtx.close();
    await userCtx.close();
  }
});

สังเกตสิ่งสำคัญ: - ใช้ browser fixture (ไม่ใช่ page) เพราะต้องสร้าง context เอง - ใช้ try/finally เพื่อให้ contexts ถูก close แม้ test fail - URL เดียวกัน (/admin) แต่ผลต่างกันเพราะ session แยกกัน — นี่คือพลังของ context isolation


Advanced: Diagnosis — วิเคราะห์ Fragile Test จาก State Leak

พื้นฐานที่ต้องรู้ก่อน — Playwright รัน test ยังไง?

default behavior (fullyParallel: false):

  tests-auth.spec.ts  → test A → test B → test C   (sequential ภายใน file)
                                                      ↑ สอง file รันพร้อมกัน
  tests-shop.spec.ts  → test 1 → test 2 → test 3   (sequential ภายใน file)
  • ภายใน file เดียวกัน → รันตามลำดับจากบนลงล่าง รับประกัน A → B → C เสมอ
  • ข้าม file → รัน parallel พร้อมกัน
  • fullyParallel: true → แม้แต่ภายใน file เดียวกัน tests ก็รัน parallel ได้ ไม่รับประกัน order

สถานการณ์: Junior test engineer ส่ง test มาให้ดู บอกว่า "ไม่แน่ใจว่า code นี้ปลอดภัยไหม รันผ่านทุกครั้งในเครื่องตัวเอง"

// ⚠️ ตัวอย่างนี้จงใจเขียนผิด เพื่อแสดง anti-pattern — ห้าม copy
// ⚠️ CODE ที่มีปัญหา — ให้วิเคราะห์หาสาเหตุก่อนดูเฉลย
import { test, expect } from '@playwright/test';

let sharedPage: any;
//  ↑ ตัวแปร module-level ที่ tests ทุกตัว share กัน

test.beforeAll(async ({ browser }) => {
  // login ครั้งเดียว เก็บ page ไว้ใช้ร่วมกันทุก test
  const context = await browser.newContext();
  sharedPage = await context.newPage();
  await sharedPage.goto('http://localhost:3000/login');
  await sharedPage.fill('[data-testid="input-username"]', 'admin');
  await sharedPage.fill('[data-testid="input-password"]', 'admin123');
  await sharedPage.click('[data-testid="btn-login"]');
});

test('test A — check admin badge', async () => {
  await sharedPage.goto('http://localhost:3000');
  await expect(sharedPage.locator('[data-testid="session-badge"]')).toContainText('admin');
});

test('test B — check admin badge again', async () => {
  await sharedPage.goto('http://localhost:3000');
  await expect(sharedPage.locator('[data-testid="session-badge"]')).toContainText('admin');
});

test('test C — logout', async () => {
  await sharedPage.click('[data-testid="nav-logout"]');
  await expect(sharedPage.locator('[data-testid="session-badge"]')).toContainText('Not logged in');
});

Exercise: วิเคราะห์ก่อนดูเฉลย

ลองตอบคำถามเหล่านี้ก่อน scroll ต่อ: 1. ด้วย default settings (sequential ภายใน file) — code นี้จะผ่านหรือพัง? เพราะอะไร? 2. code นี้มีความเสี่ยงอะไร แม้ตอนนี้จะผ่านอยู่? 3. ถ้าเปิด fullyParallel: true จะเกิดอะไรขึ้น?


เฉลย:

ข้อ 1 — ด้วย default settings: ผ่านทุกครั้ง

tests อยู่ใน file เดียวกัน รันตามลำดับ A → B → C เสมอ: - test A: เข้า homepage → เห็น admin badge ✅ - test B: เข้า homepage → ยัง login อยู่ → เห็น admin badge ✅ - test C: logout → badge เปลี่ยนเป็น "Not logged in" ✅

ข้อ 2 — ความเสี่ยง 3 ข้อแม้ตอนนี้ผ่าน

ความเสี่ยงที่ 1 — เปราะบางต่อการเปลี่ยน order

ถ้าใครย้าย test C ขึ้นไปอยู่เหนือ A และ B — test A และ B จะพังทันที เพราะ sharedPage logout ไปแล้ว code ที่ดีต้องรันได้ไม่ว่า order จะเป็นอะไร

ความเสี่ยงที่ 2 — พังทันทีถ้าเปิด fullyParallel

ถ้าทีมเปิด fullyParallel: true ในอนาคต tests A, B, C อาจรันพร้อมกัน ทั้งสามใช้ sharedPage ตัวเดียวกัน อาจเกิด race condition — คือสถานการณ์ที่ผลลัพธ์ขึ้นอยู่กับว่า test ไหนรันถึงบรรทัดไหนก่อน เช่น test A กำลัง assert ว่าเห็น "admin" แต่ test C รัน logout ไปพอดี → test A พัง

ความเสี่ยงที่ 3 — any type ซ่อน typo

let sharedPage: any ทำให้ TypeScript ไม่ตรวจให้ เช่น sharedPage.fillll() ผ่าน compile แต่พัง runtime โดยไม่มีคำเตือน

ข้อ 3 — ถ้าเปิด fullyParallel: true

tests รัน parallel พร้อมกัน ทั้งสามแย่งใช้ sharedPage ตัวเดียว ผลลัพธ์ขึ้นอยู่กับ timing — บางรอบผ่าน บางรอบพัง ไม่สามารถ reproduce ได้แน่นอน นี่คือที่มาของ "flaky test"

วิธีแก้ที่ถูกต้อง:

// ✅ แต่ละ test จัดการ login ของตัวเอง — isolated สมบูรณ์
// tested: Playwright v1.50+, Node.js 20+ (requires demo app at localhost:3000)
import { test, expect } from '@playwright/test';

test('test A — check admin badge', async ({ page }) => {
  // page fixture = context ใหม่สะอาด ไม่ share กับ test อื่น
  await page.goto('http://localhost:3000/login');
  await page.fill('[data-testid="input-username"]', 'admin');
  await page.fill('[data-testid="input-password"]', 'admin123');
  await page.click('[data-testid="btn-login"]');
  await expect(page.locator('[data-testid="session-badge"]')).toContainText('admin');
});

test('test C — logout', async ({ page }) => {
  // login ก่อนในทุก test ที่ต้องการ — ไม่พึ่ง state จาก test อื่น
  await page.goto('http://localhost:3000/login');
  await page.fill('[data-testid="input-username"]', 'admin');
  await page.fill('[data-testid="input-password"]', 'admin123');
  await page.click('[data-testid="btn-login"]');
  await page.click('[data-testid="nav-logout"]');
  await expect(page.locator('[data-testid="session-badge"]')).toContainText('Not logged in');
});

Key Insight: code ที่ "ผ่านอยู่ในตอนนี้" ไม่ได้แปลว่า "ถูกต้อง" — test ที่ดีต้องรันได้อิสระ ไม่ว่า order จะเป็นอะไร ถ้าพลิก order แล้วพัง = มี state leak


5. Common Mistakes

❌ Mistake 1: Login ใน beforeAll แล้วหวังว่า page fixture จะ share session

// ❌ ผิด: beforeAll login แต่ page fixture แต่ละ test ได้ context ใหม่
test.beforeAll(async ({ page }) => {
  await page.goto('http://localhost:3000/login');
  await page.fill('[data-testid="input-username"]', 'admin');
  await page.click('[data-testid="btn-login"]');
  // login นี้อยู่ใน context ที่ถูกทิ้งหลัง beforeAll
});

test('should see admin panel', async ({ page }) => {
  // page นี้เป็น context ใหม่ ไม่มี login state จาก beforeAll
  await expect(page.locator('[data-testid="session-badge"]')).toContainText('admin'); // ❌ พัง
});
// ✅ ถูก: ถ้าต้องการ share auth ให้ใช้ storageState (เรียนใน Ch13)
// หรือ login ใน test เอง
test('should see admin panel', async ({ page }) => {
  await page.goto('http://localhost:3000/login');
  await page.fill('[data-testid="input-username"]', 'admin');
  await page.fill('[data-testid="input-password"]', 'admin123');
  await page.click('[data-testid="btn-login"]');
  await expect(page.locator('[data-testid="session-badge"]')).toContainText('admin'); // ✅ ผ่าน
});

(source: https://playwright.dev/docs/browser-contexts — "Playwright creates a context for each test, and provides a default Page in that context.")


❌ Mistake 2: ไม่ close context ที่สร้างด้วย browser.newContext()

// ❌ ผิด: สร้าง context แต่ไม่ close
test('multi-user test', async ({ browser }) => {
  const ctx1 = await browser.newContext();
  const ctx2 = await browser.newContext();
  const page1 = await ctx1.newPage();
  const page2 = await ctx2.newPage();
  // ... test logic ...
  // ลืม close ctx1, ctx2
  // memory leak + resource ค้างอยู่
});
// ✅ ถูก: ใช้ try/finally หรือ afterEach เพื่อให้ close เสมอ แม้ test fail
test('multi-user test', async ({ browser }) => {
  const ctx1 = await browser.newContext();
  const ctx2 = await browser.newContext();
  try {
    const page1 = await ctx1.newPage();
    const page2 = await ctx2.newPage();
    // ... test logic ...
  } finally {
    await ctx1.close();
    await ctx2.close();
  }
});

(source: https://playwright.dev/docs/test-fixtures — "Test-scoped fixtures are torn down after each test, while worker-scoped fixtures are only torn down when the worker process executing tests is torn down." — fixtures ที่สร้างเองไม่มี auto-teardown)


❌ Mistake 3: คิดว่า page.close() แล้ว context ปิดด้วย

// ❌ ผิด: ปิดแค่ page แต่ context ยังอยู่
const ctx = await browser.newContext();
const page = await ctx.newPage();
await page.goto('http://localhost:3000/login');
// ... do stuff ...
await page.close(); // ❌ context ยังค้างอยู่ใน memory

// ✅ ถูก: ปิด context แทน (ซึ่ง close page ที่อยู่ใน context ด้วยอัตโนมัติ)
await ctx.close(); // ✅ close ทั้ง context และ pages ทั้งหมดใน context

(source: https://playwright.dev/docs/pages — "A Page refers to a single tab or a popup window within a browser context." — page เป็นส่วนหนึ่งของ context, การ close page ไม่ได้ destroy context)


❌ Mistake 4: ใช้ page fixture แล้วพยายามสร้าง Page เพิ่มด้วย browser.newPage()

// ❌ ผิด: browser.newPage() สร้าง page ใน context default ของ browser
// ไม่ใช่ context เดียวกับ page fixture
test('wrong multi-tab', async ({ page, browser }) => {
  const secondPage = await browser.newPage(); // ❌ คนละ context กับ page fixture
  // secondPage ไม่ share cookies กับ page
});
// ✅ ถูก: ใช้ context fixture เพื่อสร้าง page ที่ share context เดียวกัน
test('correct multi-tab', async ({ page, context }) => {
  const secondPage = await context.newPage(); // ✅ share context กับ page fixture
  // secondPage และ page share cookies/localStorage กัน
});

(source: https://playwright.dev/docs/pages — "Each BrowserContext can have multiple pages.")


6. สรุปบท

Key Takeaways

  • Browser = process ที่ Playwright reuse ข้าม tests — เปิดครั้งเดียวต่อ worker, ปิดเมื่อ worker หยุด
  • BrowserContext = incognito-like profile ที่มี cookies/localStorage/auth state ของตัวเอง — Playwright สร้างใหม่ก่อนแต่ละ test, close หลัง test โดยอัตโนมัติ
  • Page = 1 tab ใน 1 context — page หลายตัวใน context เดียวกันจะ share session กัน
  • page fixture ใช้ 90% ของงาน, context fixture ใช้เมื่อต้องการหลาย page ที่ share state, browser fixture ใช้เมื่อต้องการ multi-user ใน test เดียว
  • ถ้า test fail เพราะ session มาจากไหนไม่รู้ — ให้หา shared variable ที่ข้าม test boundary

Retrieval Questions — ลองตอบก่อนดูเฉลย

  1. นักพัฒนาคนหนึ่งบอกว่า "Playwright ช้ากว่า Selenium เพราะต้องสร้าง BrowserContext ใหม่ทุก test" — ข้อโต้แย้งของคุณคืออะไร?

  2. ถ้า test A login เป็น admin และ test B คาดหวังว่าจะเห็น homepage แบบ guest — คุณต้องเขียน cleanup code อะไรใน afterEach บ้าง?

  3. คุณกำลังเขียน test สำหรับ feature "online users counter" ที่แสดงจำนวน active sessions คุณจะใช้ fixture อะไร และ structure test อย่างไร?

ดูเฉลย 1. ข้อโต้แย้ง: BrowserContext สร้างเร็วและเบา ("fast and cheap to create") เพราะ Playwright ไม่ต้องสร้าง browser process ใหม่ — แค่สร้าง isolated profile บน browser เดิม ส่วน browser process ที่แพงนั้น Playwright reuse ข้าม tests ทั้งหมดอยู่แล้ว ดังนั้น Playwright มักเร็วกว่า Selenium ที่ต้องเปิด browser process ใหม่ทุก test 2. คำตอบ: ไม่ต้องเขียนอะไรเลย — Playwright close context หลังแต่ละ test อัตโนมัติ test B จะได้ context ใหม่ที่สะอาดโดยไม่มีร่องรอย session จาก test A นี่คือ design ที่ทำให้ไม่ต้องเขียน cleanup 3. แนวทาง: ใช้ `browser` fixture แล้วสร้าง context หลายตัว — แต่ละ context คือ active session ของ user คนละคน ตัวอย่าง: สร้าง 3 contexts พร้อมกัน ให้แต่ละตัว navigate ไปที่ app แล้ว assert ว่า counter แสดง "3 online users" เสร็จแล้ว close contexts ทีละตัวแล้ว verify counter ลดลง ใช้ `try/finally` เพื่อให้ contexts ถูก close เสมอ

ต่อไป: Ch04 — Locators & Selectors (วิธีหา element บนหน้าเว็บที่ robust และไม่ brittle)