Wiki

← Tất cả bài wiki

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ớ await bắt buộc vì assertion là async (nó phải poll). Thiếu await = test pass ảo vì promise chưa resolve.
  • Timeout mặc định 5s, chỉnh được per-assertion ({ timeout: 10_000 }) hoặc global trong playwright.config.ts qua expect: { 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 toHaveText cho match chính xác, toContainText cho partial — toHaveText so toàn bộ text (đã normalize whitespace), toContainText chỉ 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/).
  • toHaveCount thay cho sleep khi chờ list render — pattern phổ biến: filter/search xong chờ toHaveCount(n) hoặc toHaveCount(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ết await 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() vs toBeAttached(): visible = attached + có kích thước + không display:none/visibility:hidden. Chỉ cần biết element 'đã render vào DOM' thì toBeAttached() nhanh và ít khắt khe hơn.
  • toBeVisible() vs toBeInViewport(): 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ùng toBeInViewport().
  • Whitespace normalization: toHaveText trim 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ới textContent() thô.
  • toBeChecked chỉ cho checkbox/radio thật — với custom toggle (div + aria) phải assert toHaveAttribute('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.