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 ngaystrict 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ểutoHaveCountkhô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ọnfirst()/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ảngLocator[], 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ớinth(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:
-1lấ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:
findElementlặ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 trongpage.pause()/console củaplaywright test --debugđể biết selector đang match bao nhiêu.
