Mục lục
- Mục Tiêu Bài Học
- 3 Dimension Của Một Project
- Browser Dimension
- Device Dimension
- Environment Dimension
- Khai Báo Matrix Tay
- Pattern Programmatic Matrix Với flatMap
- Naming Convention
- Filter Subset Bằng --project
- Use Case Thực Tế
- Trade-off Khi Scale Matrix
- Best Practice Scale CI
- Conditional Matrix — Nightly vs PR
- Pitfall Thường Gặp
- Quiz
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau khi hoàn thành bài này, bạn sẽ:
- Hiểu project trong Playwright là tổ hợp 3 dimension: browser, device, environment.
- Khai báo matrix bằng cú pháp tay lẫn pattern programmatic
flatMap. - Đặt tên project theo convention
<browser>-<device>-<env>để filter wildcard hoạt động đúng. - Chạy subset matrix bằng
--projectvới wildcard pattern. - Thiết lập conditional matrix (smoke trên PR, full matrix trên nightly).
- Tránh 4 pitfall làm CI cost tăng hoặc filter không như mong đợi.
3 Dimension Của Một Project
Trong playwright.config.ts, mỗi entry trong mảng projects đặc tả một môi trường chạy test cụ thể. Môi trường đó bao gồm 3 chiều độc lập:
- Browser engine — engine nào render trang: Chromium, Firefox hay WebKit. Cùng một URL, cùng một test, các engine có thể render khác nhau, xử lý CSS/JS khác nhau, hoặc behave khác nhau ở các edge case.
- Device — viewport, touch capability, user-agent string và pixel density.
Desktop ChromevàiPhone 15 Procó thể dùng chung engine Chromium nhưng viewport và UA hoàn toàn khác nhau. - Environment — nơi app đang chạy:
staging,prod, hay từng tenant riêng. Thay đổi ở dimension này chủ yếu làbaseURL,extraHTTPHeaders,httpCredentials,storageState.
Gọi là "matrix" vì N browsers × M devices × K envs = N×M×K project phân biệt. Mỗi project là một điểm trong không gian 3 chiều đó. Playwright chạy mỗi project độc lập — cùng file test nhưng với ngữ cảnh khác nhau.
// Ví dụ không gian 2×2×2 = 8 project
Browsers: [Chromium, Firefox ]
Devices: [Desktop, Mobile ]
Environments: [staging, prod ]
→ chrome-desktop-staging
→ chrome-desktop-prod
→ chrome-mobile-staging
→ chrome-mobile-prod
→ firefox-desktop-staging
→ firefox-desktop-prod
→ firefox-mobile-staging
→ firefox-mobile-prod
Browser Dimension
Playwright cung cấp 3 engine nội bộ và 2 channel branded:
Engine nội bộ
- Chromium — engine mặc định, nhanh nhất, ổn định nhất. Playwright team maintain fork riêng của Chromium nên có các patch thêm về automation.
- Firefox — Gecko engine. Cần test khi app hỗ trợ Firefox (thường yêu cầu với enterprise hoặc regulated industry).
- WebKit — engine của Safari. Không cần macOS để chạy — Playwright bundle WebKit cross-platform. Quan trọng cho iOS Safari testing vì iOS không cho phép third-party engine.
Channel branded
- chrome — Google Chrome stable channel. Dùng khi cần test với Chrome thực (không phải Chromium fork). Yêu cầu Chrome đã cài trên máy.
- msedge — Microsoft Edge stable. Dùng cho enterprise dùng Edge làm browser chuẩn.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
// Engine nội bộ — cài qua npx playwright install
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// Channel branded — yêu cầu browser đã cài trên máy
{
name: 'chrome',
use: { channel: 'chrome' },
},
{
name: 'msedge',
use: { channel: 'msedge' },
},
],
});
Chromium và Chrome không giống nhau. Chromium là open-source build; Chrome là Google's build thêm proprietary codecs và DRM. Với hầu hết test E2E, Chromium đủ dùng. Chỉ cần Channel Chrome khi test DRM content, payment flow cụ thể, hoặc extension Chrome.
Device Dimension
devices export từ @playwright/test là một map chứa hàng chục preset device phổ biến. Mỗi preset là một object gồm viewport, userAgent, deviceScaleFactor, isMobile và hasTouch.
Các device hay dùng
import { devices } from '@playwright/test';
// Desktop — viewport 1280×720, no touch, desktop UA
devices['Desktop Chrome']
// { viewport: { width: 1280, height: 720 }, userAgent: 'Chrome/...', isMobile: false, hasTouch: false }
devices['Desktop Firefox']
devices['Desktop Safari']
// Mobile — viewport nhỏ, touch enabled, mobile UA
devices['iPhone 15 Pro']
// { viewport: { width: 393, height: 852 }, deviceScaleFactor: 3, isMobile: true, hasTouch: true }
devices['iPhone 15 Pro Max']
devices['Pixel 7']
devices['Galaxy S9+']
// Tablet
devices['iPad Pro 11']
// { viewport: { width: 834, height: 1194 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true }
devices['iPad Mini']
Custom viewport
Khi preset không khớp yêu cầu (ví dụ app target màn hình 1920×1080 full HD hoặc 2560×1440 QHD), khai báo trực tiếp:
{
name: 'chrome-fullhd-staging',
use: {
browserName: 'chromium',
viewport: { width: 1920, height: 1080 },
baseURL: 'https://staging.app.com',
},
},
Lưu ý: isMobile: true trong device preset không chỉ thay đổi viewport — nó còn set hasTouch: true và UA mobile string. Điều này ảnh hưởng đến cách app render responsive layout, hover behavior và touch event. Một số app check UA để serve mobile-specific HTML.
Environment Dimension
Dimension environment không liên quan đến rendering mà liên quan đến nơi app đang chạy. Các field thay đổi theo environment:
| Field | Ý nghĩa | Ví dụ |
|---|---|---|
baseURL |
Gốc URL — page.goto('/') resolve về đây |
https://staging.app.com |
extraHTTPHeaders |
Header gắn vào mọi request | { 'X-Tenant-ID': 'tenant-a' } |
httpCredentials |
HTTP Basic Auth khi app require | { username: 'qa', password: 'secret' } |
storageState |
Cookies + localStorage pre-loaded | 'playwright/.auth/admin.json' |
Ngoài ra, test code có thể đọc process.env để lấy DB endpoint, API key hoặc feature flag khác nhau giữa staging và prod. Convention thường gặp là set env var trong CI workflow riêng cho từng environment, rồi project chỉ truyền baseURL:
const baseURLs = {
staging: process.env.STAGING_URL ?? 'https://staging.app.com',
prod: process.env.PROD_URL ?? 'https://app.com',
} as const;
Khai Báo Matrix Tay
Cú pháp tay là viết từng project ra tường minh. Phù hợp khi matrix nhỏ (dưới 6-8 project) hoặc khi mỗi project có customization riêng biệt không theo pattern chung:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'chrome-desktop-staging',
use: {
...devices['Desktop Chrome'],
baseURL: 'https://staging.app.com',
},
},
{
name: 'chrome-mobile-staging',
use: {
...devices['iPhone 15 Pro'],
baseURL: 'https://staging.app.com',
},
},
{
name: 'firefox-desktop-staging',
use: {
...devices['Desktop Firefox'],
baseURL: 'https://staging.app.com',
},
},
{
name: 'webkit-desktop-staging',
use: {
...devices['Desktop Safari'],
baseURL: 'https://staging.app.com',
},
},
{
name: 'chrome-desktop-prod',
use: {
...devices['Desktop Chrome'],
baseURL: 'https://app.com',
// prod không dùng httpCredentials vì app public
},
},
{
name: 'chrome-mobile-prod',
use: {
...devices['iPhone 15 Pro'],
baseURL: 'https://app.com',
},
},
],
});
Ưu điểm của cú pháp tay: rõ ràng, dễ đọc, dễ thêm exception cho 1 project cụ thể. Nhược điểm: repetitive khi matrix lớn, dễ sai sót khi thêm browser hoặc env mới (phải copy-paste nhiều entry).
Pattern Programmatic Matrix Với flatMap
Khi matrix bắt đầu lớn (từ 6 project trở lên), generate programmatic bằng flatMap giúp tránh lặp và đảm bảo consistency:
import { defineConfig, devices } from '@playwright/test';
const browsers = ['Desktop Chrome', 'Desktop Firefox', 'Desktop Safari'] as const;
const envs = ['staging', 'prod'] as const;
const baseURLs: Record<typeof envs[number], string> = {
staging: 'https://staging.app.com',
prod: 'https://app.com',
};
export default defineConfig({
projects: browsers.flatMap(browser =>
envs.map(env => ({
name: `${browser.toLowerCase().replace(/ /g, '-')}-${env}`,
use: {
...devices[browser],
baseURL: baseURLs[env],
},
}))
),
});
Output của đoạn trên là 3 browsers × 2 envs = 6 project:
desktop-chrome-staging
desktop-chrome-prod
desktop-firefox-staging
desktop-firefox-prod
desktop-safari-staging
desktop-safari-prod
Matrix 3 chiều đầy đủ
Thêm device dimension bằng cách nest thêm một flatMap:
const browsers = ['chromium', 'firefox', 'webkit'] as const;
const devicePresets = {
desktop: 'Desktop Chrome',
mobile: 'iPhone 15 Pro',
tablet: 'iPad Pro 11',
} as const;
const envs = ['staging', 'prod'] as const;
const baseURLs = {
staging: 'https://staging.app.com',
prod: 'https://app.com',
} as const;
// Map browser name → Playwright browserName
const browserNames: Record<typeof browsers[number], 'chromium' | 'firefox' | 'webkit'> = {
chromium: 'chromium',
firefox: 'firefox',
webkit: 'webkit',
};
type DeviceKey = keyof typeof devicePresets;
export default defineConfig({
projects: browsers.flatMap(browser =>
(Object.keys(devicePresets) as DeviceKey[]).flatMap(device =>
envs.map(env => ({
name: `${browser}-${device}-${env}`,
use: {
browserName: browserNames[browser],
...devices[devicePresets[device]],
baseURL: baseURLs[env],
},
}))
)
),
});
3 × 3 × 2 = 18 project. Thêm 1 browser mới chỉ cần push vào array browsers — không cần copy-paste 6 entry.
Khi cần exception cho 1 project
Nếu project chrome-desktop-prod cần credentials khác, filter ra sau khi generate và patch:
const generated = browsers.flatMap(browser =>
envs.map(env => ({
name: `${browser}-${env}`,
use: { ...devices['Desktop Chrome'], baseURL: baseURLs[env] },
}))
);
// Patch riêng project prod để thêm httpCredentials
export default defineConfig({
projects: generated.map(project =>
project.name.includes('-prod')
? { ...project, use: { ...project.use, httpCredentials: { username: 'qa', password: process.env.PROD_PASS! } } }
: project
),
});
Naming Convention
Tên project ảnh hưởng trực tiếp đến khả năng filter bằng --project. Convention khuyến nghị:
- Format:
<browser>-<device>-<env> - Lowercase toàn bộ.
- Dùng hyphen (
-) làm separator — không dùng underscore hay space. - Thứ tự ưu tiên: browser → device → env (tổng quát → cụ thể).
Ví dụ tên hợp lệ
chromium-desktop-staging
chromium-mobile-staging
chromium-desktop-prod
firefox-desktop-staging
webkit-tablet-prod
chrome-desktop-tenant-a // multi-tenant: env = tenant name
chrome-mobile-tenant-b
Ví dụ tên dễ filter nhầm
// Tránh: space trong tên
Desktop Chrome Staging // --project="Desktop Chrome" match cả staging lẫn prod
// Tránh: không có separator nhất quán
chromiumDesktopStaging // --project="*desktop*" không match
chromium_desktop_staging // wildcard dùng * sẽ khác pattern
Wildcard * trong --project là glob-style, không phải regex. Với naming lowercase-hyphen, các pattern sau hoạt động tốt:
*-staging → tất cả project staging
chromium-* → tất cả project dùng Chromium
*-mobile-* → tất cả project mobile
*-prod → tất cả project production
Filter Subset Bằng --project
--project trên CLI nhận tên project chính xác hoặc wildcard glob. Có thể truyền nhiều lần để chọn nhiều project:
# Chỉ chạy project chromium-desktop-staging
npx playwright test --project="chromium-desktop-staging"
# Tất cả project staging (wildcard)
npx playwright test --project="*-staging"
# Tất cả project Chromium
npx playwright test --project="chromium-*"
# Tất cả project mobile (dimension device)
npx playwright test --project="*-mobile-*"
# Kết hợp nhiều project: chromium staging + firefox staging
npx playwright test --project="chromium-*-staging" --project="firefox-*-staging"
Khi không truyền --project, Playwright chạy tất cả project được khai báo trong config. Đây là lý do cần cân nhắc kỹ trước khi thêm project vào config mặc định.
Lưu ý: --project filter theo tên project, còn --grep filter theo tên test (bao gồm tag). Hai flag này độc lập và có thể dùng cùng nhau:
# Chỉ chạy test @smoke trên project *-staging
npx playwright test --project="*-staging" --grep "@smoke"
Use Case Thực Tế
Smoke test — nhanh, minimal
1 browser × 1 device × 1 env. Mục tiêu: xác nhận app không vỡ hoàn toàn sau deploy. Chạy trong 2-3 phút.
// Config cho smoke (riêng hoặc là 1 trong nhiều project)
{
name: 'chromium-desktop-staging',
use: { ...devices['Desktop Chrome'], baseURL: 'https://staging.app.com' },
},
npx playwright test --project="chromium-desktop-staging" --grep "@smoke"
Pre-release — đa browser, đa device, prod
3 browsers × 2 devices × prod. Chạy trước khi tag release để đảm bảo không có cross-browser regression trên production config.
npx playwright test --project="*-prod"
Multi-tenant — 1 browser × 1 device × N tenants
Khi app SaaS có nhiều tenant, mỗi tenant có domain hoặc subdomain riêng. Environment dimension trở thành tenant ID:
const tenants = ['tenant-a', 'tenant-b', 'tenant-c'] as const;
const tenantURLs = {
'tenant-a': 'https://tenant-a.app.com',
'tenant-b': 'https://tenant-b.app.com',
'tenant-c': 'https://tenant-c.app.com',
} as const;
projects: tenants.map(tenant => ({
name: `chrome-desktop-${tenant}`,
use: {
...devices['Desktop Chrome'],
baseURL: tenantURLs[tenant],
extraHTTPHeaders: { 'X-Tenant-ID': tenant },
},
})),
# Chỉ chạy tenant-a
npx playwright test --project="*-tenant-a"
# Tất cả tenant
npx playwright test --project="chrome-desktop-*"
Visual regression — đa browser × đa device
Screenshot baseline phụ thuộc vào cả browser lẫn device (pixel density, font rendering, OS-level antialiasing đều khác nhau). Mỗi project tạo ra tập baseline riêng. 3 browsers × 3 devices = 9 tập baseline, 9 lần chụp screenshot per test.
// snapshotPathTemplate phân tách snapshot theo project
snapshotPathTemplate: '{testDir}/__snapshots__/{testFilePath}/{arg}-{projectName}{ext}',
Trade-off Khi Scale Matrix
Số lần test thực thi tăng tuyến tính theo số project. Với 100 test và 18 project (3 browsers × 3 devices × 2 envs), tổng test instances là 1800. Mỗi test instance chạy độc lập — không share state giữa project.
| Matrix | Projects | 100 test → instances | CI duration (ước tính) |
|---|---|---|---|
| 1×1×1 (smoke) | 1 | 100 | ~3 phút |
| 3×1×1 (cross-browser) | 3 | 300 | ~9 phút |
| 3×2×1 (browser + device) | 6 | 600 | ~18 phút |
| 3×3×2 (full matrix) | 18 | 1800 | ~54 phút |
Các con số trên giả định test được parallel hóa tốt trên 4 workers và mỗi test trung bình ~7 giây. Duration thực tế phụ thuộc vào số worker, băng thông CI và độ nặng của app.
Không phải mọi test đều cần chạy trên mọi project. Test kiểm tra logic business (form validation, data display) thường không cần cross-browser — Chromium đủ. Test liên quan đến responsive layout, touch event hoặc CSS mới mới cần đầy đủ matrix.
Best Practice Scale CI
Thay vì chạy toàn bộ matrix mọi lúc, phân tầng theo context CI:
PR check — 1 project, smoke only
Mục tiêu: block merge nhanh khi có regression rõ ràng. Không cần cross-browser ở bước này.
npx playwright test --project="chromium-desktop-staging" --grep "@smoke"
Merge to main — critical path, 3 project
1 browser × 3 devices (desktop, mobile, tablet) trên staging. Kiểm tra responsive critical path mà không mở rộng sang cross-browser.
npx playwright test --project="chromium-*-staging"
Nightly — full matrix
18 project (hoặc tùy app), toàn bộ test suite. Chạy lúc 2AM khi không có PR. Kết quả report gửi lên Slack.
npx playwright test # Không filter, chạy tất cả project
Pre-release gate — prod config
3 browsers × 2 devices trên prod config, trước khi tag release. Chỉ @critical và @smoke, không @known-bug.
npx playwright test --project="*-prod" --grep "@critical|@smoke" --grep-invert "@known-bug"
Cấu trúc này giữ PR check nhanh (dưới 5 phút) trong khi vẫn đảm bảo full coverage trên nightly và pre-release.
Conditional Matrix — Nightly vs PR
Thay vì dùng 2 config file riêng, có thể dùng một config duy nhất với conditional logic dựa trên env var:
import { defineConfig, devices } from '@playwright/test';
const isNightly = process.env.NIGHTLY === 'true';
const stagingURL = 'https://staging.app.com';
const prodURL = 'https://app.com';
const smokeProject = {
name: 'chromium-desktop-staging',
use: { ...devices['Desktop Chrome'], baseURL: stagingURL },
};
const fullMatrix = [
{ name: 'chromium-desktop-staging', use: { ...devices['Desktop Chrome'], baseURL: stagingURL } },
{ name: 'chromium-mobile-staging', use: { ...devices['iPhone 15 Pro'], baseURL: stagingURL } },
{ name: 'chromium-tablet-staging', use: { ...devices['iPad Pro 11'], baseURL: stagingURL } },
{ name: 'firefox-desktop-staging', use: { ...devices['Desktop Firefox'], baseURL: stagingURL } },
{ name: 'webkit-desktop-staging', use: { ...devices['Desktop Safari'], baseURL: stagingURL } },
{ name: 'chromium-desktop-prod', use: { ...devices['Desktop Chrome'], baseURL: prodURL } },
];
export default defineConfig({
projects: isNightly ? fullMatrix : [smokeProject],
});
CI workflow kích hoạt full matrix:
jobs:
e2e-nightly:
if: github.event_name == 'schedule'
env:
NIGHTLY: 'true'
steps:
- run: npx playwright test
e2e-pr:
if: github.event_name == 'pull_request'
# NIGHTLY không set → smokeProject được dùng
steps:
- run: npx playwright test --grep "@smoke"
Pattern này giữ config centralized — không phải sync 2 file riêng. Khi thêm project mới, chỉ sửa mảng fullMatrix ở một chỗ.
Pitfall Thường Gặp
Pitfall 1 — Matrix quá lớn không cần thiết
Thêm project vì "phòng ngừa" mà không phân tích xem test nào thực sự cần cross-browser hay cross-device. Kết quả: CI nightly mất 2-3 giờ, team bắt đầu ignore kết quả vì quá lâu.
Cách tránh: bắt đầu với 1 project (Chromium desktop staging), chỉ mở rộng khi có bug thực sự liên quan đến browser/device khác. Đo duration CI trước và sau khi thêm project.
Pitfall 2 — Naming inconsistent, filter wildcard không hoạt động
// Sai: tên không nhất quán
{ name: 'Chromium Desktop Staging' }, // Space, capitalize
{ name: 'firefox-desktop-staging' }, // Lowercase hyphen
{ name: 'webkit_mobile_prod' }, // Underscore
// --project="*-staging" chỉ match firefox-desktop-staging
// Bỏ sót 'Chromium Desktop Staging' và 'webkit_mobile_prod'
Cách tránh: dùng programmatic matrix — tên được tạo từ cùng một template nên format luôn nhất quán. Nếu khai báo tay, đặt tên ngay từ đầu theo convention và enforce qua code review.
Pitfall 3 — Quên scope testMatch, test chạy trên project không liên quan
Khi có project chrome-desktop-staging và chrome-desktop-prod, nếu test file ghi data (tạo order, update profile), chạy trên prod project có thể ảnh hưởng production database.
// Giới hạn test chỉ chạy trên project staging
{
name: 'chrome-desktop-staging',
use: { ...devices['Desktop Chrome'], baseURL: stagingURL },
testMatch: '**/tests/**/*.spec.ts', // Tất cả test
},
{
name: 'chrome-desktop-prod',
use: { ...devices['Desktop Chrome'], baseURL: prodURL },
testMatch: '**/tests/readonly/**/*.spec.ts', // Chỉ test read-only
},
Pitfall 4 — Env var conflict giữa project chạy cùng worker
Playwright chạy test từ các project khác nhau trên cùng worker pool. Nếu test A của project staging và test B của project prod chạy parallel trên cùng worker, và cả hai đều set process.env.BASE_URL trong test.beforeAll, chúng sẽ conflict.
Cách tránh: không set env var global trong test code. Đọc baseURL từ use.baseURL qua baseURL fixture hoặc testInfo. Env var chỉ nên được set ở CI level, không trong test runtime.
// Sai: set global env var trong test
test.beforeAll(() => {
process.env.BASE_URL = 'https://staging.app.com'; // Conflict khi parallel
});
// Đúng: đọc từ fixture baseURL (được inject từ use.baseURL)
test('my test', async ({ page, baseURL }) => {
console.log(baseURL); // 'https://staging.app.com' hoặc 'https://app.com' tùy project
await page.goto('/');
});
Quiz
Câu 1. Config có 4 browser × 3 device × 2 env = 24 project, test suite có 50 test. Nếu chạy không filter, tổng số test instances là bao nhiêu? Và nếu filter --project="*-staging" với naming convention chuẩn?
Đáp án
Không filter: 50 × 24 = 1200 instances. Filter *-staging: chỉ còn 4×3×1 = 12 project staging → 50 × 12 = 600 instances. Naming convention lowercase-hyphen đảm bảo wildcard hoạt động đúng.
Câu 2. Tại sao devices['Desktop Chrome'] và devices['iPhone 15 Pro'] có thể cùng dùng browserName Chromium nhưng vẫn là hai project khác nhau? Sự khác biệt nằm ở đâu?
Đáp án
Sự khác biệt ở device dimension: Desktop Chrome có viewport 1280×720, isMobile: false, hasTouch: false và desktop UA. iPhone 15 Pro có viewport 393×852, deviceScaleFactor: 3, isMobile: true, hasTouch: true và mobile UA string. App có thể serve HTML khác nhau (responsive breakpoints, touch event listeners), nên test phải chạy trong context device riêng biệt dù cùng engine Chromium.
Câu 3. Bạn có config với isNightly = process.env.NIGHTLY === 'true' để chọn giữa smokeProject (1 project) và fullMatrix (18 project). CI PR job quên set env var NIGHTLY. Kết quả là gì?
Đáp án
process.env.NIGHTLY sẽ là undefined, expression undefined === 'true' là false, nên config dùng smokeProject — chỉ 1 project. Đây là behavior an toàn vì mặc định fail-safe về phía nhỏ hơn (smoke), không về phía full matrix. Nếu muốn opt-in full matrix thay vì opt-out, đây là pattern đúng.
Câu 4. Test checkout-flow.spec.ts ghi dữ liệu vào DB. Config có hai project: chrome-desktop-staging và chrome-desktop-prod. Làm thế nào để đảm bảo test này không bao giờ chạy trên project prod?
Đáp án
Hai cách: (1) Dùng testMatch trên project prod để chỉ match read-only test files, ví dụ testMatch: '**/tests/readonly/**'. (2) Trong test file, dùng test.skip(({ baseURL }) => baseURL?.includes('app.com') && !baseURL.includes('staging'), 'Skip trên prod') để tự skip khi detect prod URL. Cách 1 tốt hơn vì enforce ở config level, không phụ thuộc test developer nhớ thêm skip.
