Wiki

← Tất cả bài wiki

Strict Mode (Chế độ nghiêm ngặt — một action một element)

automationplaywright

Strict mode là luật bất biến của Playwright: một action trên locator chỉ được phép resolve ra ĐÚNG 1 element — match 0 hoặc 2+ đều throw lỗi "strict mode violation". Nó ép tester định vị chính xác thay vì "click đại phần tử đầu tiên", và cung cấp first()/last()/nth() để xử lý list một cách tường minh.

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

  • Quy tắc 1 action - 1 element: khi bạn click(), fill(), hover()... trên locator, Playwright resolve locator ra DOM và YÊU CẦU kết quả đúng 1 node. 0 node → chờ tới timeout rồi báo không thấy; 2+ node → throw ngay strict mode violation.
  • Vì sao strict: không strict, test sẽ "bấm đại" element đầu tiên match — có khi là phần tử ẩn, phần tử trùng ở mobile/desktop markup, hoặc sai vùng. Lỗi âm thầm kiểu đó nguy hiểm hơn test fail.
  • Chỉ áp dụng cho action, không áp dụng cho đọc số lượng: count(), all(), và các assertion kiểu toHaveCount không cần 1-1 — chúng được thiết kế cho nhiều element.
  • Bật mặc định, không tắt được trong @playwright/test hiện đại: strict là core behavior; cách "thoát" duy nhất là làm locator match đúng 1 (thêm điều kiện, first/nth, chain scope).
  • first()/last()/nth() là van xả áp chính thức: khi cố ý muốn 1 trong N phần tử giống nhau, bạn phải nói tường minh — không còn chuyện Playwright tự lấy cái đầu tiên.

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

  • locator.first() / last() / nth(index): trả locator mới trỏ đúng 1 element theo vị trí trong tập match. nth(0) = first, nth(-1) = cuối.
  • Các hàm được phép trên multi-match: count(), all(), allTextContents(), allInnerTexts(), evaluateAll(), elementHandles(), toHaveCount() — không strict vì chúng xử lý cả tập.
  • Khi nào ném lỗi: click/fill/hover/check/selectOption/dblclick/focus/textContent/innerText/inputValue/getAttribute/screenshot trên locator >1 match → violation ngay, không retry.
  • Khắc phục đúng cách: (a) thêm điều kiện vào locator gốc (name, hasText), (b) chain scope theo vùng, (c) .filter(), (d) chủ động chọn first()/nth(i) nếu list giống nhau.
  • Log lỗi nói gì: "locator resolved to N elements" + danh sách node — đọc số N và mấy dòng đầu là biết đang match thừa ở đâu (thường là markup ẩn hoặc quên scope).

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

  • Lỗi điển hình và cách sửa: ``ts import { test, expect } from '@playwright/test'; // LỖI: trang có 5 nút "Xoá" → strict mode violation // await page.getByRole('button', { name: 'Xoá' }).click(); // Cách 1: chọn tường minh theo vị trí await page.getByRole('button', { name: 'Xoá' }).first().click(); // Cách 2 (tốt hơn): scope vào đúng dòng const row = page.getByRole('row').filter({ hasText: 'Đơn hàng #123' }); await row.getByRole('button', { name: 'Xoá' }).click(); ``
  • Làm việc với list hợp lệ: ``ts const items = page.locator('.todo-item'); await expect(items).toHaveCount(3); // multi-match OK cho assertion await items.nth(1).getByRole('checkbox').check(); await items.last().hover(); const texts = await items.allTextContents(); // đọc cả tập — không strict ``
  • Case markup ẩn hay gặp: ``ts // Trang render 2 bản menu (desktop + mobile), 1 bản ẩn // LỖI: page.getByRole('link', { name: 'Hồ sơ' }) match cả 2 // SỬA: lọc bản đang hiển thị await page.getByRole('link', { name: 'Hồ sơ' }) .filter({ visible: true }) .click(); ``
  • nth(-1) và all() combo: const links = await page.getByRole('link').all(); await links.at(-1).click(); — all() trả mảng Locator[], duyệt bằng JS thuần.

Best practices & Khi nào dùng

  • Coi strict violation như code review từ tool: đừng vội .first() cho qua — hỏi vì sao match >1. 80% trường hợp là selector thiếu ngữ cảnh, fix bằng chain/filter sẽ bền hơn.
  • first() chỉ khi list THỰC SỰ đồng nhất: danh sách link giống hệt nhau (breadcrumbs, tag) thì first/nth hợp lý; nút trong row khác nhau thì phải filter theo nội dung dòng.
  • nth(index) = code smell nhẹ: index cứng vỡ khi thêm/xoá/sort item. Ưu tiên filter({ hasText }) trừ khi vị trí chính là đối tượng test (vd: "item đầu tiên sau khi sort").
  • Khi thật sự muốn thao tác tất cả: for (const el of await locator.all()) await el.click(); — tường minh, không vi phạm strict.
  • Assert trước khi action với số lượng: await expect(items).toHaveCount(5) rồi mới nth(i) — tránh nth vượt biên resolve ra 0 element.

Gotchas, So sánh & Lưu ý

  • "resolved to N elements" nhưng trace chỉ thấy 1: thường do markup ẩn (responsive, template, dialog unmount chậm). Soi bằng locator.count() + filter({visible:true}).
  • Violation xảy ra NGAY, không hết timeout: khác với 0 element (chờ timeout), >1 element fail tức thì — nên lỗi này trong CI nghĩa là selector sai thật, hiếm khi flake.
  • nth(-1) hợp lệ nhưng đừng lạm dụng: -1 lấy cuối — tiện cho "item mới nhất", nhưng giòn nếu list thêm item sau; cân nhắc filter theo dữ liệu vừa tạo.
  • So sánh Selenium: findElement lặng lẽ trả element ĐẦU TIÊN match — nguồn gốc vô số bug click nhầm. Strict mode của Playwright là câu trả lời triết lý: sai thì fail rõ, không đoán mò.
  • isVisible()/isEnabled() không strict? — vẫn strict: gọi trên multi-match cũng throw. Chỉ nhóm "lấy cả tập" (count/all*/toHaveCount) mới được phép.
  • Test debug nhanh: await page.locator('button').count() chạy trong page.pause()/console của playwright test --debug để biết selector đang match bao nhiêu.