Danh sách bài viết

Bài 4: Built-in Fixture browserName

browserName là built-in fixture của Playwright Test Runner, trả về string identifier engine đang chạy test: 'chromium', 'firefox', hoặc 'webkit'. Bài 4 giải thích worker-scope của fixture này, các use case thực tế (conditional skip per engine, branch logic, annotation, dùng trong custom fixture), cách phân biệt với testInfo.project.name và process.platform, pattern combine OS + engine, và các pitfall thường gặp khi dùng sai.

27/05/2026
10 phút đọc
0 lượt xem
1

Mục Tiêu Bài Học

  • Nắm rõ browserName trả về gì và scope của nó trong worker.
  • Biết khi nào dùng test.skip, test.fixme, hay branch logic với browserName.
  • Phân biệt browserName với testInfo.project.nameprocess.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.

2

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'.

3

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.

4

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.skiptest.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.

5

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.

6

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 }) => { /* ... */ });
});
7

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.

8

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'
  );
  // ...
});
9

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'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).

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');
11

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.

12

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.

13

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).

14

Tổng Kết

  • browserName trả 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.
  • browserName là engine identifier; testInfo.project.name là tên project tùy chỉnh; process.platform là OS.
  • Combine browserName + process.platform để target tổ hợp engine + OS chính xác.
  • Limitation: browserName không phân biệt channel ('chrome' vs Chromium bundled) hay version — dùng testInfo.project.use.channel hoặc browser.version() khi cần.
15

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''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 đề:

  1. 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' }.
  2. 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.
16

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.

Bài 5: Built-in Fixture request