Mục lục
Mục Tiêu Bài Học
Sau khi hoàn thành bài này, bạn sẽ:
- Hiểu
contextOptionsvàlaunchOptionshoạt động như escape hatch — cho phép truyền bất kỳ option nào củanewContext()vàlaunch()mà chưa có top-level fixture tương ứng. - Biết merge behavior khi top-level fixture và
contextOptionscùng set một key. - Áp dụng được các use case phổ biến:
reducedMotion/forcedColors,recordHar,strictSelectors, launch args anti-bot,executablePath, env vars cho browser process. - Cấu hình per-project override cho
contextOptionsvàlaunchOptions. - Tránh được 4 pitfall hay gặp — đặc biệt về worker-scope của
launchOptions.
Bài 408 (Series Cơ Bản) đã cover các launch options trong Library mode (chromium.launch()). Bài này không lặp lại phần đó — focus vào cách dùng launchOptions fixture trong Test Runner, merge behavior, và những điểm khác biệt quan trọng.
contextOptions Và launchOptions Là Gì
Playwright Test Runner expose nhiều top-level fixture như viewport, locale, timezone, headless, slowMo — mỗi fixture map 1-1 với một tham số cụ thể trong API. Nhưng không phải mọi option của browser.newContext() hay browserType.launch() đều có top-level fixture riêng.
contextOptions và launchOptions là hai fixture đặc biệt giải quyết vấn đề này:
contextOptions: nhận một object được merge vào options truyền chobrowser.newContext(). Bổ sung cho tất cả top-level fixture liên quan đến context (viewport,locale,timezone,userAgent, v.v.).launchOptions: nhận một object được merge vào options truyền chobrowserType.launch(). Bổ sung cho top-level fixture liên quan đến browser process (headless,slowMo).
Nói cách khác, đây là "catch-all" option — bất kỳ option nào của API gốc mà top-level fixture chưa cover, đặt vào đây để Playwright tự merge trước khi gọi API.
playwright.config.ts
└── use.contextOptions → merge → browser.newContext(mergedOptions)
└── use.launchOptions → merge → browserType.launch(mergedOptions)
Cấu Hình Cơ Bản
Ví dụ cấu hình dùng cả hai fixture cùng lúc:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
contextOptions: {
reducedMotion: 'reduce',
forcedColors: 'active',
strictSelectors: true,
},
launchOptions: {
args: ['--disable-blink-features=AutomationControlled'],
slowMo: 100,
env: { DEBUG: '1' },
},
},
});
Trong ví dụ trên:
reducedMotion,forcedColors,strictSelectorslà options củabrowser.newContext()chưa có top-level fixture riêng.argslà Chromium launch flags — không có top-level fixture.slowMo: 100tronglaunchOptionstương đương top-leveluse.slowMo: 100— nhưng đặt qualaunchOptions(xem merge behavior ở mục 4).envlà env vars cho browser process — không có top-level fixture.
Merge Behavior — Top-level Fixture vs contextOptions
Khi cùng lúc set một top-level fixture và cùng key đó trong contextOptions/launchOptions, Playwright merge hai object nhưng top-level fixture wins nếu trùng key:
export default defineConfig({
use: {
// Top-level fixture
viewport: { width: 1280, height: 720 },
slowMo: 200,
// contextOptions có viewport — sẽ BỊ OVERRIDE bởi top-level
contextOptions: {
viewport: { width: 375, height: 667 }, // bị bỏ qua
reducedMotion: 'reduce', // được áp dụng (không có top-level)
},
// launchOptions có slowMo — sẽ BỊ OVERRIDE bởi top-level
launchOptions: {
slowMo: 50, // bị bỏ qua
args: ['--no-sandbox'], // được áp dụng (không có top-level)
},
},
});
// Kết quả cuối: viewport = 1280×720, slowMo = 200, reducedMotion = 'reduce', args = ['--no-sandbox']
Cơ chế này không hoàn toàn được document rõ ràng trong Playwright docs — behavior thực tế: top-level fixture được xử lý trước và giá trị của chúng ghi đè key tương ứng trong contextOptions/launchOptions khi Playwright build options object cuối cùng.
Best practice:
- Dùng top-level fixture khi có — code dễ đọc hơn, type-safe hơn.
- Chỉ dùng
contextOptions/launchOptionscho option chưa có top-level fixture tương ứng. - Tránh set cùng option ở cả hai chỗ — dễ gây confusion về value nào thực sự có hiệu lực.
// KHÔNG NÊN — duplicate, khó biết value nào thắng
export default defineConfig({
use: {
viewport: { width: 1280, height: 720 },
contextOptions: {
viewport: { width: 375, height: 667 }, // sẽ bị bỏ qua nhưng gây confusion
},
},
});
// NÊN — mỗi option chỉ set một chỗ
export default defineConfig({
use: {
viewport: { width: 1280, height: 720 },
contextOptions: {
reducedMotion: 'reduce', // option không có top-level fixture
},
},
});
contextOptions — Use Case Thực Tế
1. Accessibility emulation — reducedMotion, forcedColors
Test accessibility behavior khi user bật reduced motion hoặc forced colors (high contrast mode). Không có top-level fixture cho các option này:
export default defineConfig({
projects: [
{
name: 'a11y-reduced-motion',
use: {
contextOptions: {
reducedMotion: 'reduce',
forcedColors: 'active',
},
},
},
],
});
Trong test, kiểm tra CSS @media (prefers-reduced-motion: reduce) và @media (forced-colors: active) được áp dụng đúng.
2. strictSelectors — bật strict mode per-project
strictSelectors: true khiến mọi locator throw ngay khi match nhiều hơn 1 element — thay vì chờ đến lúc tương tác. Hữu ích khi muốn enforce strict locator hygiene trong một project:
export default defineConfig({
use: {
contextOptions: {
strictSelectors: true,
},
},
});
// Khi strictSelectors: true
await page.locator('button').click();
// Nếu trang có nhiều hơn 1 button → throw ngay:
// Error: strict mode violation: locator('button') resolved to 3 elements
3. recordHar — capture network traffic
recordHar không có top-level fixture — phải đặt qua contextOptions:
export default defineConfig({
use: {
contextOptions: {
recordHar: {
path: 'test-results/network.har',
mode: 'minimal', // chỉ lưu request/response headers, không lưu body
},
},
},
});
HAR file sau đó dùng được để debug network issue, replay trong DevTools, hoặc dùng với page.routeFromHAR() để mock network.
4. serviceWorkers — tắt service worker
Khi test cần tắt service worker để tránh caching interference:
export default defineConfig({
use: {
contextOptions: {
serviceWorkers: 'block',
},
},
});
5. hasTouch — emulate touch device
Khi cần emulate touch mà không set isMobile (không có top-level fixture cho hasTouch độc lập):
export default defineConfig({
use: {
contextOptions: {
hasTouch: true,
},
},
});
launchOptions — Use Case Thực Tế
1. Launch args anti-bot
Chromium có một số flags giúp ẩn dấu hiệu automation. Pattern phổ biến khi test các trang có anti-bot detection:
export default defineConfig({
use: {
launchOptions: {
args: [
'--disable-blink-features=AutomationControlled',
'--no-sandbox',
],
},
},
});
Flag --disable-blink-features=AutomationControlled ẩn navigator.webdriver property — một trong các dấu hiệu phổ biến nhất mà bot detection dùng. Lưu ý: chỉ hoạt động với Chromium, không áp dụng cho Firefox hay WebKit.
2. executablePath — custom binary
Khi cần chạy test với một binary Chrome/Chromium cụ thể thay vì bundled binary của Playwright:
export default defineConfig({
use: {
launchOptions: {
executablePath: process.env.CHROME_PATH ?? '/usr/bin/google-chrome',
},
},
});
Use case thực tế: CI environment cung cấp sẵn Chrome, không muốn download browser binary riêng; hoặc cần test với một phiên bản Chrome cụ thể không phải bundled version.
3. env — environment variables cho browser process
Truyền env vars vào browser process — khác với env vars của Node process chạy test:
export default defineConfig({
use: {
launchOptions: {
env: {
DEBUG: 'pw:protocol', // bật Playwright protocol debug log
DISPLAY: ':99', // X display cho virtual framebuffer trên Linux
},
},
},
});
4. channel — dùng branded browser
Mặc dù channel có thể set trực tiếp qua launchOptions, cách này ít phổ biến hơn so với dùng projects[].use.channel. Hữu ích khi muốn set default channel mà không tách project:
export default defineConfig({
use: {
launchOptions: {
channel: 'chrome', // dùng branded Chrome thay vì bundled Chromium
},
},
});
Per-project Override
contextOptions và launchOptions có thể override per-project như mọi Option Fixture khác:
export default defineConfig({
// Global defaults — áp dụng cho tất cả projects
use: {
contextOptions: {
strictSelectors: true,
},
},
projects: [
{
name: 'desktop-chrome',
use: {
// launchOptions override chỉ cho project này
launchOptions: {
args: ['--disable-blink-features=AutomationControlled'],
},
},
},
{
name: 'mobile-chrome',
use: {
launchOptions: {
channel: 'chrome',
},
// contextOptions override — bổ sung hasTouch cho mobile project
contextOptions: {
hasTouch: true,
},
},
},
{
name: 'a11y',
use: {
// Project này override contextOptions để test accessibility
contextOptions: {
reducedMotion: 'reduce',
forcedColors: 'active',
strictSelectors: false, // tắt strictSelectors chỉ cho project a11y
},
},
},
],
});
Lưu ý về merge khi override: khi project định nghĩa contextOptions, nó không merge với global contextOptions — nó override hoàn toàn. Trong ví dụ trên, project a11y tắt strictSelectors nhưng global đã bật — project a11y dùng strictSelectors: false của riêng nó, không bị ảnh hưởng bởi global. Đây là convention override fixture thông thường của Playwright — sâu hơn wins, không phải deep merge.
Khác Biệt Với extraHTTPHeaders
extraHTTPHeaders là top-level fixture có trong use — truyền headers cho mọi request của context. Nó cũng là một option của browser.newContext(), nghĩa là về mặt kỹ thuật có thể set qua contextOptions.extraHTTPHeaders:
// Cách 1 — top-level fixture (KHUYẾN NGHỊ)
export default defineConfig({
use: {
extraHTTPHeaders: {
'x-api-key': process.env.API_KEY ?? '',
},
},
});
// Cách 2 — qua contextOptions (KHÔNG nên)
export default defineConfig({
use: {
contextOptions: {
extraHTTPHeaders: {
'x-api-key': process.env.API_KEY ?? '',
},
},
},
});
Cả hai đều hoạt động, nhưng dùng Cách 1 vì:
- Dễ đọc hơn — không cần biết
extraHTTPHeaderslà option củanewContext(). - Type-safe hơn — TypeScript infer đúng kiểu
Record<string, string>ngay tại top-level. - Nhất quán với top-level fixture pattern của team.
Bài 17 sẽ cover extraHTTPHeaders chi tiết — phần này chỉ nêu để làm rõ ranh giới giữa contextOptions và fixture có sẵn.
4 Pitfall Quan Trọng
1. Đặt viewport trong contextOptions — bị override bởi top-level
export default defineConfig({
use: {
viewport: { width: 1280, height: 720 }, // top-level fixture
contextOptions: {
viewport: { width: 375, height: 667 }, // được set nhưng sẽ bị bỏ qua
},
},
});
// Context thực sự nhận viewport 1280×720, không phải 375×667
Playwright không cảnh báo về key trùng — top-level fixture wins silently. Kết quả: test chạy với viewport không như kỳ vọng mà không có error message.
Fix: Nếu muốn đổi viewport, đổi top-level viewport fixture, không đặt trong contextOptions.
2. Confuse launchOptions (browser process) vs contextOptions (per-context)
Đây là nhầm lẫn phổ biến nhất. Một số option nghe có vẻ thuộc context nhưng thực ra thuộc browser launch:
// SAI — args không phải option của newContext()
export default defineConfig({
use: {
contextOptions: {
args: ['--disable-blink-features=AutomationControlled'], // không có hiệu lực
},
},
});
// ĐÚNG — args là launch option
export default defineConfig({
use: {
launchOptions: {
args: ['--disable-blink-features=AutomationControlled'],
},
},
});
Ngược lại, reducedMotion hay serviceWorkers là option của newContext(), không phải launch().
3. Set launchOptions per-test qua test.use() — không có hiệu lực
Browser được launch ở worker scope, trước khi test chạy. launchOptions thay đổi sau thời điểm này không có hiệu lực cho browser hiện tại:
// KHÔNG HOẠT ĐỘNG như kỳ vọng
test.describe('anti-bot tests', () => {
test.use({
launchOptions: {
args: ['--disable-blink-features=AutomationControlled'],
},
});
test('scrape page', async ({ page }) => {
// Browser đã được launch trước khi test.use() được áp dụng
// --disable-blink-features không có hiệu lực cho browser này
await page.goto('https://example.com');
});
});
Fix: launchOptions cần đặt ở config level hoặc per-project. Nếu cần per-test launchOptions thực sự khác nhau, phải tách thành project riêng với config khác nhau:
// playwright.config.ts — tách project để có launchOptions khác nhau
projects: [
{
name: 'anti-bot',
testMatch: '**/anti-bot/**/*.spec.ts',
use: {
launchOptions: {
args: ['--disable-blink-features=AutomationControlled'],
},
},
},
{
name: 'regular',
testMatch: '**/regular/**/*.spec.ts',
},
]
4. Dồn tất cả config vào contextOptions thay vì top-level fixture — code khó đọc
Một số dev dùng contextOptions cho mọi thứ vì thấy "đơn giản hơn là nhớ từng top-level fixture":
// KHÔNG NÊN — khó đọc, mất type hints từ defineConfig
export default defineConfig({
use: {
contextOptions: {
viewport: { width: 1280, height: 720 },
locale: 'vi-VN',
timezoneId: 'Asia/Ho_Chi_Minh',
colorScheme: 'dark',
geolocation: { latitude: 10.8, longitude: 106.7 },
},
},
});
// NÊN — dùng top-level fixture cho những gì đã có
export default defineConfig({
use: {
viewport: { width: 1280, height: 720 },
locale: 'vi-VN',
timezoneId: 'Asia/Ho_Chi_Minh',
colorScheme: 'dark',
geolocation: { latitude: 10.8, longitude: 106.7 },
contextOptions: {
// chỉ option thực sự không có top-level fixture
reducedMotion: 'reduce',
},
},
});
Vấn đề của cách đầu: mất autocomplete/type inference từ defineConfig vì TypeScript không infer kiểu sâu trong contextOptions; khó review; một số top-level fixture bị bỏ qua hoàn toàn nếu cùng tên key (behavior không rõ ràng khi mix).
Tổng Kết
contextOptionsvàlaunchOptionslà escape hatch — dùng khi option cần thiết chưa có top-level fixture riêng.contextOptions→ merge vàobrowser.newContext().launchOptions→ merge vàobrowserType.launch().- Top-level fixture wins khi trùng key với
contextOptions/launchOptions— không có warning. - Best practice: dùng top-level fixture khi có,
contextOptions/launchOptionschỉ cho phần còn lại. contextOptionsuse cases phổ biến:reducedMotion,forcedColors,strictSelectors,recordHar,serviceWorkers,hasTouch.launchOptionsuse cases phổ biến:args(Chromium flags),executablePath,env,channel.launchOptionskhông có hiệu lực khi set per-test quatest.use()— browser đã launch trước đó (worker-scope). Muốn khác nhau → tách project.- Per-project override
contextOptions/launchOptionskhông merge với global — project value override hoàn toàn. - Không đặt
extraHTTPHeaderstrongcontextOptions— dùng top-level fixture (bài 17).
Quiz Củng Cố
Câu 1
Config sau có kết quả gì khi test chạy?
export default defineConfig({
use: {
viewport: { width: 1280, height: 720 },
contextOptions: {
viewport: { width: 390, height: 844 },
strictSelectors: true,
},
},
});
Đáp án
Context được tạo với viewport 1280×720 (top-level fixture wins, giá trị trong contextOptions.viewport bị bỏ qua) và strictSelectors: true được áp dụng (không có top-level fixture tương ứng). Không có error hay warning về key trùng — behavior silent.
Câu 2
Tại sao đoạn code sau không hoạt động như kỳ vọng?
test.describe('anti-detection suite', () => {
test.use({
launchOptions: {
args: ['--disable-blink-features=AutomationControlled'],
},
});
test('check navigator.webdriver', async ({ page }) => {
await page.goto('https://bot.sannysoft.com');
// Kỳ vọng: navigator.webdriver = false
});
});
Đáp án
launchOptions được đọc ở worker scope — browser đã được launch trước khi test.use() trong describe block có cơ hội áp dụng. Khi test chạy, --disable-blink-features=AutomationControlled không được truyền vào browser process đang chạy. Để fix, đặt option này trong global use hoặc tạo project riêng với launchOptions đó.
Câu 3
Muốn capture HAR file cho tất cả test trong project network-audit. Viết config phù hợp.
Đáp án
export default defineConfig({
projects: [
{
name: 'network-audit',
use: {
contextOptions: {
recordHar: {
path: 'test-results/network-audit.har',
},
},
},
},
],
});
recordHar là option của browser.newContext() nhưng không có top-level fixture, nên phải đặt qua contextOptions. Đặt trong project riêng để chỉ project network-audit mới capture HAR — tránh HAR file lớn cho mọi test.
Câu 4
Sự khác biệt cốt lõi giữa contextOptions và launchOptions là gì? Kể tên 2 option nên đặt trong mỗi loại.
Đáp án
contextOptions ảnh hưởng đến browser context — được truyền vào browser.newContext(). Một context tương đương một "session" độc lập — mỗi test thường có context riêng. launchOptions ảnh hưởng đến browser process — được truyền vào browserType.launch(). Một browser process được share giữa các test trong cùng worker.
2 option phù hợp trong contextOptions: reducedMotion, recordHar (hoặc strictSelectors, serviceWorkers, hasTouch, forcedColors).
2 option phù hợp trong launchOptions: args (Chromium flags), executablePath (hoặc env, channel).
Câu 5
Monorepo có 2 project: desktop và mobile-touch. Desktop cần strictSelectors: true. Mobile cần hasTouch: true và strictSelectors: false. Viết config đúng.
Đáp án
export default defineConfig({
projects: [
{
name: 'desktop',
use: {
viewport: { width: 1280, height: 720 },
contextOptions: {
strictSelectors: true,
},
},
},
{
name: 'mobile-touch',
use: {
viewport: { width: 390, height: 844 },
contextOptions: {
hasTouch: true,
strictSelectors: false,
},
},
},
],
});
Per-project contextOptions override hoàn toàn — không deep merge với global. Mỗi project tự khai báo đầy đủ các key cần thiết trong contextOptions của mình.
Bài Tiếp Theo
Bài 17 tiếp tục nhóm Options Fixtures với extraHTTPHeaders và httpCredentials — fixtures set header và xác thực HTTP cho mọi request của context.
