CupCode.cc
Section 04 · Module 04 · Lesson 01 · ~15 นาที

Auto-waiting ทำงานอย่างไร

เวลาเรียน: 15 นาที ต้องรู้มาก่อน: Module 03: Core User Flows รูปแบบ: อ่าน + แบบฝึกสั้น

จบบทนี้แล้วคุณจะ...

  • เข้าใจระบบ auto-waiting ของ Playwright
  • รู้จัก actionability checks ที่ Playwright ทำอัตโนมัติ
  • เปรียบเทียบ Playwright auto-wait กับ Selenium explicit wait
  • รู้ว่าเมื่อไหร่ต้อง (และไม่ต้อง) เขียน manual wait
1 · Auto-waiting คืออะไร?

Auto-waiting คือความสามารถของ Playwright ที่จะรอ element พร้อมก่อนทำ action โดยอัตโนมัติ ไม่ต้องเขียน wait เอง

ตัวอย่าง: เมื่อคุณเขียน await page.click('button') Playwright จะ:

  1. รอให้ element ปรากฏใน DOM
  2. รอให้ element มองเห็นได้ (visible)
  3. รอให้ element หยุดเคลื่อนไหว (stable)
  4. รอให้ element enabled (ไม่ disabled)
  5. รอให้ไม่มี element อื่นบัง
  6. แล้วค่อยคลิก

ทั้งหมดนี้เกิดขึ้นอัตโนมัติ — ไม่ต้องเขียน code เพิ่ม

2 · Actionability Checks

Playwright ทำ actionability checks ต่อไปนี้ก่อนทำ action:

Check หมายความว่า Actions ที่ต้องผ่าน
Attached element อยู่ใน DOM ทุก action
Visible มองเห็นได้ (ไม่ hidden, ไม่มี size 0) click, fill, hover
Stable หยุดเคลื่อนไหว (animation จบ) click
Enabled ไม่ disabled click, fill, selectOption
Receives Events ไม่มี element อื่นบัง (overlay) click
Editable element แก้ไขได้ fill
3 · Playwright vs Selenium — เปรียบเทียบ

Selenium (ต้องเขียน wait เอง)

# Selenium — ต้อง explicit wait ทุกครั้ง
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 10)
button = wait.until(
    EC.element_to_be_clickable((By.ID, "submit"))
)
button.click()

Playwright (auto-wait ให้)

// Playwright — แค่บรรทัดเดียว
await page.getByRole('button', { name: 'Submit' }).click();

Playwright ทำ wait ให้อัตโนมัติ — code สั้นกว่า อ่านง่ายกว่า และเสถียรกว่า

4 · Timeout

ทุก action มี timeout (default 30 วินาที) ถ้า element ไม่พร้อมภายในเวลา จะ throw error:

// ใช้ default timeout (30 วินาที)
await page.getByRole('button', { name: 'Submit' }).click();

// กำหนด timeout เฉพาะ action นี้
await page.getByRole('button', { name: 'Submit' }).click({
  timeout: 10000  // 10 วินาที
});

// ตั้ง timeout สำหรับทุก action ใน config
// playwright.config.ts
export default defineConfig({
  use: {
    actionTimeout: 15000,  // 15 วินาที
  },
});
5 · เมื่อไหร่ต้อง Wait เอง?

ส่วนใหญ่ไม่ต้องเขียน wait เอง แต่มีบางกรณีที่ต้อง:

กรณีที่ต้อง wait

// รอ URL เปลี่ยน (SPA)
await page.waitForURL('**/dashboard');

// รอ API response
await page.waitForResponse(resp =>
  resp.url().includes('/api/data')
);

// รอ element หายไป (เช่น loading spinner)
await page.getByTestId('spinner').waitFor({ state: 'hidden' });

// รอ element ปรากฏ
await page.getByText('Data loaded').waitFor({ state: 'visible' });

กรณีที่ไม่ต้อง wait (Playwright ทำให้แล้ว)

// ไม่ต้อง wait ก่อน click — Playwright รอ element พร้อมเอง
await page.getByRole('button', { name: 'Submit' }).click();

// ไม่ต้อง wait ก่อน fill — Playwright รอ field editable เอง
await page.getByLabel('Email').fill('[email protected]');

// ไม่ต้อง wait ใน assertion — auto-retry ให้แล้ว
await expect(page.getByText('Success')).toBeVisible();
6 · Anti-pattern: อย่าใช้ page.waitForTimeout()
// ไม่ดี — hard-coded wait
await page.waitForTimeout(3000);
await page.getByRole('button', { name: 'Submit' }).click();

// ดี — Playwright auto-wait
await page.getByRole('button', { name: 'Submit' }).click();

page.waitForTimeout() เป็น anti-pattern เพราะ:

  • ถ้า wait น้อยไป → test flaky
  • ถ้า wait มากไป → test ช้า
  • ไม่มีทางรู้ว่าต้อง wait เท่าไหร่ถึง "พอดี"
แบบฝึกหัด

วิเคราะห์ test ด้านล่าง แล้ว refactor ให้ใช้ auto-waiting แทน manual waits:

Before (ใช้ manual waits)

test('should load products', async ({ page }) => {
  await page.goto('/products');

  // รอ 3 วินาทีให้หน้าโหลด
  await page.waitForTimeout(3000);

  // รอให้ spinner หายไป
  await page.waitForTimeout(2000);

  // คลิกสินค้าแรก
  await page.waitForTimeout(1000);
  await page.locator('.product-card:first-child').click();

  // ตรวจว่าไปหน้า detail
  await page.waitForTimeout(2000);
  const title = await page.locator('.product-title').textContent();
  expect(title).not.toBeNull();
});
ดูคำตอบแนะนำ

After (ใช้ auto-waiting)

test('should load products', async ({ page }) => {
  await page.goto('/products');

  // รอ spinner หายไป (ถ้ามี)
  await page.getByTestId('spinner').waitFor({ state: 'hidden' });

  // คลิกสินค้าแรก — Playwright auto-wait ให้
  await page.getByRole('listitem').first().click();

  // ตรวจว่าไปหน้า detail — auto-retry assertion
  await expect(page.getByRole('heading')).toBeVisible();
});

สิ่งที่เปลี่ยน:

  1. ลบ waitForTimeout() ทั้งหมด
  2. ใช้ waitFor({ state: 'hidden' }) สำหรับ spinner
  3. ใช้ getByRole แทน CSS selector
  4. ใช้ auto-retry assertion แทนการอ่าน textContent แล้ว assert

สรุปสิ่งที่ต้องจำ

  • Playwright auto-wait ก่อนทุก action — รอ visible, stable, enabled
  • ไม่ต้องเขียน wait สำหรับ click, fill, assertion — Playwright ทำให้แล้ว
  • page.waitForTimeout() เป็น anti-pattern — อย่าใช้
  • ใช้ waitForURL(), waitForResponse(), waitFor() เฉพาะเมื่อ auto-wait ไม่เพียงพอ
  • ตั้ง actionTimeout ใน config แทนการกำหนด timeout ทีละ action