Học Playwright tiếng Việt, Cộng đồng Playwright cho người Việt

Khóa học

[Vọc Playwright] Locator

https://playwright.dev/docs/locators

Giới thiệu

  • Locators là thành phần quan trọng trong cơ chế tự động chờ và khả năng retry của Playwright
  • Nói 1 cách đơn giản locators là cách để tìm các phần tử (element) trên trang tại bất kỳ thời điểm nào
  • Dưới đây là các locators phổ biến trong Playwright:
    • page.getByRole(): Tìm kiếm phần tử dựa trên vai trò cụ thể
    • page.getByText(): Tìm kiếm phần tử dựa trên nội dung văn bản
    • page.getByLabel(): Tìm kiếm một form control dựa trên text của label label
    • page.getByPlaceholder(): Tìm kiếm các input dựa trên thuộc tính placeholder
    • page.getByAltText(): Tìm kiếm hình ảnh dựa trên mô tả của nó (alt=“”)
    • page.getByTitle(): Tìm kiếm phần tử dựa trên title của chúng
    • page.getByTestId(): Tìm kiếm phần tử dựa trên thuộc tính data-testid(Các thuộc tính khác cũng có thể được cấu hình)
await page.getByLabel('User Name').fill('John');  // Tìm ô nhập "User name" và điền "John" vào đó.
await page.getByLabel('Password').fill('secret-password'); // Tìm ô nhập "Password" và điền mật khẩu.
await page.getByRole('button', { name: 'Sign in' }).click(); // Tìm nút "Sign in" và nhấp vào.
await expect(page.getByText('Welcome, John!')).toBeVisible(); // Kiểm tra xem có thấy dòng chữ "Welcome, John!" không.

Locating elements

  • Playwright có nhiều cách để tìm kiếm các phần tử trên trang web. Để giúp việc kiểm tra trang web dễ dàng hơn, chúng ta nên ưu tiên sử dụng cách tìm kiếm dựa trên role của phần tử như page.getByRole()
  • Ví dụ hãy xem cấu trúc DOM sau
<button>Sign in</button>
  • Bạn có thể tìm kiếm phần tử bằng role là button và name là Sign in
    await page.getByRole('button', { name: 'Sign in' }).click(); // Tìm và nhấp vào nút "Sign in".
    
  • Mỗi khi sử dụng 1 locator để thực hiện hành động, một phần tử DOM mới nhất sẽ được xác định trên trang. Điều này có nghĩa là nếu DOM thay đổi giữa các lần gọi do render lại, phần tử mới tương ứng với locator sẽ được sử dụng
const locator = page.getByRole('button', { name: 'Sign in' });

await locator.hover();
await locator.click();
  • Lưu ý rằng các phương thức tạo locator, chẳng hạn như page.getByLabel() cũng có sẵn trên các class LocatorFrameLocator, vì vậy bạn có thể kết hợp chúng và thu hẹp dần dần phạm vi của locator
const locator = page
  .frameLocator('#my-frame')
  .getByRole('button', { name: 'Sign in' });
await locator.click();

Locate by role

  • Locator page.getByRole() giúp bạn tìm kiếm phần tử dựa trên cách người dùng thực sự tương tác với nó, thay vì chỉ dựa trên cách nó được viết trong code, ví dụ như một phần tử có phải là button hay checkbox không. Khi tìm kiếm theo locator, bạn thường nên truyền cả tên(accessible name) để locator có thể xác định chính xác phần tử đó
  • Hãy xem cấu trúc DOM sau đây
<h3>Sign up</h3>
<label>
  <input type="checkbox" /> Subscribe
</label>
<br/>
<button>Submit</button>
  • Bạn có thể tìm kiếm từng phần tử theo role ngầm của nó – có nghĩa là nhiều phần tử HTML đã tự động gán role bởi trình duyệt mà bạn không cần phải khai báo rõ ràng(VD <button></button> luôn có role là button)
await expect(page.getByRole('heading', { name: 'Sign up' })).toBeVisible();
await page.getByRole('checkbox', { name: 'Subscribe' }).check();
await page.getByRole('button', { name: /submit/i }).click();
  • Locators role bao gồm button, checkbox, heading, link, list, table và nhiều loại khác tuân theo chuẩn W3C cho role ARIA, thuộc tính ARIA và accessible name.
  • Nên ưu tiên sử dụng locators role để tìm kiếm các phần tử vì đây là cách mà người dùng thực sự nhìn thấy trang web. Họ không biết về XPath hay CSS selector. Họ chỉ biết: đó là một button, và tôi cần click vào nó.

Locate by label

  • Hầu hết các form control đều có label riêng, có thể được sử dụng thuận tiện để tương tác với form. Trong trường hợp này, bạn có thể tìm kiếm bằng các label của nó bằng cách sử dụng page.getByLabel()
<label>Password <input type="password" /></label>
  • Bạn có thể điền vào input sau khi tìm kiếm nó bằng text của label
await page.getByLabel('Password').fill('secret');

Locate by placeholder

  • Các input có thuộc tính placeholder để gợi ý người dùng giá trị nên được nhập vào. Bạn có thể tìm kiếm 1 input như thế bằng cách sử dụng page.getByPlaceholder()
  • Sử dụng trong trường hợp tìm kiếm form control không có label
<input type="email" placeholder="name@example.com" />
await page
  .getByPlaceholder('name@example.com')
  .fill('playwright@microsoft.com');

Locate by text

  • Tìm kiếm 1 phần tử bằng text mà nó chứa. Bạn có thể tìm khớp theo chuỗi con, chuỗi chính xác hoặc 1 regex khi sử dụng page.getByText()
<span>Welcome, John</span>
  • Tìm kiếm phần tử văn bản mà nó chứa
await expect(page.getByText('Welcome, John')).toBeVisible();
  • Đặt kết quả khớp chính xác
await expect(page.getByText('Welcome, John', { exact: true })).toBeVisible();
  • Khớp với regex
await expect(page.getByText(/welcome, [A-Za-z]+$/i)).toBeVisible();
  • Lưu ý để việc khớp theo văn bản luôn chuẩn hoá khoảng trắng, ngay cả khi khớp chính xác. Ví dụ nó biến nhiều khoảng trắng thành 1 biến các dấu ngắt dòng thành khoảng trắng và và bỏ qua khoảng trắng ở đầu cuối
  • Khuyến nghị nên sử dụng locators text để tìm kiếm các phần tử không tương tác như div, span, p,… Đối với các phần tử tương tác như button, a, input,… sử dụng locators role

Locate by alt text

  • Tất cả các hình ảnh nên có alt để mô tả hình ảnh. Bạn có thể tìm kiếm một hình ảnh dựa trên text trong alt bằng cách sử dụng page.getByAltText()
  • Nên sử dụng khi phần tử của bạn hỗ trợ văn bản thay thế như các phần tử img và area
<img alt="playwright logo" src="/img/playwright-logo.svg" width="100" />

  • Bạn có thể click vào hình ảnh sau khi tìm kiếm nó bằng text của alt
await page.getByAltText('playwright logo').click();

Locate by title

  • Tìm kiếm phần tử với thuộc tính title bằng cách sử dụng page.getByTitle()
<span title='Issues count'>25 issues</span>

  • Kiểm tra số lượng sau khi tìm kiếm nó bằng title
await expect(page.getByTitle('Issues count')).toHaveText('25 issues');

Locate by test id

  • Kiểm tra bằng testIds là cách kiểm tra hiệu quả nhất, vì ngay cả khi text và role của thuộc tính thay đổi test vẫn sẽ thành công. QA và developer nên định nghĩa rõ ràng các Tests IDs và truy vấn chúng bằng page.getByTestId(). Tuy nhiên Test IDs không phải là phương pháp tiếp cận từ góc độ người dùng. Nếu role và text value quan trọng, hãy sử dụng locators như role và text
  • Nên sử dụng khi chọn phương pháp kiểm tra bằng Test ID hoặc khi không thể định vị bằng role hoặc text
<button data-testid="directions">Itinéraire</button>

  • Kiểm tra số lượng sau khi tìm kiếm nó bằng title
await page.getByTestId('directions').click();

Set a custom test id attribute

  • Theo mặc định, page.getByTestId() sẽ tìm kiếm các phần tử dựa trên thuộc tính data-testid nhưng bạn có thể cấu hình nó trong playwright.config.ts bằng cách gọi selectors.setTestIdAttribute()
  • Thiết lập Test ID để sử dụng thuộc tính dữ liệu tuỳ chỉnh cho các test của bạn
// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    testIdAttribute: 'data-pw'
  }
});
  • Trong HTML giờ có thể sử dụng data-pw làm Test ID thay vì data-testid mặc định
<button data-pw="directions">Itinéraire</button>

await page.getByTestId('directions').click();

Locate by CSS or XPath

  • Nếu bạn thực sự phải dùng locators CSS hoặc XPath, bạn có thể sử dụng page.locators()để tạo 1 locator nhận 1 selector mô tả cách tìm phần tử trên trang
  • CSS và XPath không được khuyến nghị vì DOM có thể thường xuyên bị thay đổi dẫn đến việc test không ổn định
  • Playwright hỗ trợ cả CSS và XPath selectors và tự động phát hiện chúng nếu bạn bỏ qua tiền tố css=xpath=
await page.locator('css=button').click();
await page.locator('xpath=//button').click();

await page.locator('button').click();
await page.locator('//button').click();
  • XPath và CSS selectors có thể liên kết cấu trúc với DOM. Những selectors này có thể bị phá vỡ khi cấu trúc DOM thay đổi
  • Các chuỗi CSS và XPath dài dưới đây là 1 ví dụ cụ thể về việc dẫn đến các test không ổn định
await page.locator(
  '#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > div.a4bIc > input'
).click();

await page
    .locator('//*[@id="tsf"]/div[2]/div[1]/div[1]/div/div[2]/input')
    .click();

Locate in Shadow DOM

  • Tất cả các locators trong Playwright theo mặc định hoạt động với các phần tử trong Shadow DOM. Các ngoại lệ bao gồm
    • Tìm kiếm XPath không xuyên qua shadow roots
    • Shadow roots ở closed-mode không được hỗ trợ
  • Hãy xem ví dụ sau với 1 custom web component
<x-details role=button aria-expanded=true aria-controls=inner-details>
  <div>Title</div>
  #shadow-root
    <div id=inner-details>Details</div>
</x-details>
  • Bạn có thể tìm kiếm giống như thể shadow root không tồn tại
  • Để click vào <div>Details</div>
await page.getByText('Details').click();
  • Để click vào <x-details>
await page.locator('x-details', { hasText: 'Details' }).click();
  • Để đảm bảo rằng <x-details> chứa text “Details”
await expect(page.locator('x-details')).toContainText('Details');

Filtering Locators

  • Hãy xem xét cấu trúc DOM dưới đây, nơi chúng ta muốn click vào “Add to cart” của sản phẩm thứ 2. Chúng ta có 1 vài tuỳ chọn để lọc locators và tìm đúng cái cần thiết
<ul>
  <li>
    <h3>Product 1</h3>
    <button>Add to cart</button>
  </li>
  <li>
    <h3>Product 2</h3>
    <button>Add to cart</button>
  </li>
</ul>

Filter by text

  • Locators có thể được lọc theo text bằng locator.filter(). Nó sẽ tìm kiếm 1 chuỗi cụ thể ở đâu đó bên trong phần tử, có thể trong 1 phần tử con, không phân biệt chữ hoa thường
await page
  .getByRole('listitem')
  .filter({ hasText: 'Product 2' })
  .getByRole('button', { name: 'Add to cart' })
  .click();
  • Sử dụng regex
await page
  .getByRole('listitem')
  .filter({ hasText: /Product 2/ })
  .getByRole('button', { name: 'Add to cart' })
  .click();

Filter by not having text

  • Lọc theo không có text
// 5 in-stock items
await expect(page.getByRole('listitem').filter({ hasNotText: 'Out of stock' })).toHaveCount(5);

Filter by child/descendant

  • Locators hỗ trợ tuỳ chọn chỉ chọn các phần tử có hoặc không có một phần tử con phù hợp với 1 locators khác
  • Vì thế bạn có thể lọc bằng bất kỳ lcoator nào khác như locator.getByRole(), locator.getByTestId(), locator.getByText(),…
await page
  .getByRole('listitem')
  .filter({ has: page.getByRole('heading', { name: 'Product 2' }) })
  .getByRole('button', { name: 'Add to cart' })
  .click();
  • Cũng có thể xác nhận để đảm bảo chỉ có 1
await expect(page
  .getByRole('listitem')
  .filter({ has: page.getByRole('heading', { name: 'Product 2' }) }))
  .toHaveCount(1);
  • Locator filter phải tương đối với locator gốc được truy vấn, không phải document root. Vì vậy cách sau sẽ không hoạt động vì locator filter bắt đầu khớp từ <ul> nằm ngoài <li>
// ✖ SAI
await expect(page
    .getByRole('listitem')
    .filter({ has: page.getByRole('list').getByText('Product 2') }))
    .toHaveCount(1);

Filter by not having child/descendant

  • Có thể filter bằng cách không có 1 phần tử phù hợp bên trong
await expect(page
  .getByRole('listitem')
  .filter({ hasNot: page.getByText('Product 2') }))
  .toHaveCount(1);
  • **Lưu ý rằng locator bên trong được khớp bắt đầu từ locator bên ngoài, không phải tử document root

Locator Operators

Matching inside a locator

  • Bạn có thể kết hợp các phương thức tạo locator để thu hẹp tìm kiếm tới 1 phần cụ thể trên trang
  • Trong ví dụ bên dưới, chúng ta tạo 1 locator gọi là product bằng role là listitem. Sau đó chúng ta lọc theo text. Chúng ta sử dụng locator product để tìm role button và click vào nó, sau đó sử dụng expect để đảm bảo chỉ có 1 sản phẩm với text là “Product 2”
const product = page.getByRole('listitem').filter({ hasText: 'Product 2' });

await product.getByRole('button', { name: 'Add to cart' }).click();

await expect(product).toHaveCount(1);
  • Bạn cũng có thể kết hợp 2 locators với nhau. Vd tìm kiếm nút Save trong modal cụ thể
const saveButton = page.getByRole('button', { name: 'Save' });
// ...
const dialog = page.getByTestId('settings-dialog');
await dialog.locator(saveButton).click();

Matching two locators simultaneously

  • Phương thức locator.and() thu hẹp 1 locator hiện có bằng cách khớp với 1 locator bổ sung. Ví dụ có thể kết hợp role và title để khớp với cả 2
const button = page.getByRole('button').and(page.getByTitle('Subscribe'));

Matching one of the two alternative locators

  • Nếu bạn muốn nhắm đến 1 trong 2 hoặc nhiều phần tử mà không biết sẽ là cái nào, hãy sử dụng locator.or() để tạo 1 locator khớp với tất cả các lựa chọn thay thế
  • Ví dụ nếu bạn muốn click vào nút “New email” nhưng đôi khi popup cài đặt hiển thị bảo mật xuất hiện. Trong trường hợp này, bạn có thể chờ nút “New email” hoặc popup xuất hiện và hành động phù hợp
  • Lưu ý: Nếu cả button “New email” và popup xuất hiện trên màn hình, locator “or” sẽ khớp với cả 2, có thể gây ra lỗi strict mode violation. Trường hợp này thì chúng ta sử dụng locator.first() để chỉ khớp với 1 trong số chúng
const newEmail = page.getByRole('button', { name: 'New' });
const dialog = page.getByText('Confirm security settings');
await expect(newEmail.or(dialog).first()).toBeVisible();
if (await dialog.isVisible())
  await page.getByRole('button', { name: 'Dismiss' }).click();
await newEmail.click();

Matching only visible elements

  • Thường thì thay vì chỉ kiểm tra xem 1 phần tử có hiển thị trên màn hình hay không, bạn nên tìm 1 cách chắc chắn hơn để xác định duy nhất phần tử đó
<button style='display: none'>Invisible</button>
<button>Visible</button>
  • Đoạn code dưới đây sẽ hiển thị lỗi strict mode
await page.locator('button').click();
  • Còn đoạn code này sẽ chỉ tìm nút thứ 2
await page.locator('button').locator('visible=true').click();

List

Count items in list

  • Bạn có thể sử dụng assert count locators để đếm các mục trong 1 list
<ul>
  <li>apple</li>
  <li>banana</li>
  <li>orange</li>
</ul>
  • Sử dụng assert count để đảm bảo rằng list có 3 mục
await expect(page.getByRole('listitem')).toHaveCount(3);

Assert all text in a list

  • Bạn có thể assert locators để tìm tất cả các text trong list
  • Sử dụng expect(locator).toHaveText() để đảm bảo rằng list có “apple”, “banana” và “orange”
await expect(page
  .getByRole('listitem'))
  .toHaveText(['apple', 'banana', 'orange']);

Get a specific item

  • Có nhiều cách để lấy 1 mục cụ thể trong list

Get by text

  • Sử dụng page.getByText() để tìm 1 phần tử trong list theo nội dung text của nó sau đó click vào nó
await page.getByText('orange').click();

Filter by text

  • Sử dụng locator.filter() để tìm 1 mục cụ thể trong list
  • Tìm kiếm 1 mục theo role “listitem”, sau đó lọc theo text “orange” và click vào nó
await page
  .getByRole('listitem')
  .filter({ hasText: 'orange' })
  .click();

Get by test id

  • Sử dụng page.getByTestId() để tìm 1 phần tử trong list.
<ul>
  <li data-testid='apple'>apple</li>
  <li data-testid='banana'>banana</li>
  <li data-testid='orange'>orange</li>
</ul>
await page.getByTestId('orange').click();

Get by nth item

  • Nếu có list các phần tử giống hệt nhau, cách duy nhất để phân biệt giữa chúng là thứ tự, bạn có thể chọn 1 phần tử cụ thể bằng locator.first(), locator.last() hoặc locator.nth()
const banana = await page.getByRole('listitem').nth(1);
  • Tuy nhiên hãy sử dụng cách này cần thận. Thường thì trang có thể thay đổi và locator sẽ trỏ tới 1 phần tử hoàn toàn khác so với cái mà bạn mong đợi
  • Thay vào đó hãy cố gắng tìm ra một locator duy nhất vượt qua các strictness criteria.

Chaining filters

  • Khi bạn có các phần tử với nhiều điểm tương đồng, bạn có thể sử dụng locator.filter() để chọn phần tử đúng và cũng có thể kết hợp nhiều filter
<ul>
  <li>
    <div>John</div>
    <div><button>Say hello</button></div>
  </li>
  <li>
    <div>Mary</div>
    <div><button>Say hello</button></div>
  </li>
  <li>
    <div>John</div>
    <div><button>Say goodbye</button></div>
  </li>
  <li>
    <div>Mary</div>
    <div><button>Say goodbye</button></div>
  </li>
</ul>
  • Để screenshot có “Mary” và “Say goodbye”
const rowLocator = page.getByRole('listitem');

await rowLocator
    .filter({ hasText: 'Mary' })
    .filter({ has: page.getByRole('button', { name: 'Say goodbye' }) })
    .screenshot({ path: 'screenshot.png' });

Rare use cases

  • Làm gì đó với mỗi phần tử trong list
  • Lặp qua các phần tử:
for (const row of await page.getByRole('listitem').all())
  console.log(await row.textContent());
  • Sử dụng vòng for thông thường
const rows = page.getByRole('listitem');
const count = await rows.count();
for (let i = 0; i < count; ++i)
  console.log(await rows.nth(i).textContent());

Evaluate in the page

  • Code bên trong locator.evaluateAll() chạy trong trang, bạn có thể gọi bất kỳ API DOM nào tại đây
const rows = page.getByRole('listitem');
const texts = await rows.evaluateAll(
    list => list.map(element => element.textContent));

Strictness

  • Locators trong Playwright là nghiêm ngặt. Điều này có nghĩa là tất cả các thao tác trên locators mà ám chỉ một phần tử DOM element sẽ ném ra 1 exception có nhiều hơn 1 phần tử khớp
  • Ví dụ đoạn code bên dưới sẽ ném ra lỗi nếu có nhiều button trong DOM
await page.getByRole('button').click();
  • Ngược lại, Playwright hiểu khi bạn thực hiện thao tác trên nhiều phần tử, do đó cách gọi sau sẽ hoàn toàn hoạt động tốt khi locator khớp với nhiều phần tử
await page.getByRole('button').count();
  • Bạn có thể bỏ qua tính nghiêm ngặt bằng cách cho Playwright biết phần tử nào sẽ sử dụng khi có nhiều phần tử khớp thông qua locator.first(), locator.last(), và locator.nth()
  • Tuy nhiên các phương thức này không được khuyến khích sử dụng

More Locators

  • Để biết thêm các locators ít được sử dụng hơn, hãy xem ở đây:

https://playwright.dev/docs/other-locators

Trả lời