expect(locator) Assertions (Web-first assertions trên Locator)
automationplaywright
expect(locator) là nhóm assertion 'web-first' của Playwright: thay vì kiểm tra một lần rồi fail ngay, nó tự động retry cho đến khi element đạt trạng thái mong đợi hoặc hết timeout. Đây là cách chuẩn để kiểm tra trạng thái UI (hiển thị, text, số lượng, enable/disable, checked...) mà không cần sleep/wait thủ công — loại bỏ gần hết flaky do timing.
Khái niệm & Bản chất
- Web-first assertion (assertion ưu tiên web) là cơ chế Playwright tự poll (kiểm tra lặp lại) điều kiện trên DOM cho tới khi pass hoặc timeout — khác hẳn assert kiểu Jest chỉ kiểm tra đúng một lần tại thời điểm gọi.
- Bản chất retry:
await expect(locator).toBeVisible()không 'chờ 5s rồi check' mà check liên tục trong tối đa 5s — element xuất hiện ở giây thứ 2 là pass ngay, không lãng phí thời gian. Chỉ khi hết timeout mà vẫn chưa đạt điều kiện mới fail. - Giải quyết bài toán flaky kinh điển: trang web thật render bất đồng bộ (API trả về chậm, animation, lazy-load). Assert thường (
expect(await el.isVisible())) chụp trạng thái một thời điểm → test đúng mà fail. Locator assertion chờ trạng thái ổn định nên kết quả deterministic hơn. - Phủ sóng toàn bộ trạng thái element: visibility, text content, attribute, value của input, checkbox state, số lượng phần tử match, viewport, CSS — gần như mọi thứ bạn muốn kiểm chứng trên UI đều có matcher tương ứng.
Cách hoạt động & API chính
- Cú pháp chung:
await expect(locator).<matcher>(expected?, options?)— nhớawaitbắt buộc vì assertion là async (nó phải poll). Thiếuawait= test pass ảo vì promise chưa resolve. - Timeout mặc định 5s, chỉnh được per-assertion (
{ timeout: 10_000 }) hoặc global trongplaywright.config.tsquaexpect: { timeout: 10_000 }. Đảo ngược điều kiện bằng.not:await expect(locator).not.toBeVisible(). - Nhóm visibility/geometry:
toBeVisible(),toBeHidden(),toBeAttached()(tồn tại trong DOM),toBeInViewport()(nằm trong vùng nhìn thấy — khác visible). - Nhóm nội dung:
toHaveText(string|regex|array),toContainText(partial),toHaveCount(n)(đếm element match),toBeEmpty(),toHaveInnerText(),toHaveInnerHTML(). - Nhóm form/state:
toBeEnabled()/toBeDisabled(),toBeEditable(),toBeChecked()(checkbox/radio),toBeFocused(),toHaveValue()(input),toHaveValues()(multi-select). - Nhóm thuộc tính/style:
toHaveAttribute(name, value),toHaveClass(),toHaveId(),toHaveCSS(name, value),toHaveJSProperty(name, value),toHaveScreenshot()(visual — xem bài riêng).
Ví dụ & Cú pháp thực tế
- ```ts import { test, expect } from '@playwright/test'; test('locator assertions cơ bản', async ({ page }) => { await page.goto('/login'); const heading = page.getByRole('heading', { name: 'Đăng nhập' }); await expect(heading).toBeVisible(); // Điền sai mật khẩu → chờ message lỗi xuất hiện (auto-retry) await page.getByLabel('Email').fill('qa@hoctest.com'); await page.getByLabel('Mật khẩu').fill('sai-mat-khau'); await page.getByRole('button', { name: 'Đăng nhập' }).click(); const alert = page.getByRole('alert'); await expect(alert).toBeVisible(); await expect(alert).toContainText('sai mật khẩu'); // partial match await expect(alert).toHaveText(/thông tin đăng nhập không đúng/i); // regex exact });
- ```ts test('list & form assertions', async ({ page }) => { await page.goto('/users'); // Chờ đúng số dòng render xong — thay cho sleep(1000) await expect(page.getByRole('row')).toHaveCount(11); // 1 header + 10 data const checkbox = page.getByRole('checkbox', { name: 'Chọn tất cả' }); await checkbox.check(); await expect(checkbox).toBeChecked(); const deleteBtn = page.getByRole('button', { name: 'Xoá' }); await expect(deleteBtn).toBeDisabled(); // chưa chọn item nào await page.getByRole('row').nth(1).getByRole('checkbox').check(); await expect(deleteBtn).toBeEnabled(); // auto-retry chờ state đổi // Kiểm tra attribute & value await expect(page.getByLabel('Tìm kiếm')).toHaveAttribute('placeholder', 'Nhập tên...'); await expect(page.getByLabel('Trạng thái')).toHaveValue('active'); });
- Nhớ
await— nếu quên, assertion trả về Promise pending, test cứ chạy tiếp và gần như luôn 'xanh' giả. Bật ESLint plugin@typescript-eslint/no-floating-promisesđể bắt lỗi này tự động.
Best practices & Khi nào dùng
- Ưu tiên locator assertion cho mọi kiểm tra UI — đây là default đúng trong Playwright. Chỉ rớt xuống non-retrying
expect(value)khi bạn đã kéo giá trị ra ngoài (số lượng, response API...) và cần so sánh logic. - Dùng
toHaveTextcho match chính xác,toContainTextcho partial —toHaveTextso toàn bộ text (đã normalize whitespace),toContainTextchỉ cần chứa chuỗi con. Dùng regex khi nội dung có phần động:toHaveText(/Đơn hàng #\d+ đã tạo/). toHaveCountthay chosleepkhi chờ list render — pattern phổ biến: filter/search xong chờtoHaveCount(n)hoặctoHaveCount(0)để chắc kết quả đã apply, rồi mới assert nội dung.- Chain negation cẩn thận:
expect(locator).not.toBeVisible()pass cả khi element chưa hề tồn tại — nếu muốn chắc element biến mất sau action, viếtawait expect(locator).toBeHidden()rõ nghĩa hơn. - Đặt timeout hợp lý: điều kiện phụ thuộc backend chậm (export file, đồng bộ) thì truyền
{ timeout: 30_000 }riêng cho assertion đó thay vì tăng timeout global.
Gotchas, So sánh & Lưu ý
toBeVisible()vstoBeAttached(): visible = attached + có kích thước + khôngdisplay:none/visibility:hidden. Chỉ cần biết element 'đã render vào DOM' thìtoBeAttached()nhanh và ít khắt khe hơn.toBeVisible()vstoBeInViewport(): element bị che bởi sticky header hoặc nằm ngoài màn hình vẫn 'visible' theo nghĩa DOM nhưng không 'in viewport'. Kiểm tra trải nghiệm người dùng thật thì dùngtoBeInViewport().- Whitespace normalization:
toHaveTexttrim và gộp khoảng trắng — text' Xin\n chào 'match'Xin chào'. Tiện cho HTML format, nhưng đừng kỳ vọng so byte-perfect vớitextContent()thô. toBeCheckedchỉ cho checkbox/radio thật — với custom toggle (div + aria) phải asserttoHaveAttribute('aria-checked', 'true'). Playwright sẽ báo lỗi nếu bạn gọi sai loại element.- So với Cypress:
should('be.visible')của Cypress cũng retry tương tự, nhưng Playwright cho control timeout/negation tường minh hơn và matcher phong phú hơn (toBeInViewport,toHaveJSProperty). - Retry không miễn phí: mỗi poll là một evaluate xuống browser — trăm assertion trong vòng lặp chặt vẫn nhanh, nhưng đừng lạm dụng poll-heavy matcher trong hot path của test.
