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ủapage.waitForSelector()— API cũ đã deprecated vì trả về ElementHandle (dễ stale), còn locator luôn resolve lại element mới nhất.waitForResponse/waitForRequestchờ network;waitForLoadStatechờ vòng đời trang;waitForFunctionchờ biểu thức JS tùy ý;waitForEventchờ 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 địnhvisible.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ề objectResponseđể đọ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émTimeoutError. Pattern chuẩn: dựng promise trước, trigger action sau, rồiawait— 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.waitForthaywaitForSelector: ``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``waitForFunctioncho đ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 );``waitForLoadStatesau điều hướng: ``ts await page.getByRole('link', { name: 'Chi tiết' }).click(); await page.waitForLoadState('domcontentloaded'); await page.waitForURL('**/order/*');``waitForEventchờ 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ôngwaitForTimeout. - Dùng
waitForResponsekhi 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
waitForURLthaywaitForLoadStatesau đ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). waitForFunctionlà 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(...)SAUclick()→ 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ùnglocator.waitFor()hoặc web-first assertion.waitForTimeoutlà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 ruleno-wait-for-timeout.networkidletrongwaitForLoadStatenghe 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ònexpect(locator).toBeVisible()retry + so sánh — trong test nên assert trực tiếp, wait chỉ cho bước trung gian. waitForFunctionchạ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.
