Mục lục
- Mục Tiêu Bài Học
- browserName Là Gì
- Worker-Scope Của browserName
- Use Case: Conditional Skip Per Engine
- Use Case: Branch Logic Theo Engine
- Use Case: Annotation Với browserName
- Use Case: Dùng Trong Custom Fixture
- Phân Biệt browserName vs testInfo.project.name
- Phân Biệt browserName vs process.platform
- Pattern Combine Engine + OS
- browserName Trong CI
- Limitation: Engine Identifier Không Phản Ánh Channel
- Common Pitfalls
- Tổng Kết
- Quiz
- Bài Tiếp Theo
Mục Tiêu Bài Học
- Nắm rõ
browserNametrả về gì và scope của nó trong worker. - Biết khi nào dùng
test.skip,test.fixme, hay branch logic vớibrowserName. - Phân biệt
browserNamevớitestInfo.project.namevàprocess.platform. - Nhận diện 4 pitfall phổ biến và cách tránh.
Bài này không nhắc lại kiến trúc 3 engine (Chromium / Firefox / WebKit) đã có ở Series 1 bài 43. Trọng tâm là cách dùng fixture browserName hiệu quả trong test.
browserName Là Gì
browserName là một trong 6 built-in fixture của Playwright Test Runner (cùng nhóm với page, context, browser, request, playwright). Giá trị của nó là string identifier của engine đang chạy test:
type BrowserName = 'chromium' | 'firefox' | 'webkit';
Cách dùng cơ bản — destructure từ fixture object trong callback của test():
import { test, expect } from '@playwright/test';
test('engine-specific behavior', async ({ page, browserName }) => {
console.log('Running on:', browserName); // 'chromium', 'firefox', hoặc 'webkit'
// ...
});
Playwright inject giá trị này dựa trên project đang chạy — không cần khai báo thêm gì trong config. Nếu project dùng devices['Desktop Chrome'] thì browserName === 'chromium'; nếu dùng devices['iPhone 15'] (WebKit) thì browserName === 'webkit'.
Worker-Scope Của browserName
browserName là worker-scoped fixture — nghĩa là giá trị của nó được xác định một lần khi worker khởi động và cố định suốt vòng đời worker đó. Một worker chỉ chạy một engine duy nhất.
Khi Playwright chạy cross-browser với nhiều project, mỗi project được chạy bởi worker riêng (hoặc tập worker riêng). Worker của project chromium luôn có browserName === 'chromium'; worker của project webkit luôn có browserName === 'webkit'. Không bao giờ có trường hợp 2 giá trị engine trong cùng một worker.
Project chromium → Worker A: browserName = 'chromium'
Project firefox → Worker B: browserName = 'firefox'
Project webkit → Worker C: browserName = 'webkit'
Điều này có nghĩa: bạn có thể đọc browserName tại bất kỳ vị trí nào trong test (kể cả beforeAll, afterAll) — giá trị sẽ không thay đổi giữa các test trong cùng worker.
Use Case: Conditional Skip Per Engine
Use case phổ biến nhất: bỏ qua test trên một engine cụ thể khi feature chưa hỗ trợ hoặc có bug đã biết chưa fix.
Pattern skip với condition và reason:
test('CSS feature chỉ Chromium', async ({ page, browserName }) => {
test.skip(browserName !== 'chromium', 'Feature requires Chromium-specific CSS');
await page.goto('/');
// assertion cho Chromium CSS feature
});
Pattern fixme — biết test đang broken, sẽ fix sau:
test('animation transform 3D', async ({ page, browserName }) => {
test.fixme(
browserName === 'webkit',
'WebKit bug WK-2345 — behavior lệch, chờ upstream fix'
);
await page.goto('/animation');
// ...
});
Phân biệt test.skip và test.fixme:
test.skip(condition, reason)— bỏ qua hoàn toàn, không tính fail. Dùng khi feature chưa available trên engine đó và không có kế hoạch fix.test.fixme(condition, reason)— đánh dấu test đang broken và biết nguyên nhân. Hiển thị riêng trong HTML report. Dùng khi có kế hoạch fix sau.
Mọi skip / fixme đều phải kèm reason rõ ràng, lý tưởng là link tới issue tracker. Không có reason → khó biết khi nào được xóa bỏ skip.
Use Case: Branch Logic Theo Engine
Không phải lúc nào cũng cần skip — đôi khi logic test đúng trên tất cả engine nhưng cách thực hiện assertion khác nhau:
test('date input behavior', async ({ page, browserName }) => {
await page.goto('/form');
const input = page.locator('input[type="date"]');
if (browserName === 'webkit') {
// WebKit native date picker có cách fill khác với Chromium/Firefox
await input.fill('2026-06-01');
await page.keyboard.press('Tab'); // cần Tab để confirm trên WebKit
} else {
await input.fill('2026-06-01');
}
await expect(input).toHaveValue('2026-06-01');
});
Một ví dụ khác — file download trigger khác nhau theo engine:
test('file download', async ({ page, browserName }) => {
await page.goto('/export');
const [download] = await Promise.all([
page.waitForEvent('download'),
page.click('#download-btn'),
]);
// WebKit đôi khi cần thêm thời gian cho dialog dismiss
if (browserName === 'webkit') {
await page.waitForTimeout(300);
}
const path = await download.path();
expect(path).toBeTruthy();
});
Lưu ý: branch logic làm test khó đọc hơn. Nếu khác biệt lớn, cân nhắc tách thành 2 test riêng với test.skip per engine thay vì dùng if/else lồng sâu.
Use Case: Annotation Với browserName
Annotation là metadata gắn vào test, hiển thị trong HTML report và có thể lọc qua API. Dùng browserName để ghi engine vào annotation giúp tracing dễ hơn:
test('checkout flow', async ({ page, browserName }) => {
test.info().annotations.push({
type: 'browser',
description: browserName,
});
await page.goto('/checkout');
// ...
});
Khi test fail, HTML report sẽ hiển thị annotation browser: webkit ngay dưới tên test — biết ngay engine nào fail mà không cần đọc tên project.
Annotation cũng dùng được trong beforeEach để apply cho tất cả test trong describe block:
test.describe('Payment flows', () => {
test.beforeEach(async ({ browserName }) => {
test.info().annotations.push({
type: 'engine',
description: browserName,
});
});
test('credit card payment', async ({ page }) => { /* ... */ });
test('paypal payment', async ({ page }) => { /* ... */ });
});
Use Case: Dùng Trong Custom Fixture
Custom fixture (xem bài A.3 về test.extend()) có thể nhận browserName như dependency để branch setup logic:
import { test as base } from '@playwright/test';
type MyFixtures = {
authPage: { token: string; storageStatePath: string };
};
export const test = base.extend<MyFixtures>({
authPage: async ({ browserName }, use) => {
// Cookie format giữa các engine đôi khi khác nhau.
// Dùng storageState file riêng per engine để tránh mismatch.
const storageStatePath = `playwright/.auth/${browserName}-state.json`;
const token = await fetchToken(browserName);
await use({ token, storageStatePath });
// cleanup nếu cần
},
});
Ví dụ cụ thể hơn — login state per engine:
export const test = base.extend<{}, { workerStorageState: string }>({
workerStorageState: [async ({ browser, browserName }, use) => {
const statePath = `playwright/.auth/user-${browserName}.json`;
// ...setup login một lần per worker...
await use(statePath);
}, { scope: 'worker' }],
storageState: ({ workerStorageState }, use) => use(workerStorageState),
});
Pattern này đặc biệt hữu ích khi cookie domain hoặc SameSite attribute xử lý khác nhau giữa Chromium và WebKit.
Phân Biệt browserName vs testInfo.project.name
Đây là điểm hay bị nhầm nhất:
| Thuộc tính | Giá trị | Mô tả |
|---|---|---|
browserName |
'chromium' | 'firefox' | 'webkit' |
Engine identifier — chỉ 3 giá trị cố định |
testInfo.project.name |
string tùy ý | Tên project đặt trong playwright.config.ts |
Ví dụ config với nhiều project cùng engine:
projects: [
{ name: 'desktop-chrome', use: { ...devices['Desktop Chrome'] } },
{ name: 'mobile-chrome', use: { ...devices['Pixel 8'] } },
{ name: 'desktop-safari', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile-safari', use: { ...devices['iPhone 15'] } },
]
Trong test chạy với project 'mobile-chrome':
test('example', async ({ page, browserName }) => {
console.log(browserName); // 'chromium'
console.log(testInfo.project.name); // 'mobile-chrome'
});
Khi muốn skip theo engine — dùng browserName. Khi muốn skip theo cấu hình project cụ thể (vd chỉ skip 'mobile-chrome' nhưng không skip 'desktop-chrome') — dùng testInfo.project.name:
test('desktop-only feature', async ({ page }, testInfo) => {
test.skip(
testInfo.project.name === 'mobile-chrome',
'Feature này không hỗ trợ viewport mobile'
);
// ...
});
Phân Biệt browserName vs process.platform
| Giá trị | Ý nghĩa | Ví dụ |
|---|---|---|
browserName |
Browser engine đang chạy test | 'chromium', 'firefox', 'webkit' |
process.platform |
OS của máy đang chạy test | 'darwin', 'linux', 'win32' |
Hai giá trị này độc lập — có thể chạy WebKit trên Linux (browserName === 'webkit' và process.platform === 'linux'). Kết hợp cả hai khi cần target một tổ hợp OS + engine cụ thể (xem bước 10).
Pattern Combine Engine + OS
Dùng test.fixme với condition kết hợp để target đúng tổ hợp cần xử lý:
test('scroll behavior', async ({ page, browserName }) => {
test.fixme(
browserName === 'webkit' && process.platform === 'linux',
'WebKit Linux không stable cho test này — bug FF-WK-001'
);
await page.goto('/long-page');
// ...
});
Pattern này hữu ích khi:
- Bug chỉ tái hiện trên WebKit Linux (CI runner) nhưng không tái hiện trên WebKit macOS (máy dev).
- Test dùng font rendering — font render khác nhau giữa Linux và macOS ngay cả cùng engine.
- Test liên quan đến file path separator —
'\\'trên Windows vs'/'trên Unix.
Có thể extend thêm để check 3 chiều:
const isFlaky =
browserName === 'webkit' &&
process.platform === 'linux' &&
process.env.CI === 'true';
test.fixme(isFlaky, 'WebKit Linux CI — flaky do GPU unavailable, tracked: #456');
browserName Trong CI
Trong report CLI, mỗi test xuất hiện với prefix engine từ project name — ví dụ:
[chromium] › tests/login.spec.ts › login success
[firefox] › tests/login.spec.ts › login success
[webkit] › tests/login.spec.ts › login success
Prefix này là project name, không phải browserName — nhưng nếu project đặt tên theo engine (như ví dụ trên) thì trông giống nhau.
GitHub Actions matrix thường dùng env var để chạy từng project:
strategy:
matrix:
browser: [chromium, firefox, webkit]
steps:
- run: npx playwright test --project=${{ matrix.browser }}
Playwright không đọc env var matrix.browser trực tiếp — bạn truyền qua --project flag. Khi đó browserName tự đúng theo project được chọn, không cần set thêm gì.
Bộ lọc CLI dùng --project (project name), không phải --browser. Không có flag --browser trong Playwright Test Runner.
Limitation: Engine Identifier Không Phản Ánh Channel
browserName chỉ có 3 giá trị cố định — không phân biệt được channel (kênh phát hành). Cả Chromium bundled và Google Chrome stable đều trả về 'chromium':
// Project A: dùng Chromium bundled (default)
// Project B: dùng channel: 'chrome' (Google Chrome stable)
// Cả 2 đều có browserName === 'chromium'
test('example', async ({ page, browserName }) => {
console.log(browserName); // 'chromium' trong cả 2 project
});
Nếu cần phân biệt channel — đọc testInfo.project.use.channel:
test('channel-aware test', async ({ page }, testInfo) => {
const channel = testInfo.project.use.channel ?? 'chromium-bundled';
console.log(channel); // 'chrome', 'msedge', hoặc undefined
test.skip(
channel === undefined,
'Test này yêu cầu Chrome stable có Widevine DRM'
);
// ...
});
Tương tự: không phân biệt được version. Chromium bundled v1.50 và v1.51 đều là 'chromium'. Muốn version → gọi browser.version() hoặc đọc PLAYWRIGHT_CHROMIUM_VERSION env.
Common Pitfalls
Pitfall 1: So sánh sai case
browserName luôn là lowercase. So sánh với giá trị có chữ hoa → never match:
// SAI — không bao giờ true
if (browserName === 'Chromium') { ... }
if (browserName === 'WEBKIT') { ... }
// ĐÚNG
if (browserName === 'chromium') { ... }
if (browserName === 'webkit') { ... }
TypeScript sẽ cảnh báo nếu type annotation đúng (browserName: 'chromium' | 'firefox' | 'webkit'), nhưng nếu cast sang string thì mất warning.
Pitfall 2: Nhầm với project name
// SAI — project name 'mobile-chrome' không bao giờ bằng browserName
test.skip(browserName === 'mobile-chrome', 'wrong');
// ĐÚNG — dùng testInfo.project.name cho project name
test.skip(testInfo.project.name === 'mobile-chrome', 'correct');
Pitfall 3: Skip toàn test khi chỉ cần skip 1 assertion
// CÓ THỂ TRÁNH ĐƯỢC — bỏ qua cả test vì 1 assertion nhỏ
test('form validation', async ({ page, browserName }) => {
test.skip(browserName === 'firefox', 'Date picker UI different');
// ... 20 assertion khác vẫn valid trên Firefox
});
// TỐT HƠN — chỉ skip phần assertion bị ảnh hưởng
test('form validation', async ({ page, browserName }) => {
await page.goto('/form');
// ... 20 assertion khác chạy bình thường trên mọi engine
if (browserName !== 'firefox') {
// assertion cho date picker native UI — chỉ skip trên Firefox
await expect(page.locator('.date-picker-calendar')).toBeVisible();
}
});
Pitfall 4: Nhầm engine với channel/version
browserName === 'chromium' không có nghĩa là "Google Chrome". Nếu dùng browserName để branch code liên quan đến Widevine DRM, codec H.264, hoặc Google services → logic sẽ sai vì Chromium bundled không có các tính năng này. Dùng testInfo.project.use.channel thay thế (xem bước 12).
Tổng Kết
browserNametrả về'chromium' | 'firefox' | 'webkit'— worker-scoped, cố định suốt vòng đời worker.- Dùng
test.skip(condition, reason)khi feature không available;test.fixme(condition, reason)khi bug đã biết và có kế hoạch fix. - Branch logic
if (browserName === ...)cho trường hợp test hợp lệ trên mọi engine nhưng cách thực hiện khác nhau. Nếu khác biệt lớn, ưu tiên tách test riêng. browserNamelà engine identifier;testInfo.project.namelà tên project tùy chỉnh;process.platformlà OS.- Combine
browserName+process.platformđể target tổ hợp engine + OS chính xác. - Limitation:
browserNamekhông phân biệt channel ('chrome'vs Chromium bundled) hay version — dùngtestInfo.project.use.channelhoặcbrowser.version()khi cần.
Quiz
Câu 1
Project config có tên 'mobile-safari' với devices['iPhone 15']. Trong test chạy qua project này, browserName sẽ là gì?
Đáp án
'webkit'. devices['iPhone 15'] gán browserName: 'webkit'. Project name 'mobile-safari' là tên tùy chỉnh, không ảnh hưởng đến giá trị browserName.
Câu 2
Test sau có vấn đề gì?
test('clipboard API', async ({ page, browserName }) => {
test.skip(browserName === 'Firefox', 'Firefox chưa support Clipboard API');
await page.goto('/clipboard-demo');
// ...
});
Đáp án
So sánh sai case. browserName luôn lowercase — giá trị là 'firefox', không phải 'Firefox'. Điều kiện browserName === 'Firefox' never true → skip không bao giờ kích hoạt, test sẽ fail trên Firefox thay vì được skip. Sửa lại: test.skip(browserName === 'firefox', ...).
Câu 3
Bạn cần skip test chỉ khi chạy trên project 'desktop-chrome' (Chromium với channel: 'chrome'), nhưng không skip trên project 'chromium' (Chromium bundled). Dùng browserName hay testInfo.project.name?
Đáp án
Dùng testInfo.project.name. Cả 2 project đều có browserName === 'chromium' — không thể phân biệt bằng browserName. Chỉ có testInfo.project.name mới khác nhau giữa 'desktop-chrome' và 'chromium'.
test('DRM video', async ({ page }, testInfo) => {
test.skip(
testInfo.project.name !== 'desktop-chrome',
'Test này yêu cầu Google Chrome stable với Widevine'
);
// ...
});
Câu 4
Bạn có test đang fail chỉ trên WebKit khi chạy trong CI (Linux), nhưng pass khi chạy locally (macOS). Viết condition cho test.fixme để đúng mục tiêu.
Đáp án
test.fixme(
browserName === 'webkit' && process.platform === 'linux',
'WebKit Linux CI fail do font rendering khác macOS — tracked: #789'
);
Condition kết hợp cả engine (browserName === 'webkit') và OS (process.platform === 'linux') để chỉ fixme trên đúng tổ hợp gây ra bug. Test vẫn chạy bình thường trên WebKit macOS và các engine khác.
Câu 5
Custom fixture dưới đây có vấn đề gì tiềm ẩn?
export const test = base.extend({
authState: async ({ browserName }, use) => {
if (browserName === 'chromium') {
await use('playwright/.auth/chrome-state.json');
} else {
await use('playwright/.auth/state.json');
}
},
});
Đáp án
Hai vấn đề:
- Fixture không có
scope: 'worker'— mặc định là test-scope, sẽ load storageState lại mỗi test thay vì mỗi worker. Với auth fixture nặng (cần HTTP call để setup), đây là performance issue. Thêm{ scope: 'worker' }. - Firefox và WebKit dùng chung
'state.json'. Nếu cookie format khác nhau giữa 2 engine gây mismatch, cần 3 file riêng (1 per engine) hoặc ít nhất là'state-${browserName}.json'cho cả 3.
Bài Tiếp Theo
Bài 5 xem xét fixture request — built-in APIRequestContext để gọi HTTP trong test mà không cần mở browser.
