Đang tải...

Lửng Lọc Lõi

Đeo kính để soi Bug cho rõ, nhíu mày để nhắc Dev sửa cho kỹ. Với tôi, 'chạy được' thôi là chưa đủ! 🧐💻


0

Playwright locator: viết test form theo role và label để bớt phụ thuộc CSS

Lửng Lọc Lõi
Lửng Lọc Lõi

5 giờ tới · 7 phút đọc

Vấn đề với CSS selector trong test form

Khi viết automation test cho form đăng ký, đăng nhập hay checkout, cách phổ biến nhất là dùng CSS selector như input.form-control hay #email-field. Vấn đề? Dev đổi class hoặc refactor HTML, test gãy ngay - dù tính năng vẫn hoạt động đúng.

Đây là dạng flaky test phổ biến nhất trong web automation: test fail không phải vì bug, mà vì selector không còn khớp.

Playwright cung cấp hai locator được khuyến nghị trong tài liệu chính thức để giải quyết vấn đề này: getByRole() và getByLabel(). Cả hai đều dựa trên ngữ nghĩa HTML và ARIA - thứ ít thay đổi hơn class hay id trong quá trình refactor.

348096 CSS selector gãy khi dev đổi class, locator theo role/label thì không

Lưu ý phiên bản: Bài này dùng Playwright từ v1.20 trở lên - phiên bản ổn định hóa API getByRole, getByLabel. Kiểm tra phiên bản hiện tại tại tài liệu Locators chính thức của Playwright.


getByRole là gì và khi nào dùng

getByRole() tìm phần tử dựa trên ARIA role - vai trò ngữ nghĩa của phần tử theo chuẩn WAI-ARIA. Ví dụ: <button> có role button, <input type="text"> có role textbox, <a> có role link.

Cú pháp cơ bản:

page.getByRole('textbox', { name: 'Email' })
page.getByRole('button', { name: 'Đăng nhập' })
page.getByRole('checkbox', { name: 'Ghi nhớ đăng nhập' })

Tham số name khớp với accessible name của phần tử - thường là label text, aria-label, hoặc nội dung button. Khớp không phân biệt hoa thường theo mặc định.

348097 Accessible name lấy từ label, aria-label hoặc nội dung text của phần tử

Khi nào dùng getByRole?

  • Button, link, checkbox, radio, combobox (select), dialog
  • Bất kỳ phần tử nào có semantic role rõ ràng trong HTML
  • Khi muốn test phản ánh đúng cách người dùng thực và screen reader tương tác với trang

Khi KHÔNG nên chỉ dùng getByRole:

  • Phần tử dùng role tùy chỉnh không chuẩn (custom component thiếu ARIA)
  • Trang dùng nhiều phần tử cùng role và không có name phân biệt được

getByLabel là gì và khi nào dùng

getByLabel() tìm input được liên kết với một <label> cụ thể. Đây là cách tự nhiên nhất để locate field trong form vì nó phản ánh đúng cách người dùng nhìn form.

Cú pháp:

page.getByLabel('Email')
page.getByLabel('Mật khẩu')
page.getByLabel('Ngày sinh')

Playwright tìm label có text khớp, rồi trả về input liên kết qua for/id hoặc label bọc input trực tiếp:

<!-- Cách 1: for/id -->
<label for="email">Email</label>
<input id="email" type="email" />

<!-- Cách 2: label bọc input -->
<label>
  Mật khẩu
  <input type="password" />
</label>

Cả hai cách trên đều hoạt động với getByLabel('Email') và getByLabel('Mật khẩu').

348098 getByLabel hoạt động với cả hai cách viết HTML label phổ biến

Khác biệt với getByRole:

  • getByLabel chuyên cho input fields trong form
  • getByRole('textbox', { name: 'Email' }) và getByLabel('Email') thường ra cùng kết quả - chọn cái nào đọc tự nhiên hơn trong context
  • getByLabel sẽ fail nếu input không có label liên kết đúng chuẩn (giả định minh họa: input thiếu label là lỗi accessibility luôn)

Ví dụ tối thiểu: test form đăng nhập

Dưới đây là ví dụ giả định minh họa - form HTML đơn giản và test Playwright tương ứng. Bạn có thể tự chạy để kiểm tra.

Form HTML mẫu (giả định):

<!DOCTYPE html>
<html lang="vi">
<head><meta charset="UTF-8"><title>Đăng nhập</title></head>
<body>
  <form>
    <div>
      <label for="email">Email</label>
      <input id="email" type="email" name="email" />
    </div>
    <div>
      <label for="password">Mật khẩu</label>
      <input id="password" type="password" name="password" />
    </div>
    <div>
      <label>
        <input type="checkbox" name="remember" /> Ghi nhớ đăng nhập
      </label>
    </div>
    <button type="submit">Đăng nhập</button>
  </form>
</body>
</html>

Test Playwright (TypeScript):

import { test, expect } from '@playwright/test';

test('điền và submit form đăng nhập', async ({ page }) => {
  // Mở file HTML local - thay bằng URL thật trong dự án
  await page.goto('file:///path/to/login.html');

  // Điền email qua label
  await page.getByLabel('Email').fill('[email protected]');

  // Điền mật khẩu qua label
  await page.getByLabel('Mật khẩu').fill('matkhau123');

  // Check checkbox qua role + name
  await page.getByRole('checkbox', { name: 'Ghi nhớ đăng nhập' }).check();

  // Verify checkbox đã được check
  await expect(
    page.getByRole('checkbox', { name: 'Ghi nhớ đăng nhập' })
  ).toBeChecked();

  // Click button submit qua role + name
  await page.getByRole('button', { name: 'Đăng nhập' }).click();
});

348099 Form đơn giản, test rõ ràng - không cần CSS selector nào

Kết quả mong đợi khi chạy đúng: Test pass, không có assertion error. Nếu label text không khớp chính xác, Playwright báo getByLabel('...') resolved to X elements hoặc no element found.


Cách chạy và cấu hình isolation

Cài đặt và chạy lần đầu:

# Cài Playwright (nếu chưa có)
npm init playwright@latest

# Chạy tất cả test
npx playwright test

# Chạy một file test cụ thể
npx playwright test tests/login.spec.ts

# Chạy với UI mode để debug trực quan
npx playwright test --ui

Isolation: Playwright tạo browser context mới cho mỗi test theo mặc định - nghĩa là cookie, localStorage, session không bị chia sẻ giữa các test. Đây là isolation tự động, không cần cấu hình thêm.

Tuy nhiên nếu test dùng state chung (ví dụ: đăng nhập một lần rồi dùng cho nhiều test), bạn cần storageState:

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    // Tất cả test dùng cùng storage state (đã đăng nhập sẵn)
    // Chỉ dùng khi cần - hiểu rõ trade-off mất isolation
    // storageState: 'playwright/.auth/user.json',
  },
  // Mỗi test file chạy độc lập
  fullyParallel: true,
});

348100 Mỗi test có browser context riêng - isolation mặc định của Playwright

Cách tự kiểm tra isolation đang hoạt động:

test('test A set localStorage', async ({ page }) => {
  await page.goto('about:blank');
  await page.evaluate(() => localStorage.setItem('key', 'value'));
});

test('test B không thấy localStorage của test A', async ({ page }) => {
  await page.goto('about:blank');
  const val = await page.evaluate(() => localStorage.getItem('key'));
  expect(val).toBeNull(); // Luôn pass nếu isolation hoạt động
});

Chạy hai test này theo thứ tự, cả hai phải pass.


Lỗi thường gặp và cách xử lý

1. getByLabel không tìm thấy phần tử

Nguyên nhân phổ biến nhất: label không liên kết đúng với input.

<!-- Sai: label và input không liên kết -->
<label>Email</label>
<input type="email" />

<!-- Đúng: dùng for/id -->
<label for="email">Email</label>
<input id="email" type="email" />

Nếu không thể sửa HTML (component của bên thứ ba), dùng getByPlaceholder hoặc getByRole với name lấy từ aria-label thay thế.

2. getByRole trả về nhiều phần tử

Xảy ra khi trang có nhiều button cùng text hoặc nhiều input cùng role mà không có name phân biệt. Fix:

// Thêm name để thu hẹp
page.getByRole('button', { name: 'Đăng nhập' })

// Hoặc dùng within để giới hạn scope
const form = page.getByRole('form', { name: 'Form đăng nhập' })
form.getByRole('button', { name: 'Đăng nhập' })

3. Khớp text không chính xác

Theo mặc định name khớp substring không phân biệt hoa thường. Nếu cần khớp chính xác:

page.getByRole('button', { name: /^Đăng nhập$/ }) // dùng regex

348101 Ba lỗi hay gặp và cách fix tương ứng

4. Component thiếu ARIA attribute

Custom component (React, Vue) đôi khi render ra <div> không có role. Ví dụ giả định: <div class="custom-checkbox"> sẽ không có role checkbox trừ khi dev thêm role="checkbox" và aria-checked. Trường hợp này, báo dev fix hoặc tạm dùng locator('[data-testid="..."]') - nhưng cần thêm data-testid vào HTML.

Trang web dùng cookie để hoạt động và đo hiệu quả quảng cáo. Xem chính sách bảo mật