Mục lục
- Mục tiêu và cách học
- Chuẩn bị môi trường
- Cài đặt Playwright
- Khởi tạo và triển khai project
- Cấu hình nền tảng
- Viết test đầu tiên
- Locator: tìm phần tử bền vững
- Action, assertion và các hàm thường dùng
- Auto-wait và đồng bộ
- Tổ chức code cho dự án thật
- Dữ liệu, đăng nhập, upload và download
- Network, popup, iframe và nhiều tab
- Chạy test, debug và đọc report
- Codegen và công cụ trong VS Code
- Giảm flaky và quy chuẩn review
- Cheatsheet
- Bài thực hành và lộ trình đào tạo
- Xử lý sự cố
- Duy trì handbook HTML và thêm ảnh
- Tài liệu tham khảo
Lưu ý tương thích. Tại thời điểm cập nhật handbook, tài liệu chính thức của Playwright liệt kê Node.js 22.x/24.x/26.x và Windows 11+ hoặc Windows Server 2019+ trong yêu cầu hệ thống. Node.js 22 phù hợp, nhưng Windows 10 không còn nằm trong danh sách hỗ trợ chính thức của bản mới nhất. Bộ test có thể vẫn chạy trong môi trường nội bộ, nhưng nhóm cần chạy bài smoke bên dưới và chốt một phiên bản Playwright đã được kiểm chứng; phương án dài hạn là nâng hệ điều hành. Không nên hiểu “cài được” là “được nhà cung cấp hỗ trợ”.
1. Mục tiêu và cách học
1.1 Sau handbook này bạn làm được gì?
Bạn có thể:
- chuẩn bị Node.js, npm/Nexus, VS Code và browser runtime;
- tạo một project Playwright Test bằng TypeScript;
- viết test theo mô hình Arrange – Act – Assert;
- chọn locator ưu tiên accessibility thay vì CSS/XPath mong manh;
- hiểu auto-wait, web-first assertion và nguyên nhân test flaky;
- tổ chức Page Object, fixture, test data và authentication state;
- chạy theo file, tag, project, UI Mode, debug mode;
- dùng trace, screenshot, video và HTML report để điều tra lỗi;
- dùng Codegen như công cụ khám phá rồi tinh chỉnh code;
- duy trì handbook và dựng lại HTML khi có nội dung hoặc ảnh mới.
1.2 Phạm vi
Handbook dùng Playwright Test + TypeScript + Chromium làm đường cơ sở. Firefox/WebKit được giới thiệu như project mở rộng. Trọng tâm là UI end-to-end; API testing và CI chỉ trình bày ở mức hỗ trợ UI test.
Handbook đồng thời cũng sẽ cố gắng giải thích các khái niệm ở mức cơ bản nhất để người đọc nắm rõ:
- Là gì?
- Tại sao?
- Để làm gì?
Các ví dụ dùng ứng dụng giả định có URL https://app.example.internal. Hãy thay bằng URL, tài khoản test và data-testid thật của hệ thống.
1.3 Tư duy nền tảng
Một UI test tốt cần bốn thuộc tính:
- Độc lập: không phụ thuộc test chạy trước.
- Có thể lặp lại: cùng đầu vào cho cùng kết quả trong cùng điều kiện.
- Quan sát được: lỗi có trace, screenshot, log hoặc report đủ để điều tra.
- Bảo trì được: locator, dữ liệu và nghiệp vụ không trộn lẫn tùy tiện.
Playwright tự động chờ rất tốt, nhưng không tự hiểu nghiệp vụ. Người viết test vẫn phải xác định đúng điều kiện “hệ thống đã sẵn sàng” và “kết quả đã đúng”.
2. Chuẩn bị môi trường
2.1 Checklist trước khi cài
| Thành phần | Mục tiêu | Cách kiểm tra |
|---|---|---|
| Windows | Windows 10 theo môi trường nội bộ | winver |
| Node.js | Major version 22 | node --version |
| npm | Đi kèm Node.js | npm --version |
| Nexus | npm đọc user config nội bộ | npm config get userconfig |
| Registry | URL Nexus của tổ chức | npm config get registry |
| VS Code | Bản được tổ chức phê duyệt | menu Help → About |
| Git | Khuyến nghị cài | git --version |
Mở PowerShell trong VS Code bằng Terminal → New Terminal, sau đó chạy:
node --version
npm --version
npm config get userconfig
npm config get registry
npm ping
Kỳ vọng với môi trường của handbook:
v22.x.x
C:\Users\<tên_user>\.npmrc
<URL Nexus nội bộ>
Nếu npm config get registry không trỏ đến Nexus nội bộ, người đọc sẽ làm như sau:
Lệnh đặt registry:
npm config set registry <registry_url>
Sau khi hoàn thành lệnh, kiểm tra lại với npm config get registry một lần nữa
Sau đó thực hiện đăng nhập registry lần đầu
npm login registry=<registry_url>
Sau đó, powershell sẽ yêu cầu người đọc nhập Username

Sau đó enter, powershell tiếp tục yêu cầu người đọc nhập password

Khi nhập password, powershell sẽ giữ nguyên biểu tượng xoay mà không hiển thị user đã nhập đến đâu, nên cần nhập cẩn thận
Sau khi hoàn thành, powershell sẽ thong báo đã Logged in thành công

**Các tham số hiện tại (cập nhật 6/8/2026) **
registry_url : http://10.1.12.177:8445/repository/active-npm-public/
username : dev-ntkt
password : d89db12kglo
2.2 Nexus và hai loại tải xuống khác nhau
Nexus npm cung cấp các package như @playwright/test. Browser của Playwright là binary riêng, được tải bằng npx playwright install; binary này không nhất thiết đi qua npm registry. Vì vậy có thể gặp tình huống npm install thành công nhưng cài browser thất bại.
Tại MB, với policy cơ bản thì tại khâu npx playwright install sẽ bị lỗi, do bản chất kết nối tới server host binary chromium đã bị chặn policy. Một giải pháp khác chính là trỏ playwright tới một trình duyệt chromium đã được cài đặt từ trước, để đảm bảo tính tương thích tuyệt đối, cần sử dụng chính xác phiên bản chromium cho phiên bản playwright đang sử dụng, chi tiết được mô tả ở phần cài đặt playwright.
Nếu tổ chức có mirror browser riêng, quản trị viên có thể cung cấp biến PLAYWRIGHT_DOWNLOAD_HOST. Không tự đoán URL. Nếu cần proxy, dùng cấu hình proxy đã được tổ chức phê duyệt. Ví dụ dạng lệnh, không phải giá trị thật:
$env:PLAYWRIGHT_DOWNLOAD_HOST = "https://mirror-noi-bo.example/playwright"
npx playwright install chromium
Playwright mặc định lưu browser trong cache dùng chung theo người dùng. Có thể kiểm tra browser mà phiên bản hiện tại cần bằng:
npx playwright install --list
2.3 Cài extension VS Code
Trong VS Code, mở Extensions (Ctrl+Shift+X), tìm Playwright Test for VSCode của Microsoft và cài đặt. Extension hỗ trợ:
- chạy/debug test ngay bên cạnh dòng code;
- hiển thị cây test trong Testing panel;
- chọn browser/project;
- Record new test và Pick locator;
- mở trace cho lần chạy liên quan.

Extension giúp thao tác nhanh, nhưng toàn bộ lệnh trong handbook vẫn chạy được từ terminal để nhóm không phụ thuộc giao diện.
3. Cài đặt Playwright
3.1 Cách A — khởi tạo tự động
Tạo thư mục project và chạy trình khởi tạo:
New-Item -ItemType Directory -Path ui-automation
Set-Location ui-automation
npm init playwright@latest
Chọn:
- TypeScript - sử dụng phím mũi tên trên bàn phím - mặc định đã chọn
TypeScript- chỉ cần ấnEnter; - thư mục test:
tests- đã được điền mặc định, có thể ấnEnter; - GitHub Actions: Chọn No - chỉ cần ấn phím
n; - install browsers: Yes nếu máy truy cập được nguồn browser, nếu không chọn No và cài riêng. Tại MB, nên cài riêng vì đa số nhân sự sẽ không thể cài đặt trình duyệt tự động - chỉ cần ấn phím
n;
Nếu powershell hỏi
Need to install the following packages:
create-playwright@1.17.139
Ok to proceed? (y)
Gõ y rồi Enter

Kiểm tra:
npx playwright --version
npx playwright test
npx playwright show-report
Lưu ý: khi này thư mục làm việc của Playwright sẽ chính là thư mục ui-automation đã tạo với 2 câu lệnh đầu mục 3.1. Để playwright hoạt động bình thường, cần đảm bảo powershell đã được trỏ tới thư mục này
3.2 Hạ cấp Playwright - để phù hợp với phiên bản Chrome
Cách này phù hợp khi bạn đã có sẵn một phiên bản Chrome, vì playwright được phát triển để mỗi phiên bản hoạt động chính xác với một phiên bản Chrome cụ thể
npm install -D @playwright/test@1.55.0
npx playwright --version
npx playwright install --list
Trong tài liệu này, tác giả duyvt2 khuyến khích sử dụng phiên bản 1.55.0 để tương thích với Chrome 148.0.7778.96
Người đọc có thể vào thư mục oneshare tại
\\vdi-onefile.bank.mb.group\KhoiCNTT\duyvt2\Chrome
Lấy về file chrome-win64.zip, đặt ở ổ D:\\ và giải nén. Sau đó, đường dẫn tới folder có chứa binary chrome.exe sẽ là D:\chrome-win64\chrome-win64.
Người đọc mở trình sửa biến môi trường Windowss 10

Sau đó, thực hiện thêm biến



**Điều thêm biến như sau **
Variable Name : CHROME_PATH
Variable value: D:\chrome-win64\chrome-win64\chrome.exe
Tùy thuộc vào vị trí giải nén chrome-win64 mà giá trị này sẽ thay đổi, điều quan trọng là trỏ đến chính xác binary chrome.exe
Sau đó ấn OK, rồi tiếp tục OK ở cửa sổ Environment Variables để thay đổi được ghi nhận

Sau đó chúng ta có thể mở lại powershell và thử lệnh chạy smoke nhanh xác nhận tương thích
Sau đó chúng ta sẽ mở file playwright.config.ts trong thư mục làm việc ui-automation. File sau khi mở sẽ có nội dung như sau:
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://localhost:3000',
// reuseExistingServer: !process.env.CI,
// },
});
Chúng ta quan tâm đến phần
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
Tại đây chúng ta sẽ sửa: mở rộng object dictionary {} và thêm 2 key là executablePath và channel
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
executablePath: process.env.CHROME_PATH,
channel: 'chrome'
},
},
Ctrl + S để lưu file.
Với config này, playwright sẽ hiểu chúng ta đang sử dụng đường dẫn chrome là theo biến CHROME_PATH của môi trường chúng ta đã cài đặt ở trên
3.3 Smoke test xác nhận Windows 10
Vì Windows 10 không nằm trong phạm vi hỗ trợ chính thức của Playwright mới nhất, hãy chạy smoke test ngay sau cài đặt:
npx playwright test --project=chromium --workers=1
Khi Playwright hoạt động bình thường, powershell sẽ thông báo chúng ta test đã chạy

Đồng thời trình duyệt mặc định sẽ mở ra báo cáo HTML đơn giản về kết quả chạy test vừa rồi

Có thể thoát để tiếp tục đi lệnh bằng tổ hợp Ctrl + C trong powershell
Tiếp tục kiểm tra chế độ headed - tức là trình duyệt sẽ không bị ẩn đi nữa
npx playwright test --project=chromium --workers=1 --headed
Khi chạy playwright, mặc định trình duyệt sẽ ở chế độ headless - tức là ẩn trình duyệt, với chế độ headed - trình duyệt được hiển thị và chúng ta có thể thấy nó thực hiện auto thế nào
Với chế độ --headed thì 2 bài test smoke của playwright sẽ pass

Tiêu chí đạt:
- Chromium khởi động được ở headless và headed
3.4 Lệnh xác minh nhanh
Hãy xác minh lại toàn bộ quá trình để đảm bảo mọi thứ đã hoạt động bình thường
node --version
npm config get registry
npm ls @playwright/test
npx playwright --version
npx playwright install --list
Nên thấy
>> npm config get registry
>> npm ls @playwright/test
>> npx playwright --version
>> npx playwright install --list
v22.20.0
http://10.1.12.177:8445/repository/active-npm-public/
ui-automation@1.0.0 D:\Test\ui-automation
`-- @playwright/test@1.55.0
Version 1.55.0
4. Khởi tạo và triển khai project
Là gì? Đây là bước biến một thư mục trống thành repository kiểm thử có cấu trúc và quy ước chung. Tại sao? Nếu mỗi người đặt file, cấu hình và dữ liệu theo một cách khác nhau, bộ test sẽ nhanh chóng khó chạy và khó review. Để làm gì? Tạo một nền móng mà thành viên mới, máy CI và người bảo trì đều có thể cài, chạy và mở rộng theo cùng một quy trình.
4.1 Cấu trúc khuyến nghị
Là gì? Cấu trúc project là cách phân chia test, Page Object, fixture, dữ liệu và tiện ích thành các khu vực có trách nhiệm rõ ràng. Tại sao? Tách đúng trách nhiệm giúp tránh lặp code và tránh biến file test thành một kịch bản dài khó đọc. Để làm gì? Giúp người đọc biết logic nghiệp vụ nằm ở đâu, logic tương tác UI nằm ở đâu và nên thêm code mới vào vị trí nào.
ui-automation/
├─ .auth/ # storage state, không commit
├─ fixtures/ # fixture mở rộng
│ └─ test-fixtures.ts
├─ pages/ # Page Object
│ ├─ login.page.ts
│ └─ dashboard.page.ts
├─ test-data/ # dữ liệu tĩnh không nhạy cảm
│ └─ users.ts
├─ tests/
│ ├─ auth.setup.ts
│ ├─ login.spec.ts
│ └─ orders/
│ └─ create-order.spec.ts
├─ utils/ # helper không thuộc Page Object
├─ playwright.config.ts
├─ package.json
├─ package-lock.json
├─ tsconfig.json
├─ .env.example # chỉ tên biến, không chứa secret
└─ .gitignore
Nguyên tắc:
tests: mô tả hành vi và kỳ vọng nghiệp vụ;pages: chi tiết tương tác UI dùng lại;fixtures: cách tạo ngữ cảnh/test dependency;test-data: dữ liệu đọc được, không chứa mật khẩu/token;utils: thao tác kỹ thuật dùng chung, không biến thành “sọt helper”.
4.2 package.json mẫu
Là gì?
package.jsonlà manifest của project Node.js, khai báo metadata, dependency và các lệnh chuẩn trongscripts. Tại sao? Nó khóa cách cả nhóm gọi Playwright và cho biết project cần những package nào. Để làm gì? Dùng các lệnh ngắn, nhất quán nhưnpm test, đồng thời giúp local và CI cài đúng công cụ cần thiết.
{
"name": "ui-automation",
"private": true,
"scripts": {
"test": "playwright test",
"test:ui": "playwright test --ui",
"test:debug": "playwright test --debug",
"test:smoke": "playwright test --grep @smoke",
"report": "playwright show-report"
},
"devDependencies": {
"@playwright/test": "1.62.1",
"typescript": "^5.9.0"
}
}
Chạy npm install để tạo/cập nhật lockfile. Trong CI nên dùng npm ci, vì lệnh này cài đúng lockfile và báo lỗi khi package.json lệch package-lock.json.
4.3 .gitignore mẫu
Là gì?
.gitignorelà danh sách file và thư mục Git không được đưa vào lịch sử source code. Tại sao? Artifact test, dependency, secret và authentication state vừa nặng vừa có thể chứa dữ liệu nhạy cảm. Để làm gì? Giữ repository sạch, giảm xung đột và ngăn vô tình commit token, cookie, report hoặc file tải xuống.
node_modules/
test-results/
playwright-report/
blob-report/
.auth/
.env
downloads/
Không commit storageState, .env, file download chứa dữ liệu khách hàng, trace của môi trường thật hoặc screenshot có thông tin nhạy cảm.
4.4 Quy trình đưa project cho thành viên mới
Là gì? Đây là quy trình onboarding để tái tạo project từ repository trên một máy mới. Tại sao? Một project chỉ chạy trên máy người tạo chưa phải là project có thể bàn giao. Để làm gì? Xác nhận lockfile, browser, biến môi trường và lệnh smoke đủ rõ để bất kỳ thành viên được cấp quyền nào cũng khởi động được bộ test.
git clone <repository-url>
Set-Location ui-automation
npm ci
npx playwright install chromium
Copy-Item .env.example .env
npx playwright test --project=chromium
Người nhận điền .env bằng secret từ kênh được phê duyệt. README của project phải ghi rõ URL môi trường, quyền tài khoản cần có, cách lấy secret và lệnh smoke test.
5. Cấu hình nền tảng
Là gì? Cấu hình nền tảng là tập hợp chính sách chạy test dùng chung cho toàn bộ project. Tại sao? Browser, timeout, retry, report và artifact nếu khai báo rải rác sẽ tạo kết quả không nhất quán. Để làm gì? Đặt một nguồn cấu hình trung tâm để local, CI và các suite sử dụng cùng hành vi mặc định.
5.1 playwright.config.ts mẫu
Là gì?
playwright.config.tslà file cấu hình chính của Playwright Test. Tại sao? Đây là nơi framework đọc các quyết định ở cấp project trước khi thu thập và chạy test. Để làm gì? Khai báo thư mục test, browser project, base URL, parallelism, retry, timeout, reporter, screenshot, video và trace.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 2 : undefined,
timeout: 30_000,
expect: { timeout: 5_000 },
reporter: [
['list'],
['html', { open: 'never', outputFolder: 'playwright-report' }],
],
use: {
baseURL: process.env.BASE_URL ?? 'https://app.example.internal',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
actionTimeout: 10_000,
navigationTimeout: 20_000,
testIdAttribute: 'data-testid',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
outputDir: 'test-results',
});
Trong giai đoạn đầu, có thể chỉ để project chromium để feedback nhanh. Mở rộng Firefox/WebKit khi test đã ổn định và sản phẩm thực sự hỗ trợ các browser đó.
5.2 Hiểu các nhóm timeout
Là gì? Timeout là ngân sách thời gian tối đa dành cho test, assertion, action hoặc navigation. Tại sao? Không có giới hạn, một lỗi đồng bộ có thể làm test treo; giới hạn quá lớn lại làm feedback chậm và che hiệu năng kém. Để làm gì? Fail có kiểm soát và giúp khoanh vùng thao tác nào vượt quá thời gian chấp nhận được.
| Cấu hình | Phạm vi | Gợi ý |
|---|---|---|
timeout | Toàn bộ một test | 30 giây là điểm bắt đầu |
expect.timeout | Web-first assertion | 5 giây |
actionTimeout | Mỗi action | 10 giây hoặc 0 để dùng timeout chung |
navigationTimeout | Điều hướng | Theo tốc độ môi trường |
Không tăng mọi timeout lên vài phút để “chữa” flaky. Trước tiên phải tìm điều kiện chờ sai, dữ liệu xung đột, API chậm hoặc locator không duy nhất.
5.3 Biến môi trường
Là gì? Biến môi trường là giá trị được truyền từ máy chạy vào test mà không ghi cứng trong source code. Tại sao? URL, tài khoản và secret thay đổi theo môi trường và không nên commit vào Git. Để làm gì? Chạy cùng một bộ test trên nhiều môi trường bằng cách thay cấu hình bên ngoài code.
PowerShell cho một phiên terminal:
$env:BASE_URL = "https://test.example.internal"
$env:E2E_USERNAME = "automation-user"
$env:E2E_PASSWORD = "<lay-tu-secret-store>"
npx playwright test
Đọc trong TypeScript:
const username = process.env.E2E_USERNAME;
if (!username) throw new Error('Thiếu E2E_USERNAME');
Fail sớm với thông báo rõ ràng tốt hơn để test chạy đến trang login rồi timeout khó hiểu.
6. Viết test đầu tiên
Là gì? Một test Playwright là một kịch bản độc lập mô tả hành vi người dùng và kết quả mong đợi. Tại sao? Automation chỉ có giá trị khi nó xác nhận được một yêu cầu cụ thể, không phải chỉ thao tác qua giao diện. Để làm gì? Biến acceptance criteria thành kiểm tra có thể chạy lặp lại và cung cấp bằng chứng khi sản phẩm sai.
6.1 Anatomy của một test
Là gì? Anatomy là các thành phần tạo nên một test: tên test, fixture, bước chuẩn bị, hành động và assertion. Tại sao? Hiểu vai trò từng phần giúp test có intent rõ và dễ xác định nơi gây lỗi. Để làm gì? Viết test theo luồng Arrange – Act – Assert thay vì trộn dữ liệu, thao tác và kiểm tra vào nhau.
import { test, expect } from '@playwright/test';
test('người dùng đăng nhập thành công @smoke', async ({ page }) => {
// Arrange
await page.goto('/login');
// Act
await page.getByLabel('Tên đăng nhập').fill('demo-user');
await page.getByLabel('Mật khẩu').fill('secret');
await page.getByRole('button', { name: 'Đăng nhập' }).click();
// Assert
await expect(page).toHaveURL(/\/dashboard$/);
await expect(page.getByRole('heading', { name: 'Tổng quan' })).toBeVisible();
});
Điểm quan trọng:
- callback là
asyncvà mọi action/assertion bất đồng bộ đều cóawait; pagelà fixture biệt lập cho test;- URL tương đối dùng được nhờ
baseURL; - tên test mô tả hành vi, không đặt kiểu
TC01đơn độc; - tag
@smokecho phép lọc bằng--grep.
6.2 Nhóm test và hook
Là gì?
test.describenhóm các test cùng chủ đề; hook nhưbeforeEachchạy logic chuẩn bị tại thời điểm xác định. Tại sao? Setup lặp lại làm test dài, nhưng chia sẻ state sai cách lại phá isolation. Để làm gì? Tái sử dụng bước chuẩn bị an toàn mà vẫn giữ mỗi test độc lập và dễ chạy riêng.
import { test, expect } from '@playwright/test';
test.describe('Quản lý hồ sơ', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/profile');
});
test('hiển thị tên người dùng', async ({ page }) => {
await expect(page.getByTestId('display-name')).toHaveText('Nguyễn An');
});
test('cập nhật số điện thoại', async ({ page }) => {
await page.getByLabel('Số điện thoại').fill('0900000000');
await page.getByRole('button', { name: 'Lưu' }).click();
await expect(page.getByRole('status')).toHaveText('Cập nhật thành công');
});
});
Không dùng beforeAll để chia sẻ cùng một page giữa các test. Mỗi test nên có browser context riêng để giữ isolation.
6.3 Steps làm report dễ đọc
Là gì?
test.stepđặt tên cho một nhóm thao tác bên trong test. Tại sao? Report chỉ gồm nhiều lệnh click/fill thường khó phản ánh luồng nghiệp vụ. Để làm gì? Biến trace và report thành các bước có nghĩa như “Nhập thông tin đơn” hoặc “Kiểm tra kết quả”, giúp điều tra lỗi nhanh hơn.
test('tạo đơn hàng', async ({ page }) => {
await test.step('Mở form tạo đơn', async () => {
await page.goto('/orders/new');
});
await test.step('Nhập thông tin đơn', async () => {
await page.getByLabel('Mã khách hàng').fill('CUS-001');
await page.getByLabel('Số lượng').fill('2');
});
await test.step('Lưu và kiểm tra kết quả', async () => {
await page.getByRole('button', { name: 'Tạo đơn' }).click();
await expect(page.getByRole('status')).toContainText('Tạo thành công');
});
});
7. Locator: tìm phần tử bền vững
Là gì? Locator là mô tả có thể tái sử dụng để Playwright tìm element tại thời điểm cần tương tác hoặc kiểm tra. Tại sao? DOM thay đổi liên tục; selector gắn với class hoặc vị trí dễ hỏng dù hành vi người dùng không đổi. Để làm gì? Kết nối test với UI bằng vai trò, nhãn và định danh ổn định, đồng thời tận dụng auto-wait và retry của Playwright.
7.1 Thứ tự ưu tiên
Là gì? Thứ tự ưu tiên locator là quy tắc chọn cách tìm element từ ngữ nghĩa người dùng đến selector kỹ thuật. Tại sao? Role, label và accessible name thường ổn định hơn cấu trúc DOM. Để làm gì? Tạo locator dễ đọc, hỗ trợ accessibility và ít phải sửa khi giao diện được refactor.
Ưu tiên locator phản ánh cách người dùng và công nghệ hỗ trợ tiếp cận nhìn thấy trang:
getByRole()với accessible name;getByLabel()cho form control;getByPlaceholder(),getByText(),getByAltText(),getByTitle();getByTestId()cho điểm neo ổn định do đội phát triển cung cấp;- CSS locator khi không có lựa chọn ngữ nghĩa;
- XPath chỉ như phương án cuối.
page.getByRole('button', { name: 'Lưu' });
page.getByLabel('Email');
page.getByPlaceholder('Nhập từ khóa');
page.getByText('Không có dữ liệu', { exact: true });
page.getByAltText('Logo công ty');
page.getByTestId('order-total');
7.2 Thu hẹp phạm vi
Là gì? Thu hẹp phạm vi là tìm một vùng cha có ý nghĩa trước, rồi tìm element con bên trong vùng đó. Tại sao? Những nút như “Sửa”, “Xóa”, “Lưu” thường xuất hiện nhiều lần trên một trang. Để làm gì? Chỉ rõ đúng dòng, card, dialog hoặc form cần thao tác mà không phụ thuộc vào vị trí thứ tự.
Nếu có nhiều nút “Xóa”, tìm đúng dòng trước:
const row = page.getByRole('row').filter({ hasText: 'CUS-001' });
await row.getByRole('button', { name: 'Xóa' }).click();
await expect(row).toBeHidden();
Hoặc dùng has:
const product = page.getByRole('listitem').filter({
has: page.getByRole('heading', { name: 'Playwright Course' }),
});
await product.getByRole('button', { name: 'Thêm vào giỏ' }).click();
7.3 Strictness là tín hiệu tốt
Là gì? Strictness yêu cầu locator dùng cho action phải resolve thành đúng một element. Tại sao? Nếu nhiều element cùng khớp, Playwright không thể biết ý định thật của test. Để làm gì? Phát hiện locator mơ hồ sớm và buộc người viết diễn đạt chính xác phần tử nghiệp vụ cần tương tác.
Action trên locator cần trỏ đến đúng một phần tử. Nếu locator khớp nhiều phần tử, Playwright báo strict mode violation. Hãy sửa locator bằng tên, vai trò hoặc phạm vi nghiệp vụ; đừng mặc định thêm .first() để che lỗi.
// Không rõ nút nào
await page.getByRole('button', { name: 'Lưu' }).click();
// Rõ form nào
const profileForm = page.getByRole('form', { name: 'Thông tin cá nhân' });
await profileForm.getByRole('button', { name: 'Lưu' }).click();
7.4 Locator nên tránh
Là gì? Đây là các locator phụ thuộc sâu vào DOM, class sinh tự động, XPath dài hoặc vị trí
nth. Tại sao? Chúng mô tả cách trang được dựng thay vì điều người dùng nhìn thấy. Để làm gì? Nhận diện technical debt trong selector và biết khi nào cần thay bằng role, label hoặcdata-testid.
// Phụ thuộc sâu vào DOM và class sinh tự động
page.locator('div.container > div:nth-child(3) > button.css-1a2b3c');
// Phụ thuộc vị trí
page.locator('(//button)[7]');
// Text quá rộng, có thể khớp nhiều nơi
page.getByText('Lưu');
Nếu sản phẩm khó định vị, đề xuất dev bổ sung role/label đúng chuẩn hoặc data-testid. Testability là một thuộc tính của sản phẩm, không chỉ là trách nhiệm của QA.
8. Action, assertion và các hàm thường dùng
Là gì? Action mô phỏng thao tác của người dùng; assertion kiểm tra trạng thái hoặc kết quả sau thao tác. Tại sao? Chỉ action mà không assertion thì test không chứng minh được điều gì, còn assertion không có luồng phù hợp thì không tái hiện đúng nghiệp vụ. Để làm gì? Tạo kịch bản vừa thực hiện hành vi vừa xác nhận hệ thống phản hồi đúng.
8.1 Điều hướng và thao tác cơ bản
Là gì? Đây là nhóm API điều hướng trang và tác động lên element như click, fill, check, select hoặc press. Tại sao? Chúng mô phỏng các tương tác quan sát được của người dùng trên trình duyệt. Để làm gì? Xây dựng phần “Act” của test bằng API có actionability checks và auto-wait sẵn.
await page.goto('/orders');
await page.reload();
await page.goBack();
await page.getByRole('button', { name: 'Tạo mới' }).click();
await page.getByLabel('Tên').fill('Nguyễn An');
await page.getByLabel('Ghi chú').pressSequentially('Nội dung', { delay: 30 });
await page.getByLabel('Trạng thái').selectOption('ACTIVE');
await page.getByLabel('Nhận thông báo').check();
await page.getByLabel('Nhận thông báo').uncheck();
await page.getByText('Chi tiết').hover();
await page.getByLabel('Tìm kiếm').press('Enter');
Dùng fill() cho hầu hết input. Chỉ dùng pressSequentially() khi ứng dụng thật sự xử lý từng sự kiện bàn phím, vì thao tác này chậm hơn.
8.2 Web-first assertion
Là gì? Web-first assertion là assertion nhận
LocatorhoặcPagevà tự retry đến khi điều kiện đạt hoặc hết timeout. Tại sao? UI thường cập nhật bất đồng bộ nên đọc giá trị một lần có thể quá sớm. Để làm gì? Kiểm tra trạng thái cuối theo cách ổn định hơn mà không cần tự viết vòng lặp hoặc sleep.
await expect(page).toHaveURL(/\/orders\/\d+$/);
await expect(page).toHaveTitle(/Đơn hàng/);
await expect(page.getByRole('heading')).toHaveText('Chi tiết đơn hàng');
await expect(page.getByRole('status')).toContainText('Thành công');
await expect(page.getByLabel('Email')).toHaveValue('qa@example.com');
await expect(page.getByRole('button', { name: 'Lưu' })).toBeEnabled();
await expect(page.getByTestId('loading')).toBeHidden();
await expect(page.getByRole('row')).toHaveCount(11);
await expect(page.getByLabel('Đồng ý')).toBeChecked();
Web-first assertion tự retry đến khi đạt hoặc timeout. Tránh snapshot giá trị quá sớm:
// Không tốt: đọc một lần, không retry trạng thái UI
expect(await page.getByRole('status').textContent()).toContain('Thành công');
// Tốt: tự retry locator assertion
await expect(page.getByRole('status')).toContainText('Thành công');
8.3 Soft assertion và polling
Là gì? Soft assertion ghi nhận lỗi nhưng cho test chạy tiếp; polling gọi lại một hàm cho đến khi giá trị đạt kỳ vọng. Tại sao? Có lúc cần thu nhiều sai lệch trong một màn hình hoặc chờ trạng thái nằm ngoài DOM. Để làm gì? Thu thập thêm bằng chứng và kiểm tra các tiến trình bất đồng bộ như job hoặc trạng thái API.
await expect.soft(page.getByTestId('name')).toHaveText('Nguyễn An');
await expect.soft(page.getByTestId('status')).toHaveText('Active');
await expect.poll(async () => {
const response = await page.request.get('/api/jobs/123');
return (await response.json()).status;
}).toBe('COMPLETED');
Soft assertion cho phép test tiếp tục để thu thêm bằng chứng, nhưng test vẫn fail khi kết thúc. Không lạm dụng nếu bước sau phụ thuộc kết quả bước trước.
8.4 Screenshot
Là gì? Screenshot là ảnh chụp toàn trang hoặc một element tại thời điểm test chạy. Tại sao? Error message không phải lúc nào cũng cho biết UI đang bị overlay, sai dữ liệu hay sai layout. Để làm gì? Bổ sung bằng chứng trực quan cho report, debug và đối chiếu nghiệp vụ; không dùng ảnh thay cho assertion.
await page.screenshot({ path: 'test-results/full-page.png', fullPage: true });
await page.getByTestId('invoice').screenshot({ path: 'test-results/invoice.png' });
Config screenshot: 'only-on-failure' thường đủ. Screenshot thủ công hữu ích cho bằng chứng nghiệp vụ hoặc điều tra điểm cụ thể.
9. Auto-wait và đồng bộ
Là gì? Đồng bộ là bảo đảm test chỉ hành động hoặc kiểm tra khi trạng thái cần thiết đã sẵn sàng; auto-wait là cơ chế Playwright hỗ trợ việc đó. Tại sao? Browser và ứng dụng cập nhật bất đồng bộ, nên tốc độ test không được giả định giống tốc độ UI. Để làm gì? Giảm race condition và flaky bằng cách chờ điều kiện thực thay vì chờ một số giây cố định.
9.1 Playwright tự chờ điều gì?
Là gì? Playwright tự chờ các actionability checks như visible, stable, enabled và receives events trước nhiều action. Tại sao? Element có mặt trong DOM chưa có nghĩa người dùng đã tương tác được. Để làm gì? Chỉ thực hiện action khi element ở trạng thái tương tác hợp lệ và trả lỗi có call log nếu điều kiện không đạt.
Trước một action như locator.click(), Playwright thực hiện các actionability checks phù hợp. Với click, phần tử phải:
- resolve thành đúng một element;
- visible;
- stable, không còn chuyển động;
- nhận được pointer event, không bị overlay che;
- enabled.
Nếu chưa đạt, Playwright chờ đến timeout. Assertions như toBeVisible() và toHaveText() cũng tự retry.
// Không cần sleep trước click
await page.getByRole('button', { name: 'Tiếp tục' }).click();
// Assertion tự chờ thông báo xuất hiện
await expect(page.getByRole('status')).toHaveText('Hoàn tất');
9.2 Không dùng chờ cứng
Là gì? Chờ cứng là dừng test một khoảng cố định bằng
waitForTimeout. Tại sao? Khoảng chờ có thể thừa trên máy nhanh nhưng vẫn thiếu trên máy chậm, làm suite vừa lâu vừa flaky. Để làm gì? Nhận biết và thay sleep bằng locator assertion, event hoặc trạng thái nghiệp vụ cụ thể.
// Tránh: nhanh/chậm tùy môi trường, làm suite chậm và flaky
await page.waitForTimeout(5000);
// Tốt: chờ điều kiện người dùng quan sát được
await expect(page.getByRole('progressbar')).toBeHidden();
await expect(page.getByRole('heading', { name: 'Kết quả' })).toBeVisible();
waitForTimeout() chỉ phù hợp khi debug tạm thời; không commit vào test chính thức.
9.3 Khi phải chờ response cụ thể
Là gì? Chờ response là theo dõi một network response thỏa URL, method và trạng thái mong muốn. Tại sao? Một số action hoàn tất về mặt UI trước khi backend xử lý xong. Để làm gì? Đồng bộ chính xác với request quan trọng, lấy dữ liệu phản hồi khi cần và sau đó tiếp tục xác nhận kết quả trên UI.
Đăng ký listener trước action gây ra request:
const responsePromise = page.waitForResponse(response =>
response.url().includes('/api/orders') &&
response.request().method() === 'POST' &&
response.ok(),
);
await page.getByRole('button', { name: 'Tạo đơn' }).click();
const response = await responsePromise;
const order = await response.json();
expect(order.id).toBeTruthy();
Sau đó vẫn nên assert UI, vì mục tiêu là kiểm thử trải nghiệm người dùng:
await expect(page.getByRole('status')).toContainText('Tạo thành công');
9.4 force không phải cách sửa đồng bộ
Là gì?
{ force: true }yêu cầu action bỏ qua một số kiểm tra khả năng tương tác. Tại sao? Nó có thể click xuyên qua trạng thái mà người dùng thật không thể thao tác và che bug UI. Để làm gì? Chỉ dùng cho trường hợp có chủ đích đã được phân tích, không dùng như cách mặc định để vượt timeout.
await locator.click({ force: true });
force bỏ qua một số actionability checks. Chỉ dùng khi hành vi cần kiểm thử thực sự đòi hỏi nó và đã hiểu vì sao phần tử bị che/không nhận event. Dùng force để vượt lỗi thường che bug UI hoặc locator sai.
9.5 networkidle không phải tín hiệu nghiệp vụ phổ quát
Là gì?
networkidlemô tả một khoảng thời gian không có network connection đang hoạt động, không mô tả chức năng đã sẵn sàng. Tại sao? Polling, analytics và WebSocket có thể làm trang không bao giờ idle hoặc idle khi UI vẫn chưa đúng. Để làm gì? Nhắc người viết chọn tín hiệu nghiệp vụ cụ thể như heading, dữ liệu, loading state hoặc response cần thiết.
Ứng dụng có polling, analytics hoặc WebSocket có thể không bao giờ “idle”. Ưu tiên chờ heading, table, status, response cụ thể hoặc trạng thái loading của chức năng đang kiểm thử.
10. Tổ chức code cho dự án thật
Là gì? Tổ chức code là cách phân tách trách nhiệm giữa test, UI abstraction, fixture, dữ liệu và tiện ích. Tại sao? Suite càng lớn thì chi phí thay đổi càng phụ thuộc vào cấu trúc, không chỉ vào việc test đang pass. Để làm gì? Giữ intent nghiệp vụ dễ đọc, giảm lặp lại và giới hạn phạm vi sửa khi UI hoặc quy trình thay đổi.
10.1 Page Object Model
Là gì? Page Object là class đóng gói locator và thao tác có ý nghĩa trên một trang hoặc khu vực giao diện. Tại sao? Nếu mọi spec tự viết lại locator và chuỗi click, một thay đổi UI sẽ buộc sửa nhiều nơi. Để làm gì? Cung cấp API ở mức nghiệp vụ như
login()hoặccreateOrder()để test tập trung vào hành vi và kỳ vọng.
pages/login.page.ts:
import { type Locator, type Page } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly username: Locator;
readonly password: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.username = page.getByLabel('Tên đăng nhập');
this.password = page.getByLabel('Mật khẩu');
this.submitButton = page.getByRole('button', { name: 'Đăng nhập' });
this.errorMessage = page.getByRole('alert');
}
async goto() {
await this.page.goto('/login');
}
async login(username: string, password: string) {
await this.username.fill(username);
await this.password.fill(password);
await this.submitButton.click();
}
}
Test:
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login.page';
test('không cho đăng nhập với mật khẩu sai', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('demo-user', 'wrong-password');
await expect(loginPage.errorMessage).toHaveText('Thông tin đăng nhập không đúng');
});
Page Object nên đóng gói cách tương tác, nhưng assertion nghiệp vụ thường để trong test để intent rõ. Có thể đặt assertion đặc thù UI trong component object khi thực sự dùng lại.
10.2 Component Object
Là gì? Component Object là abstraction cho thành phần UI tái sử dụng như header, modal, menu hoặc data grid. Tại sao? Một component có thể xuất hiện trên nhiều page và không thuộc riêng Page Object nào. Để làm gì? Tái sử dụng locator/hành vi ở đúng cấp độ, tránh Page Object quá lớn và tránh copy code giữa các trang.
Header, modal, data grid dùng trên nhiều trang nên tách thành component:
export class ConfirmDialog {
constructor(private readonly root: Locator) {}
async confirm() {
await this.root.getByRole('button', { name: 'Xác nhận' }).click();
}
async cancel() {
await this.root.getByRole('button', { name: 'Hủy' }).click();
}
}
10.3 Custom fixture
Là gì? Custom fixture mở rộng fixture mặc định của Playwright bằng dependency do project định nghĩa. Tại sao? Test thường cần Page Object, user, dữ liệu hoặc setup giống nhau nhưng vẫn phải có lifecycle rõ ràng. Để làm gì? Inject dependency vào test, chuẩn hóa setup/teardown và để Playwright quản lý phạm vi sử dụng an toàn.
fixtures/test-fixtures.ts:
import { test as base, expect } from '@playwright/test';
import { LoginPage } from '../pages/login.page';
type AppFixtures = {
loginPage: LoginPage;
};
export const test = base.extend<AppFixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
});
export { expect };
Sử dụng:
import { test, expect } from '../fixtures/test-fixtures';
test('hiển thị lỗi đăng nhập', async ({ loginPage }) => {
await loginPage.goto();
await loginPage.login('user', 'wrong');
await expect(loginPage.errorMessage).toBeVisible();
});
10.4 Test độc lập và dọn dữ liệu
Là gì? Test độc lập tự tạo điều kiện đầu vào và không cần test khác chạy trước; cleanup đưa môi trường về trạng thái an toàn. Tại sao? Shared state và phụ thuộc thứ tự là nguồn lỗi lớn khi chạy song song hoặc retry. Để làm gì? Cho phép chạy một test bất kỳ, chia shard, retry và điều tra lỗi mà không tạo hiệu ứng dây chuyền.
Không viết chuỗi test A tạo dữ liệu → test B sửa → test C xóa. Khi B fail, C và các lần chạy sau bị ảnh hưởng. Tốt hơn:
- mỗi test tạo dữ liệu riêng qua API/fixture;
- dùng mã duy nhất, ví dụ timestamp hoặc UUID;
- cleanup trong fixture hoặc
afterEach, nhưng cleanup phải chịu được dữ liệu không tồn tại; - nếu cần serial vì bản chất nghiệp vụ, đánh dấu rõ và giới hạn phạm vi.
test.describe.configure({ mode: 'serial' }); // chỉ khi thật sự bắt buộc
11. Dữ liệu, đăng nhập, upload và download
Là gì? Đây là nhóm kỹ thuật quản lý đầu vào, phiên đăng nhập và file trong các luồng UI thực tế. Tại sao? Dữ liệu và authentication thường quyết định test có độc lập, an toàn và tái lập được hay không. Để làm gì? Chuẩn bị đúng state trước test, giảm thao tác lặp và kiểm soát file/secret phát sinh trong lúc chạy.
11.1 Data-driven test
Là gì? Data-driven test chạy cùng một hành vi với nhiều bộ input và expected result. Tại sao? Copy nguyên test cho từng dữ liệu tạo lặp code và dễ lệch logic. Để làm gì? Mở rộng độ phủ validation hoặc rule nghiệp vụ trong khi mỗi case vẫn xuất hiện độc lập trên report.
const cases = [
{ input: '', message: 'Tên đăng nhập là bắt buộc' },
{ input: 'ab', message: 'Tên đăng nhập phải có ít nhất 3 ký tự' },
];
for (const item of cases) {
test(`validate username: ${JSON.stringify(item.input)}`, async ({ page }) => {
await page.goto('/register');
await page.getByLabel('Tên đăng nhập').fill(item.input);
await page.getByRole('button', { name: 'Đăng ký' }).click();
await expect(page.getByRole('alert')).toHaveText(item.message);
});
}
Mỗi case phải tạo một test riêng để report và retry độc lập.
11.2 Authentication state
Là gì? Authentication state là snapshot cookie, local storage và IndexedDB của một browser context sau đăng nhập. Tại sao? Đăng nhập lại bằng UI ở mọi test làm suite chậm và tạo thêm điểm lỗi không liên quan. Để làm gì? Tái sử dụng phiên đã xác thực cho các test phù hợp, đồng thời vẫn bảo vệ file state như một secret.
Đăng nhập một lần ở setup, lưu cookies/local storage rồi tái sử dụng. tests/auth.setup.ts:
import { test as setup, expect } from '@playwright/test';
const authFile = '.auth/user.json';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Tên đăng nhập').fill(process.env.E2E_USERNAME!);
await page.getByLabel('Mật khẩu').fill(process.env.E2E_PASSWORD!);
await page.getByRole('button', { name: 'Đăng nhập' }).click();
await expect(page).toHaveURL(/\/dashboard/);
await page.context().storageState({ path: authFile });
});
Config project dependency:
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: { ...devices['Desktop Chrome'], storageState: '.auth/user.json' },
dependencies: ['setup'],
},
],
File storage state có thể chứa cookie/token đăng nhập. Luôn đưa
.auth/vào.gitignore, lưu artifact CI có thời hạn ngắn và không gửi file qua chat/email.
Nếu các test thay đổi server-side state của cùng một tài khoản, dùng nhiều tài khoản theo worker hoặc tạo dữ liệu độc lập để tránh đụng nhau.
11.3 Upload file
Là gì? Upload test gán một hoặc nhiều file cục bộ cho
input[type=file]bằngsetInputFiles. Tại sao? Hộp chọn file của hệ điều hành không phải phần DOM và không nên được automation bằng tọa độ. Để làm gì? Kiểm tra luồng tải tài liệu lên một cách trực tiếp, lặp lại được và không phụ thuộc giao diện Windows.
await page.getByLabel('Tải tài liệu').setInputFiles('test-data/sample.pdf');
// Xóa lựa chọn file
await page.getByLabel('Tải tài liệu').setInputFiles([]);
Đường dẫn tương đối phụ thuộc working directory. Trong code dùng dài hạn, resolve từ project root để tránh chạy từ thư mục khác bị lỗi.
11.4 Download file
Là gì? Download event đại diện cho file mà trang bắt đầu tải xuống sau một action. Tại sao? Nếu đăng ký listener sau click, test có thể bỏ lỡ event xảy ra rất nhanh. Để làm gì? Chờ đúng file, kiểm tra tên/nội dung cần thiết và lưu artifact vào vị trí được kiểm soát.
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Xuất Excel' }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toMatch(/\.xlsx$/);
await download.saveAs(`downloads/${download.suggestedFilename()}`);
Đăng ký waitForEvent trước click. Dọn thư mục download và không commit dữ liệu thật.
12. Network, popup, iframe và nhiều tab
Là gì? Đây là các tình huống browser có nhiều nguồn sự kiện hoặc ngữ cảnh ngoài DOM chính của trang. Tại sao? Request, frame, dialog và tab mới không thể luôn xử lý bằng locator trên
pagehiện tại. Để làm gì? Chọn đúng đối tượng và đăng ký đúng event để test không bỏ lỡ hành vi bất đồng bộ.
12.1 Mock response
Là gì? Mock response chặn một request và trả về response do test kiểm soát thay vì gọi backend thật. Tại sao? Một số trạng thái hiếm, lỗi biên hoặc dữ liệu nhạy cảm khó tạo ổn định trên môi trường thật. Để làm gì? Kiểm tra cách UI phản ứng với dữ liệu xác định; không dùng mock để thay thế toàn bộ end-to-end coverage.
await page.route('**/api/profile', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ name: 'Nguyễn An', status: 'ACTIVE' }),
});
});
await page.goto('/profile');
await expect(page.getByText('Nguyễn An')).toBeVisible();
Mock giúp kiểm thử trạng thái khó tạo và giảm phụ thuộc, nhưng không thay thế toàn bộ end-to-end test với backend thật. Ghi rõ test nào mocked.
12.2 Sửa request hoặc tiếp tục request
Là gì? Network interception cho phép quan sát, sửa hoặc tiếp tục request trước khi nó rời browser context. Tại sao? Test đôi khi cần thêm header, mô phỏng tham số hoặc kiểm soát một dependency cụ thể. Để làm gì? Tạo điều kiện kiểm thử có chủ đích mà không phải sửa code sản phẩm hoặc dựng proxy riêng.
await page.route('**/api/search**', async route => {
const headers = {
...route.request().headers(),
'x-test-run': 'playwright',
};
await route.continue({ headers });
});
12.3 Popup hoặc tab mới
Là gì? Popup/tab mới là một đối tượng
Pagekhác được sinh ra từ action trên trang hiện tại. Tại sao? Locator của page cũ không thể truy cập nội dung của page mới. Để làm gì? Bắt event trước action, lấy đúngPagemới và tiếp tục assertion trong đúng ngữ cảnh.
const newPagePromise = page.waitForEvent('popup');
await page.getByRole('link', { name: 'Mở hóa đơn' }).click();
const invoicePage = await newPagePromise;
await invoicePage.waitForLoadState();
await expect(invoicePage).toHaveTitle(/Hóa đơn/);
Nếu event phát sinh ở context thay vì page:
const pagePromise = context.waitForEvent('page');
12.4 iframe
Là gì? iframe là một document HTML được nhúng bên trong trang nhưng có DOM riêng. Tại sao? Locator bắt đầu từ
pagekhông tự xuyên qua ranh giới frame. Để làm gì? DùngframeLocatorđể tìm và tương tác với element bên trong payment widget, editor hoặc nội dung nhúng.
const paymentFrame = page.frameLocator('[title="Payment"]');
await paymentFrame.getByLabel('Card number').fill('4111111111111111');
await paymentFrame.getByRole('button', { name: 'Pay' }).click();
frameLocator giữ cách định vị và auto-wait tự nhiên hơn việc lấy frame rồi tự truy vấn thủ công.
12.5 Dialog JavaScript
Là gì? JavaScript dialog là hộp
alert,confirm,prompthoặcbeforeunloaddo trình duyệt quản lý. Tại sao? Dialog chặn luồng trang và không phải element DOM để locator click. Để làm gì? Đăng ký handler trước action rồi accept, dismiss hoặc nhập prompt theo đúng kịch bản.
Đăng ký handler trước action:
page.once('dialog', dialog => dialog.accept());
await page.getByRole('button', { name: 'Xóa' }).click();
13. Chạy test, debug và đọc report
Là gì? Đây là vòng phản hồi từ chọn test cần chạy, quan sát quá trình thực thi đến phân tích artifact sau khi fail. Tại sao? Viết được test chưa đủ; đội phải chạy đúng phạm vi và tìm nguyên nhân lỗi nhanh. Để làm gì? Chọn chế độ phù hợp cho phát triển, CI, debug và điều tra regression.
13.1 Lệnh chạy thường dùng
Là gì? Playwright CLI là giao diện dòng lệnh để chọn file, test title, project, worker và chế độ chạy. Tại sao? Chạy toàn bộ suite cho mọi thay đổi thường tốn thời gian và không phù hợp lúc debug. Để làm gì? Thu hẹp đúng phạm vi test, tái hiện điều kiện CI và điều khiển parallelism có chủ đích.
# Tất cả test theo config
npx playwright test
# Một file
npx playwright test tests/login.spec.ts
# Test bắt đầu tại dòng 42
npx playwright test tests/login.spec.ts:42
# Lọc theo tên/tag
npx playwright test --grep "@smoke"
npx playwright test --grep-invert "@slow"
# Một browser project
npx playwright test --project=chromium
# Có giao diện browser
npx playwright test --headed
# Chạy tuần tự để điều tra race condition
npx playwright test --workers=1
# Chỉ liệt kê test, không chạy
npx playwright test --list
# Chạy lại test fail của lần gần nhất
npx playwright test --last-failed
PowerShell cần quote biểu thức có ký tự đặc biệt. Nếu lọc theo tag, nên dùng chuỗi đơn giản như "@smoke".
13.2 UI Mode
Là gì? UI Mode là giao diện tương tác để duyệt, chạy và quan sát test theo từng bước. Tại sao? Feedback trực quan về DOM snapshot, locator và network giúp phát triển test nhanh hơn terminal đơn thuần. Để làm gì? Khám phá test, chạy watch mode và điều tra cục bộ; kết quả cuối vẫn cần được xác nhận ở chế độ giống CI.
npx playwright test --ui
UI Mode hữu ích khi phát triển test:
- lọc và chạy một test;
- watch mode;
- xem từng action;
- DOM snapshot trước/sau bước;
- network, console, source và attachment;
- locator picker.
UI Mode không thay thế chạy headless như CI. Trước khi gửi review, luôn chạy lại bằng command gần với pipeline.
13.3 Debug mode và Inspector
Là gì? Debug mode mở browser cùng Playwright Inspector và cho phép dừng/đi từng action. Tại sao? Một lỗi timing hoặc locator thường cần quan sát trạng thái ngay trước bước fail. Để làm gì? Step over, pick locator, xem call log và kiểm tra trực tiếp trạng thái trang trong lúc test đang dừng.
npx playwright test tests/login.spec.ts --debug
npx playwright test tests/login.spec.ts:15 --debug
Có thể tạm đặt breakpoint trong code hoặc thêm:
await page.pause();
page.pause() yêu cầu headed/Inspector và không được để lại trong test commit chính thức.
13.4 HTML report
Là gì? HTML report là báo cáo tổng hợp kết quả test, thời gian, retry, lỗi và attachment. Tại sao? Log terminal không đủ thuận tiện để so sánh nhiều test hoặc chia sẻ bằng chứng. Để làm gì? Lọc passed/failed/flaky, mở chi tiết từng test và bắt đầu quá trình triage từ một giao diện thống nhất.
npx playwright show-report
Khi đọc report:
- đọc error và call log;
- xác định action/assertion đầu tiên fail;
- xem screenshot để phát hiện overlay, sai trang, sai dữ liệu;
- mở trace để xem DOM/network/console;
- so sánh retry: fail lần đầu nhưng pass lần sau là flaky, không phải “đã ổn”.
13.5 Trace Viewer
Là gì? Trace là bản ghi timeline gồm action, DOM snapshot, network, console, source và attachment; Trace Viewer dùng để đọc bản ghi đó. Tại sao? Screenshot chỉ phản ánh một thời điểm và thường không cho biết chuỗi sự kiện trước lỗi. Để làm gì? Tái dựng gần như toàn bộ quá trình test để phân biệt lỗi locator, dữ liệu, network hay ứng dụng.
Với trace: 'on-first-retry', trace được ghi ở lần retry đầu. Mở file:
npx playwright show-trace test-results/<thu-muc-test>/trace.zip
Trace giúp xem:
- timeline từng action;
- DOM snapshot theo thời điểm;
- locator resolved đến element nào;
- network request/response;
- console log và lỗi trang;
- source và attachment.
Không bật trace/video “on” cho mọi test trên CI lâu dài nếu artifact quá lớn. Chọn chiến lược retain/on-first-retry theo nhu cầu điều tra và chính sách dữ liệu.
13.6 Phân loại lỗi nhanh
Là gì? Phân loại lỗi là bước xác định lỗi thuộc test code, dữ liệu, môi trường, browser hay sản phẩm trước khi sửa. Tại sao? Cùng một triệu chứng timeout có thể có nhiều nguyên nhân hoàn toàn khác nhau. Để làm gì? Chọn bằng chứng và hướng điều tra đúng, tránh tăng timeout hoặc retry một cách cảm tính.
| Triệu chứng | Kiểm tra đầu tiên |
|---|---|
| Timeout click | locator có duy nhất, element có bị overlay/disabled không |
| Expected visible nhưng không thấy | sai trang, dữ liệu, quyền, timing nghiệp vụ |
| Pass khi chạy riêng, fail khi chạy suite | shared data/state, parallelism, cleanup |
| Pass local, fail CI | env/config/browser/font/timezone/quyền mạng |
| Retry mới pass | race condition hoặc môi trường không ổn định |
| Browser không khởi động | binary, antivirus, policy, OS support, cache |
14. Codegen và công cụ trong VS Code
Là gì? Codegen và extension VS Code là công cụ hỗ trợ ghi thao tác, sinh locator và chạy/debug test từ editor. Tại sao? Chúng rút ngắn giai đoạn khám phá UI nhưng không tự hiểu nghiệp vụ hoặc tiêu chuẩn bảo trì. Để làm gì? Tạo bản nháp nhanh, sau đó con người tinh chỉnh thành test độc lập, rõ intent và ổn định.
14.1 Tạo code bằng thao tác thật
Là gì? Codegen quan sát thao tác trên browser và sinh code Playwright tương ứng. Tại sao? Người mới có thể học API và tìm locator nhanh hơn so với viết toàn bộ từ trí nhớ. Để làm gì? Ghi lại một luồng ngắn làm điểm khởi đầu, không dùng output chưa review như test hoàn chỉnh.
npx playwright codegen https://app.example.internal
Codegen mở browser và Playwright Inspector. Tương tác với trang để ghi action; dùng toolbar để thêm assertion và Pick Locator. Có thể ghi trực tiếp ra file:
npx playwright codegen --target=playwright-test --output=tests/generated.spec.ts https://app.example.internal
14.2 Ghi với thiết lập cụ thể
Là gì? Các option Codegen đặt viewport, device, theme, test-id hoặc authentication state cho phiên ghi. Tại sao? Locator và hành vi responsive có thể khác theo điều kiện browser. Để làm gì? Sinh bản nháp trong đúng ngữ cảnh mà test mục tiêu cần mô phỏng.
npx playwright codegen --viewport-size="1440,900" https://app.example.internal
npx playwright codegen --device="iPhone 13" https://app.example.internal
npx playwright codegen --color-scheme=dark https://app.example.internal
npx playwright codegen --test-id-attribute=data-testid https://app.example.internal
Lưu và nạp trạng thái đăng nhập:
npx playwright codegen --save-storage=.auth/codegen.json https://app.example.internal
npx playwright codegen --load-storage=.auth/codegen.json https://app.example.internal
File storage là secret. Không commit hoặc chia sẻ.
14.3 Quy trình dùng Codegen đúng cách
Là gì? Đây là bước chuyển output tự sinh thành test đạt chuẩn của project. Tại sao? Codegen có thể ghi action thừa, dữ liệu cứng và locator chỉ phù hợp tại thời điểm record. Để làm gì? Giữ phần hữu ích, bổ sung assertion nghiệp vụ, refactor abstraction và kiểm tra độ ổn định trước khi review.
Codegen tạo bản nháp, không phải test hoàn chỉnh:
- record luồng ngắn, một mục tiêu;
- copy code vào spec;
- đổi locator phát sinh thành role/label/test id ổn định;
- bỏ action thừa do thao tác khám phá;
- thay dữ liệu cứng bằng data/fixture phù hợp;
- thêm assertion nghiệp vụ rõ ràng;
- gom thao tác lặp lại vào Page Object/component;
- chạy test lặp lại, chạy cả headed lẫn headless;
- review secret, dữ liệu cá nhân và file storage trước commit.
14.4 VS Code Testing panel
Là gì? Testing panel là giao diện của VS Code để khám phá và điều khiển test từ editor. Tại sao? Nó giảm chuyển đổi giữa code, terminal và debugger trong lúc phát triển. Để làm gì? Chạy một test, đặt breakpoint, chọn project, record và xem lỗi inline mà vẫn dùng chung cấu hình Playwright.
Mở biểu tượng Testing ở Activity Bar. Từ đây có thể:
- chạy/debug test hoặc suite;
- bật Show Browser;
- chọn project;
- record test;
- pick locator;
- xem lỗi inline.
Nếu test không xuất hiện, kiểm tra đã mở đúng folder chứa playwright.config.ts, package đã cài và extension đang chọn đúng config.
15. Giảm flaky và quy chuẩn review
Là gì? Flaky test là test cho kết quả không nhất quán khi sản phẩm và đầu vào không đổi; quy chuẩn review là hàng rào ngăn nguyên nhân flaky đi vào suite. Tại sao? Test thiếu tin cậy làm đội bỏ qua cảnh báo thật và tốn thời gian rerun. Để làm gì? Xây dựng suite có tín hiệu rõ, lỗi tái hiện được và chi phí bảo trì có kiểm soát.
15.1 Nguyên nhân flaky phổ biến
Là gì? Nguyên nhân flaky thường là race condition, locator mong manh, shared state hoặc khác biệt môi trường. Tại sao? Retry chỉ che triệu chứng nếu không xác định nguồn bất định. Để làm gì? Dùng danh sách này như checklist điều tra trước khi tăng timeout hoặc chấp nhận rerun.
- chờ cứng bằng timeout;
- locator phụ thuộc DOM/class/vị trí;
- test chia sẻ tài khoản hoặc dữ liệu;
- thứ tự test ngầm định;
- click bằng
forceche overlay; - assertion đọc giá trị một lần thay vì web-first;
- bỏ quên
await; - môi trường, API hoặc job nền không ổn định;
- animation, virtual list, lazy loading;
- timezone/locale khác giữa local và CI.
15.2 Checklist trước khi review
Là gì? Checklist review là tập điều kiện tối thiểu mà test phải đạt trước khi merge. Tại sao? Một lỗi nhỏ như
test.only, thiếuawaithoặc commit storage state có thể ảnh hưởng cả pipeline hoặc bảo mật. Để làm gì? Chuẩn hóa chất lượng review và phát hiện lỗi lặp lại trước khi chúng trở thành technical debt.
- Tên test nói rõ hành vi và kết quả.
- Test chạy độc lập và có dữ liệu riêng.
- Không có
test.only,page.pause()hoặcwaitForTimeout(). - Không commit secret/storage state/download nhạy cảm.
- Locator ưu tiên role/label/test id và trỏ duy nhất.
- Assertion kiểm tra kết quả nghiệp vụ, không chỉ “click không lỗi”.
- Mọi Promise cần thiết có
await. - Event promise được tạo trước action gây event.
- Page Object không che mất intent của test.
- Test pass headless và lặp lại nhiều lần.
- Report/trace không làm lộ dữ liệu cấm.
15.3 Chạy lặp để phát hiện flaky
Là gì?
--repeat-eachchạy cùng test nhiều lần dưới một hoặc nhiều mức parallelism. Tại sao? Một lần pass không chứng minh test ổn định, đặc biệt với lỗi timing và xung đột dữ liệu. Để làm gì? Tăng xác suất tái hiện race condition trước khi merge và so sánh hành vi tuần tự với song song.
npx playwright test tests/login.spec.ts --repeat-each=10 --workers=1
npx playwright test tests/login.spec.ts --repeat-each=10 --workers=4
Chạy tuần tự giúp kiểm tra timing cơ bản; chạy song song giúp phát hiện xung đột dữ liệu. Không dùng retry để biến flaky thành pass im lặng. Retry là cơ chế thu thập thêm bằng chứng và giảm nhiễu tạm thời, không phải định nghĩa chất lượng.
15.4 Assertion có giá trị
Là gì? Assertion có giá trị là oracle kiểm tra trực tiếp kết quả nghiệp vụ mà test tuyên bố bảo vệ. Tại sao? Một chuỗi click chạy hết không đồng nghĩa hệ thống xử lý đúng. Để làm gì? Làm cho test fail đúng lúc dữ liệu, trạng thái, quyền hoặc thông báo quan trọng không đạt yêu cầu.
Một test chỉ click qua nhiều màn hình nhưng không assert kết quả cuối không bảo vệ nghiệp vụ. Mỗi test nên có ít nhất một oracle rõ:
- trạng thái hoặc dữ liệu đúng trên UI;
- URL/route đúng nếu có ý nghĩa;
- bản ghi xuất hiện/biến mất;
- quyền điều khiển đúng;
- thông báo lỗi đúng và dữ liệu không bị thay đổi.
16. Cheatsheet
Là gì? Cheatsheet là bảng tra cứu cô đọng các lệnh và API đã được giải thích ở những chương trước. Tại sao? Khi làm việc hằng ngày, người viết test cần nhớ cách dùng nhanh hơn là đọc lại toàn bộ lý thuyết. Để làm gì? Tìm đúng cú pháp trong vài giây; nếu chưa hiểu nguyên lý, quay lại chương tương ứng thay vì copy lệnh một cách máy móc.
16.1 Khởi tạo và browser
Là gì? Nhóm lệnh này quản lý project, package Playwright và browser binary. Tại sao? Package JavaScript và browser revision là hai thành phần riêng nhưng phải tương thích. Để làm gì? Khởi tạo môi trường, xác minh version và chuẩn bị browser trước khi chạy test.
| Mục đích | Lệnh |
|---|---|
| Tạo project | npm init playwright@latest |
| Cài package | npm ci |
| Cài Chromium | npx playwright install chromium |
| Xem version | npx playwright --version |
| Liệt kê browser | npx playwright install --list |
16.2 Chạy và debug
Là gì? Đây là các biến thể CLI để chạy, lọc, quan sát và điều tra test. Tại sao? Mỗi giai đoạn cần phạm vi khác nhau: phát triển một test, chạy smoke hoặc chạy toàn suite. Để làm gì? Chọn lệnh ngắn nhất phù hợp với mục tiêu hiện tại.
| Mục đích | Lệnh |
|---|---|
| Chạy tất cả | npx playwright test |
| Một file | npx playwright test tests/a.spec.ts |
| Theo dòng | npx playwright test tests/a.spec.ts:20 |
| Theo tag/tên | npx playwright test -g "@smoke" |
| Một project | npx playwright test --project=chromium |
| Có browser | npx playwright test --headed |
| UI Mode | npx playwright test --ui |
| Inspector | npx playwright test --debug |
| Một worker | npx playwright test --workers=1 |
| Report | npx playwright show-report |
| Trace | npx playwright show-trace trace.zip |
16.3 Locator
Là gì? Nhóm API locator mô tả cách tìm element và thu hẹp phạm vi tìm kiếm. Tại sao? Locator là điểm nối giữa test và UI nên ảnh hưởng trực tiếp đến độ ổn định. Để làm gì? Tra nhanh cú pháp role, label, text, test id và filter nhưng vẫn tuân theo thứ tự ưu tiên ở chương 7.
page.getByRole('button', { name: 'Lưu' });
page.getByLabel('Email');
page.getByPlaceholder('Tìm kiếm');
page.getByText('Hoàn tất', { exact: true });
page.getByAltText('Ảnh đại diện');
page.getByTitle('Đóng');
page.getByTestId('submit-order');
page.locator('.legacy-selector');
page.getByRole('row').filter({ hasText: 'CUS-001' });
locator.getByRole('button', { name: 'Xóa' });
locator.nth(0); // chỉ khi thứ tự là một phần của yêu cầu
16.4 Action
Là gì? Action là các thao tác Playwright thực hiện lên page hoặc locator. Tại sao? Mỗi loại control có API phù hợp và các action này được tích hợp auto-wait. Để làm gì? Chọn đúng thao tác thay cho mô phỏng chuột/bàn phím bằng tọa độ hoặc script tùy ý.
await page.goto('/path');
await locator.click();
await locator.dblclick();
await locator.fill('text');
await locator.clear();
await locator.press('Enter');
await locator.check();
await locator.uncheck();
await locator.selectOption('value');
await locator.hover();
await locator.dragTo(target);
await locator.setInputFiles('file.pdf');
await locator.scrollIntoViewIfNeeded();
16.5 Assertion
Là gì? Assertion là các matcher diễn đạt trạng thái mong đợi của page, locator hoặc response. Tại sao? Chúng là tiêu chí quyết định test pass hay fail. Để làm gì? Tra nhanh matcher web-first phù hợp với kết quả cần bảo vệ.
await expect(locator).toBeVisible();
await expect(locator).toBeHidden();
await expect(locator).toBeEnabled();
await expect(locator).toBeDisabled();
await expect(locator).toBeChecked();
await expect(locator).toHaveText('Exact');
await expect(locator).toContainText('Partial');
await expect(locator).toHaveValue('value');
await expect(locator).toHaveCount(3);
await expect(locator).toHaveAttribute('href', /orders/);
await expect(page).toHaveURL(/dashboard/);
await expect(page).toHaveTitle(/Dashboard/);
await expect(response).toBeOK();
16.6 Event pattern
Là gì? Event pattern là trình tự tạo promise chờ event trước, kích hoạt action sau, rồi await kết quả. Tại sao? Download, response và popup có thể xuất hiện ngay khi click nên listener đăng ký muộn sẽ bỏ lỡ sự kiện. Để làm gì? Đồng bộ đúng với sự kiện browser mà không tạo race condition.
const downloadPromise = page.waitForEvent('download');
await trigger.click();
const download = await downloadPromise;
const responsePromise = page.waitForResponse('**/api/orders');
await submit.click();
const response = await responsePromise;
const popupPromise = page.waitForEvent('popup');
await openPopup.click();
const popup = await popupPromise;
Quy tắc nhớ: promise trước, action sau, await kết quả cuối.
17. Bài thực hành và lộ trình đào tạo
Là gì? Đây là chuỗi bài tập biến kiến thức từng chương thành kỹ năng thao tác và điều tra thực tế. Tại sao? Đọc cú pháp không đủ để hình thành tư duy chọn locator, assertion và bằng chứng lỗi. Để làm gì? Đánh giá người học qua sản phẩm chạy được, giải thích được và có thể review được.
17.1 Lab 1 — cài đặt và smoke
Là gì? Lab smoke xác nhận toolchain và browser có thể chạy một kịch bản tối thiểu. Tại sao? Nếu nền tảng chưa ổn, mọi lỗi ở lab sau đều khó phân biệt với lỗi cài đặt. Để làm gì? Tạo baseline chung và ghi nhận version đã được kiểm chứng trên môi trường đào tạo.
Mục tiêu: mọi học viên chạy được test đầu tiên.
- kiểm tra Node/npm/Nexus;
- khởi tạo TypeScript project;
- cài Chromium;
- chạy example test headless;
- chạy headed và mở HTML report;
- ghi lại version Playwright/browser đã kiểm chứng trên Windows 10.
Tiêu chí hoàn thành: cùng một test pass ba lần liên tiếp và người học chỉ ra được report nằm ở đâu.
17.2 Lab 2 — locator và assertion
Là gì? Lab này luyện cách tìm element và xác nhận kết quả trên một form thực tế. Tại sao? Đây là hai kỹ năng quyết định test có vừa đúng nghiệp vụ vừa bền vững hay không. Để làm gì? Biến một acceptance criterion thành test có locator ngữ nghĩa và oracle rõ ràng.
Chọn một form nội bộ an toàn:
- nhập dữ liệu hợp lệ;
- kiểm tra validation của một field;
- dùng ít nhất ba locator ngữ nghĩa;
- không dùng XPath;
- có assertion cho thành công và thất bại.
17.3 Lab 3 — Page Object và dữ liệu
Là gì? Lab này refactor test đơn lẻ thành cấu trúc có abstraction, config và dữ liệu bên ngoài. Tại sao? Code chạy được chưa chắc đã có thể mở rộng thành suite. Để làm gì? Thực hành phân tách trách nhiệm và quản lý credential an toàn.
- tách trang login thành Page Object;
- tạo hai test data-driven;
- đưa base URL vào config;
- đưa credential vào env;
- bảo đảm không commit secret.
17.4 Lab 4 — debug flaky
Là gì? Lab flaky cung cấp một test không ổn định để người học tái hiện và sửa nguyên nhân. Tại sao? Kỹ năng điều tra quan trọng hơn việc chỉ rerun đến khi pass. Để làm gì? Luyện đọc trace, nhận diện race condition và chứng minh bản sửa ổn định bằng chạy lặp.
Giảng viên cung cấp test có waitForTimeout, locator theo nth-child và shared data. Học viên:
- tái hiện bằng
--repeat-each; - dùng UI Mode/trace tìm nguyên nhân;
- thay bằng web-first assertion và locator bền vững;
- chạy tuần tự rồi song song;
- giải thích vì sao bản sửa ổn định hơn.
17.5 Lộ trình gợi ý
Là gì? Lộ trình chia nội dung thành các buổi có đầu ra kiểm chứng được. Tại sao? Học quá nhiều API cùng lúc dễ tạo thói quen copy mà không hiểu. Để làm gì? Đi từ setup đến thiết kế suite theo mức độ phức tạp tăng dần và có tiêu chí hoàn thành cho từng giai đoạn.
| Buổi | Nội dung | Sản phẩm đầu ra |
|---|---|---|
| 1 | Setup, config, test anatomy | Smoke test chạy được |
| 2 | Locator, action, assertion, auto-wait | 3 test độc lập |
| 3 | Page Object, fixture, auth, data | Mini suite có cấu trúc |
| 4 | Network, file, popup, iframe | Test tình huống nâng cao |
| 5 | UI Mode, trace, flaky, review | Báo cáo điều tra lỗi |
| 6 | Chuẩn hóa suite và pipeline | Bộ test sẵn sàng tích hợp CI |
18. Xử lý sự cố
Là gì? Troubleshooting là quy trình thu thập bằng chứng, khoanh vùng lớp lỗi và thử biện pháp an toàn theo thứ tự. Tại sao? Cùng một thông báo có thể đến từ Nexus, browser binary, OS policy, test code hoặc ứng dụng. Để làm gì? Tìm nguyên nhân gốc mà không tự ý hạ bảo mật, xóa dữ liệu hoặc che lỗi bằng timeout/retry.
18.1 npm install không truy cập được package
Là gì? Đây là lỗi ở luồng npm lấy package metadata hoặc tarball từ registry. Tại sao? Registry, authentication, certificate hoặc cache nội bộ có thể không hợp lệ. Để làm gì? Xác minh từng điểm kết nối và cung cấp bằng chứng cho đội Nexus/IT mà không làm lộ token.
npm config get userconfig
npm config get registry
npm ping
npm view @playwright/test version
Nếu sai registry, làm theo hướng dẫn Nexus nội bộ; không tự ghi đè .npmrc khi chưa hiểu certificate/auth. Không dán token vào terminal được ghi hình hoặc ticket.
18.2 Cài package được nhưng không tải được browser
Là gì? Package Playwright đã có nhưng browser binary mà nó điều khiển chưa được tải hoặc không truy cập được. Tại sao? npm registry và kho browser là hai kênh phân phối khác nhau. Để làm gì? Phân biệt lỗi package với lỗi mirror/proxy browser và làm việc đúng với đội hạ tầng.
npx playwright install --list
npx playwright install chromium
Xác nhận proxy/firewall hoặc mirror browser với đội hạ tầng. Nexus npm và browser download là hai luồng khác nhau. Nếu cache browser được phân phối sẵn, cần đồng bộ đúng revision mà version Playwright yêu cầu.
18.3 Executable doesn't exist
Là gì? Playwright không tìm thấy executable đúng revision mà package hiện tại yêu cầu. Tại sao? Browser chưa được cài, cache bị xóa hoặc package vừa nâng version. Để làm gì? Đối chiếu version và cài đúng browser revision thay vì trỏ tùy tiện sang executable khác.
Thường package đã cài nhưng browser revision chưa có:
npx playwright install chromium
Nếu vừa nâng @playwright/test, cài lại browser tương ứng. Kiểm tra lệnh đang chạy trong đúng project và npx playwright --version đúng kỳ vọng.
18.4 Browser bị đóng hoặc không launch trên Windows 10
Là gì? Browser process crash, bị policy chặn hoặc không tương thích trước khi test bắt đầu. Tại sao? Windows 10 nằm ngoài phạm vi hỗ trợ chính thức của các bản Playwright mới nên rủi ro môi trường phải được xem xét riêng. Để làm gì? Thu bằng chứng từ error, policy và máy đối chứng để quyết định pin version hay nâng OS.
- xem error đầy đủ và Event Viewer nếu có crash;
- kiểm tra antivirus/application control có chặn binary trong cache;
- thử
--headedvà--workers=1để thu hẹp; - xác minh cùng version trên một máy Windows được hỗ trợ;
- không bỏ qua khả năng incompatibility vì Windows 10 ngoài danh sách hỗ trợ mới nhất;
- chốt version đã kiểm chứng hoặc lập kế hoạch nâng OS.
18.5 Test không hiện trong VS Code
Là gì? Extension không discover được test từ config và workspace hiện tại. Tại sao? Mở sai root, dependency chưa có hoặc file không khớp quy tắc tìm test. Để làm gì? Phân biệt lỗi extension với lỗi cấu trúc project bằng
--listvà kiểm tra config.
- mở đúng root folder chứa
playwright.config.ts; - chạy
npm ci; - kiểm tra file khớp
testMatch/đuôi.spec.ts; - mở Testing panel và chọn đúng config/project;
- chạy
npx playwright test --listđể phân biệt lỗi project với lỗi extension.
18.6 Timeout nhưng nhìn bằng mắt thấy element
Là gì? Element có vẻ xuất hiện nhưng không thỏa locator hoặc actionability condition mà action yêu cầu. Tại sao? Nó có thể nằm trong iframe, bị che, disabled, trùng locator hoặc thuộc sai page/account. Để làm gì? Dùng call log và trace để xác định điều kiện nào chưa đạt thay vì chỉ tăng timeout.
Kiểm tra call log/trace:
- locator có khớp hai element, một element ẩn không;
- iframe chưa được dùng
frameLocator; - overlay/toast/loading che element;
- element disabled vì request chưa xong;
- click dẫn tới tab khác;
- accessible name khác với text nhìn thấy;
- test đang ở sai account/role/environment.
Không tăng timeout trước khi biết nguyên nhân.
18.7 Lỗi certificate nội bộ
Là gì? Browser không tin cậy certificate do môi trường nội bộ cung cấp. Tại sao? CA chưa được cài đúng hoặc certificate của môi trường có vấn đề. Để làm gì? Ưu tiên sửa trust chain; chỉ dùng
ignoreHTTPSErrorscho môi trường test khi chính sách cho phép và rủi ro đã được chấp nhận.
Chỉ khi môi trường test dùng certificate nội bộ và chính sách cho phép, có thể cấu hình:
use: {
ignoreHTTPSErrors: true,
}
Đây là ngoại lệ cho test environment, không phải cách sửa certificate production. Ưu tiên cài CA đúng theo hướng dẫn tổ chức.
19. Duy trì handbook HTML và thêm ảnh
Là gì? Handbook được quản lý như source: Markdown là nội dung gốc, CSS là theme, PowerShell dựng một HTML độc lập. Tại sao? Sửa trực tiếp file xuất bản sẽ làm nội dung khó review và không tái tạo được. Để làm gì? Cập nhật kiến thức, ảnh và giao diện theo quy trình lặp lại mà không cần cài dependency hoặc truy cập mạng.
19.1 Cấu trúc bộ handbook
Là gì? Đây là cấu trúc thư mục của chính tài liệu đào tạo, tách nguồn, asset, style, script và output. Tại sao? Người bảo trì cần biết file nào được sửa và file nào được sinh tự động. Để làm gì? Tránh chỉnh nhầm
dist, giữ ảnh có tổ chức và giúp build chạy từ một entry point rõ ràng.
playwright-handbook/
├─ handbook.md # nội dung nguồn
├─ assets/images/playwright/ # ảnh chụp màn hình
├─ styles/handbook.css # theme HTML và xử lý ảnh
├─ scripts/build-html.ps1 # chuyển MD → HTML offline
├─ dist/ # file HTML sinh ra
└─ README.md
19.2 Thêm ảnh chụp màn hình
Là gì? Ảnh được tham chiếu bằng đường dẫn Markdown, sau đó script resolve và nhúng base64 vào HTML. Tại sao? Đường dẫn tương đối dễ vỡ khi di chuyển file, còn ảnh nhúng giúp HTML tự chứa. Để làm gì? Bổ sung hướng dẫn trực quan mà vẫn giữ ảnh căn giữa, đúng tỷ lệ và không phụ thuộc file ngoài sau khi build.
- Che hoặc crop username, email, URL nhạy cảm, token, dữ liệu khách hàng.
- Lưu ảnh vào
assets/images/playwright/, ưu tiên PNG/WebP. - Dùng tên không dấu và dấu gạch ngang.
- Chèn vào Markdown:

Script resolve đường dẫn tương đối dựa trên vị trí của handbook.md, đọc ảnh và nhúng base64 vào HTML. File HTML tạo ra có thể di chuyển độc lập. CSS:
- căn giữa ảnh và caption;
- giữ đúng tỷ lệ;
- giới hạn chiều rộng theo vùng hiển thị;
- giới hạn chiều cao để ảnh dễ quan sát;
- giữ quy tắc in nếu người đọc chủ động in từ trình duyệt.
Nếu ảnh không tồn tại hoặc định dạng không hỗ trợ, build dừng với đường dẫn đã resolve để dễ sửa. Hỗ trợ PNG, JPEG, GIF, WebP và SVG.
19.3 Dựng lại HTML
Là gì? Build đọc Markdown, chuyển block/inline syntax, nhúng CSS và ảnh rồi ghi ra một file HTML. Tại sao? Output cần được tạo lại mỗi khi nội dung hoặc asset thay đổi. Để làm gì? Phát hành handbook có thể mở offline trên browser mà không chạy Node/npm hay tải tài nguyên mạng.
.\scripts\build-html.ps1
Kết quả:
dist/playwright-ui-automation-handbook.html
Build file khác:
.\scripts\build-html.ps1 -InputFile handbook.md -OutputHtml dist\handbook-v2.html
19.4 Checklist trước khi phát hành handbook
Là gì? Đây là cổng kiểm tra nội dung, bảo mật và hiển thị trước khi chia sẻ HTML. Tại sao? Screenshot, code mẫu hoặc metadata có thể vô tình chứa secret và thay đổi layout có thể làm tài liệu khó đọc. Để làm gì? Bảo đảm bản phát hành chính xác, tự chứa, không lộ dữ liệu và hiển thị tốt ở desktop/mobile.
- Lệnh cài đặt phù hợp Nexus hiện tại.
- Version Node/Playwright và ngày cập nhật được ghi rõ.
- Mọi code block đã được chạy hoặc review.
- Link tài liệu còn hoạt động.
- Ảnh rõ khi zoom 100% và không tràn khỏi vùng nội dung.
- Không có secret/PII/URL nhạy cảm trong chữ, ảnh hoặc metadata.
- Mở HTML kiểm tra mục lục, bảng, code và responsive layout.
- Commit Markdown, CSS và script; có thể không commit
dist/tùy quy trình phát hành.
20. Tài liệu tham khảo
Là gì? Đây là danh sách nguồn chính thức dùng để kiểm chứng API và hành vi Playwright. Tại sao? Framework thay đổi theo phiên bản nên handbook có thể dần lỗi thời. Để làm gì? Tra cứu chi tiết, xác nhận thay đổi trước khi cập nhật ví dụ và tránh dựa vào blog hoặc snippet không rõ phiên bản.
Tài liệu chính thức, nên dùng để kiểm tra thay đổi theo phiên bản:
- Installation
- Locators
- Auto-waiting và actionability
- Best Practices
- Running and debugging tests
- Command line
- Test generator / Codegen
- Trace Viewer
- Authentication
- Page Object Model
- Playwright Test API
Nguyên tắc cuối: test UI phải mô tả điều người dùng làm, xác nhận điều người dùng nhận được, và để lại đủ bằng chứng khi kết quả không đúng.