Wiki

← Tất cả bài wiki

Explicit Waits (Chờ đợi tường minh)

automationplaywright

Explicit waits là cơ chế chủ động chờ một điều kiện cụ thể xảy ra (element xuất hiện, response trả về, trạng thái trang, biểu thức JS đúng) thay vì ngủ cứng. Playwright đã auto-wait cho hầu hết action/assertion, nên explicit wait chỉ dùng cho điều kiện mà auto-wait không lo được — đặc biệt phải tránh waitForTimeout.

Khái niệm & Bản chất

  • Explicit wait = chờ có điều kiện: 'chờ tối đa N giây cho đến khi X đúng' — khác hẳn implicit wait (chờ mặc định cho mọi thao tác) và hard sleep (ngủ bất kể điều kiện).
  • Playwright auto-wait sẵn: click(), fill(), expect().toBeVisible()... đều tự chờ element đạt trạng thái actionable. Explicit wait chỉ cần khi điều kiện nằm ngoài action — ví dụ chờ API trả về trước khi đọc DOM.
  • locator.waitFor() là bản thay thế của page.waitForSelector() — API cũ đã deprecated vì trả về ElementHandle (dễ stale), còn locator luôn resolve lại element mới nhất.
  • waitForResponse/waitForRequest chờ network; waitForLoadState chờ vòng đời trang; waitForFunction chờ biểu thức JS tùy ý; waitForEvent chờ sự kiện (popup, download, dialog).
  • waitForTimeout(ms) = hard sleep — chỉ tồn tại cho mục đích debug, không bao giờ nên có trong test thật.

Cách hoạt động & API chính

  • await locator.waitFor({ state: 'visible' | 'attached' | 'hidden' | 'detached' }) — chờ element đạt trạng thái; mặc định visible.
  • await page.waitForLoadState('load' | 'domcontentloaded' | 'networkidle') — chờ mốc load của document.
  • await page.waitForResponse(r => r.url().includes('/api/x') && r.status() === 200) — chờ response khớp predicate, trả về object Response để đọc body.
  • await page.waitForFunction(() => window.appReady === true) — poll biểu thức JS trong page context; truyền arg qua tham số thứ 2.
  • await page.waitForURL('**/checkout') — chờ URL đổi (hỗ trợ glob/regex/function).
  • Mọi hàm wait đều nhận { timeout } riêng; hết hạn ném TimeoutError. Pattern chuẩn: dựng promise trước, trigger action sau, rồi await — tránh race condition.

Ví dụ & Cú pháp thực tế

  • Chờ API trước khi assert UI (pattern dựng promise trước): ``ts const respPromise = page.waitForResponse( r => r.url().includes('/api/orders') && r.status() === 200 ); await page.getByRole('button', { name: 'Tải lại' }).click(); const resp = await respPromise; const orders = await resp.json(); // đọc luôn body để cross-check await expect(page.getByRole('row')).toHaveCount(orders.length); ``
  • locator.waitFor thay waitForSelector: ``ts // CŨ (đừng dùng): await page.waitForSelector('.toast'); await page.locator('.toast').waitFor({ state: 'visible' }); await page.locator('#spinner').waitFor({ state: 'hidden' }); // chờ spinner biến mất ``
  • waitForFunction cho điều kiện custom: ``ts await page.waitForFunction( expected => document.querySelectorAll('li.item').length >= expected, 5 // truyền expected = 5 vào hàm ); ``
  • waitForLoadState sau điều hướng: ``ts await page.getByRole('link', { name: 'Chi tiết' }).click(); await page.waitForLoadState('domcontentloaded'); await page.waitForURL('**/order/*'); ``
  • waitForEvent chờ sự kiện page: ``ts const downloadPromise = page.waitForEvent('download'); await page.getByText('Xuất Excel').click(); const download = await downloadPromise; ``

Best practices & Khi nào dùng

  • Thứ tự ưu tiên: web-first assertion (expect(locator).toBeVisible() tự retry) > locator.waitFor() > waitForResponse/waitForURL > waitForFunction > tuyệt đối không waitForTimeout.
  • Dùng waitForResponse khi kết quả assert phụ thuộc dữ liệu API — nó vừa là wait vừa cho bạn body để so dữ liệu UI ↔ backend.
  • Dùng waitForURL thay waitForLoadState sau điều hướng trong SPA — URL đổi là tín hiệu rõ ràng hơn mốc load (SPA không reload document).
  • waitForFunction là giải pháp cuối cho điều kiện không có API sẵn (biến global, số lượng DOM, state của lib JS) — tránh lạm dụng vì poll bằng JS khó debug.
  • Đặt timeout hợp lý: mặc định 30s đủ cho CI; chỉ nới cho case tải nặng thật sự, đừng nới để 'chữa' flake.

Gotchas, So sánh & Lưu ý

  • Race condition kinh điển: gọi page.waitForResponse(...) SAU click() → response đã về trước khi wait kịp đăng ký → treo đến timeout. Luôn tạo promise trước, click sau.
  • waitForSelector đã deprecated — vẫn chạy được nhưng trả về ElementHandle dễ stale; code mới dùng locator.waitFor() hoặc web-first assertion.
  • waitForTimeout làm test chậm và vẫn flake (máy CI chậm thì 2s không đủ, máy nhanh thì phí 2s). Team nào nghiêm túc thì cấm bằng eslint rule no-wait-for-timeout.
  • networkidle trong waitForLoadState nghe hấp dẫn nhưng dễ treo với app có polling/analytics/websocket — xem bài Navigation & load states.
  • Explicit wait không thay được web-first assertion: await locator.waitFor() chỉ chờ element tồn tại/hiện, còn expect(locator).toBeVisible() retry + so sánh — trong test nên assert trực tiếp, wait chỉ cho bước trung gian.
  • waitForFunction chạy trong context của page — không dùng được biến Node trực tiếp, phải truyền qua tham số arg.