PLAYWRIGHT UI AUTOMATION

Handbook đào tạo từ cài đặt đến xây dựng bộ kiểm thử UI có thể bảo trì

Môi trường mục tiêu
Windows 10 · Node.js 22 · npm/Nexus nội bộ · Visual Studio Code

Phiên bản 1.0 · Cập nhật 05/08/2026
Tác giả: duyvt2 - Duy đẹp trai

Mục lục

  1. Mục tiêu và cách học
  2. Chuẩn bị môi trường
  3. Cài đặt Playwright
  4. Khởi tạo và triển khai project
  5. Cấu hình nền tảng
  6. Viết test đầu tiên
  7. Locator: tìm phần tử bền vững
  8. Action, assertion và các hàm thường dùng
  9. Auto-wait và đồng bộ
  10. Tổ chức code cho dự án thật
  11. Dữ liệu, đăng nhập, upload và download
  12. Network, popup, iframe và nhiều tab
  13. Chạy test, debug và đọc report
  14. Codegen và công cụ trong VS Code
  15. Giảm flaky và quy chuẩn review
  16. Cheatsheet
  17. Bài thực hành và lộ trình đào tạo
  18. Xử lý sự cố
  19. Duy trì handbook HTML và thêm ảnh
  20. 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ể:

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õ:

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:

  1. Độc lập: không phụ thuộc test chạy trước.
  2. Có thể lặp lại: cùng đầu vào cho cùng kết quả trong cùng điều kiện.
  3. Quan sát được: lỗi có trace, screenshot, log hoặc report đủ để điều tra.
  4. 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ầnMục tiêuCách kiểm tra
WindowsWindows 10 theo môi trường nội bộwinver
Node.jsMajor version 22node --version
npmĐi kèm Node.jsnpm --version
Nexusnpm đọc user config nội bộnpm config get userconfig
RegistryURL Nexus của tổ chứcnpm config get registry
VS CodeBản được tổ chức phê duyệtmenu Help → About
GitKhuyến nghị càigit --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

Nhập username
Nhập username

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

Nhập password
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

Login thành công
Login 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ợ:

Extension VSCode Playwright
Extension VSCode phục vụ Playwright

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:

Nếu powershell hỏi

Need to install the following packages:
create-playwright@1.17.139
Ok to proceed? (y)

y rồi Enter

Cài đặt thành công
Cài đặt Playwright (chưa có trình duyệt) thành công

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

Sửa biến môi trường
Gõ trong cửa sổ Start "ENV", chọn Edit the system environment variables

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

Click Env
Click chọn Environment Variables...
New Env
Click chọn New
Add Env
Thực hiện thêm biến môi trường

**Đ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

Confirm Env
Xác nhận thay đổi biến môi trường

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à executablePathchannel

  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

Test Ran
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

HTML Report
Báo cáo HTML mặc định của Playwright

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

Headed Mode
Kết quả chạy test --headed

Tiêu chí đạt:

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:

4.2 package.json mẫu

Là gì? package.json là manifest của project Node.js, khai báo metadata, dependency và các lệnh chuẩn trong scripts. 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ì? .gitignore là 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.ts là 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ìnhPhạm viGợi ý
timeoutToàn bộ một test30 giây là điểm bắt đầu
expect.timeoutWeb-first assertion5 giây
actionTimeoutMỗi action10 giây hoặc 0 để dùng timeout chung
navigationTimeoutĐiều hướngTheo 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:

6.2 Nhóm test và hook

Là gì? test.describe nhóm các test cùng chủ đề; hook như beforeEach chạ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:

  1. getByRole() với accessible name;
  2. getByLabel() cho form control;
  3. getByPlaceholder(), getByText(), getByAltText(), getByTitle();
  4. getByTestId() cho điểm neo ổn định do đội phát triển cung cấp;
  5. CSS locator khi không có lựa chọn ngữ nghĩa;
  6. 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ặc data-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 Locator hoặc Page và 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:

Nếu chưa đạt, Playwright chờ đến timeout. Assertions như toBeVisible()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ì? networkidle mô 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ặc createOrder() để 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:

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ằng setInputFiles. 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 page hiệ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 Page khá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 đúng Page mớ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ừ page không tự xuyên qua ranh giới frame. Để làm gì? Dùng frameLocator để 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, prompt hoặc beforeunload do 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:

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:

  1. đọc error và call log;
  2. xác định action/assertion đầu tiên fail;
  3. xem screenshot để phát hiện overlay, sai trang, sai dữ liệu;
  4. mở trace để xem DOM/network/console;
  5. 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:

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ứngKiểm tra đầu tiên
Timeout clicklocator có duy nhất, element có bị overlay/disabled không
Expected visible nhưng không thấysai trang, dữ liệu, quyền, timing nghiệp vụ
Pass khi chạy riêng, fail khi chạy suiteshared data/state, parallelism, cleanup
Pass local, fail CIenv/config/browser/font/timezone/quyền mạng
Retry mới passrace condition hoặc môi trường không ổn định
Browser không khởi độngbinary, 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:

  1. record luồng ngắn, một mục tiêu;
  2. copy code vào spec;
  3. đổi locator phát sinh thành role/label/test id ổn định;
  4. bỏ action thừa do thao tác khám phá;
  5. thay dữ liệu cứng bằng data/fixture phù hợp;
  6. thêm assertion nghiệp vụ rõ ràng;
  7. gom thao tác lặp lại vào Page Object/component;
  8. chạy test lặp lại, chạy cả headed lẫn headless;
  9. 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ể:

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.

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ếu await hoặ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.

15.3 Chạy lặp để phát hiện flaky

Là gì? --repeat-each chạ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õ:

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 đíchLệnh
Tạo projectnpm init playwright@latest
Cài packagenpm ci
Cài Chromiumnpx playwright install chromium
Xem versionnpx playwright --version
Liệt kê browsernpx 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 đíchLệnh
Chạy tất cảnpx playwright test
Một filenpx playwright test tests/a.spec.ts
Theo dòngnpx playwright test tests/a.spec.ts:20
Theo tag/tênnpx playwright test -g "@smoke"
Một projectnpx playwright test --project=chromium
Có browsernpx playwright test --headed
UI Modenpx playwright test --ui
Inspectornpx playwright test --debug
Một workernpx playwright test --workers=1
Reportnpx playwright show-report
Tracenpx 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.

  1. kiểm tra Node/npm/Nexus;
  2. khởi tạo TypeScript project;
  3. cài Chromium;
  4. chạy example test headless;
  5. chạy headed và mở HTML report;
  6. 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:

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.

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:

  1. tái hiện bằng --repeat-each;
  2. dùng UI Mode/trace tìm nguyên nhân;
  3. thay bằng web-first assertion và locator bền vững;
  4. chạy tuần tự rồi song song;
  5. 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ổiNội dungSản phẩm đầu ra
1Setup, config, test anatomySmoke test chạy được
2Locator, action, assertion, auto-wait3 test độc lập
3Page Object, fixture, auth, dataMini suite có cấu trúc
4Network, file, popup, iframeTest tình huống nâng cao
5UI Mode, trace, flaky, reviewBáo cáo điều tra lỗi
6Chuẩn hóa suite và pipelineBộ 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.

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 --list và kiểm tra config.

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:

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 ignoreHTTPSErrors cho 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.

Quy trình dựng handbook offline
Markdown và ảnh cục bộ được PowerShell chuyển thành HTML offline
  1. Che hoặc crop username, email, URL nhạy cảm, token, dữ liệu khách hàng.
  2. Lưu ảnh vào assets/images/playwright/, ưu tiên PNG/WebP.
  3. Dùng tên không dấu và dấu gạch ngang.
  4. Chèn vào Markdown:
![Testing panel trong VS Code](assets/images/playwright/vscode-testing-panel.png "Chạy test từ Testing panel")

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:

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.

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:


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.