Danh sách bài viết

Bài 16: Option Fixture contextOptions, launchOptions

Bài 16 nhóm "Fixtures - Options". contextOptions và launchOptions là hai Option Fixture đặc biệt: thay vì map 1-1 với một tham số cụ thể như viewport hay headless, chúng nhận toàn bộ một object được merge trực tiếp vào options truyền cho browser.newContext() và browserType.launch(). Đây là escape hatch khi cần dùng một option của API chưa được exposed qua top-level fixture — ví dụ reducedMotion, forcedColors, strictSelectors, recordHar, serviceWorkers qua contextOptions; hoặc Chromium launch args anti-bot, executablePath, env vars cho browser process qua launchOptions. Bài này tập trung vào merge behavior giữa top-level fixture và các options này, use case thực tế, per-project override, và 4 pitfall cần tránh.

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

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

Sau khi hoàn thành bài này, bạn sẽ:

  • Hiểu contextOptionslaunchOptions hoạt động như escape hatch — cho phép truyền bất kỳ option nào của newContext()launch() mà chưa có top-level fixture tương ứng.
  • Biết merge behavior khi top-level fixture và contextOptions cù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 contextOptionslaunchOptions.
  • 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.

2

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.

contextOptionslaunchOptions 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 cho browser.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 cho browserType.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)
3

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, strictSelectors là options của browser.newContext() chưa có top-level fixture riêng.
  • args là Chromium launch flags — không có top-level fixture.
  • slowMo: 100 trong launchOptions tương đương top-level use.slowMo: 100 — nhưng đặt qua launchOptions (xem merge behavior ở mục 4).
  • env là env vars cho browser process — không có top-level fixture.
4

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/launchOptions cho 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
    },
  },
});
5

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)@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,
    },
  },
});
6

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
    },
  },
});
7

Per-project Override

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

8

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 extraHTTPHeaders là option của newContext().
  • 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.

9

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

10

Tổng Kết

  • contextOptionslaunchOptions là escape hatch — dùng khi option cần thiết chưa có top-level fixture riêng.
  • contextOptions → merge vào browser.newContext(). launchOptions → merge vào browserType.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/launchOptions chỉ cho phần còn lại.
  • contextOptions use cases phổ biến: reducedMotion, forcedColors, strictSelectors, recordHar, serviceWorkers, hasTouch.
  • launchOptions use cases phổ biến: args (Chromium flags), executablePath, env, channel.
  • launchOptions không có hiệu lực khi set per-test qua test.use() — browser đã launch trước đó (worker-scope). Muốn khác nhau → tách project.
  • Per-project override contextOptions/launchOptions không merge với global — project value override hoàn toàn.
  • Không đặt extraHTTPHeaders trong contextOptions — dùng top-level fixture (bài 17).
11

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 contextOptionslaunchOptions 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: desktopmobile-touch. Desktop cần strictSelectors: true. Mobile cần hasTouch: truestrictSelectors: 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.

12

Bài Tiếp Theo

Bài 17 tiếp tục nhóm Options Fixtures với extraHTTPHeadershttpCredentials — fixtures set header và xác thực HTTP cho mọi request của context.

Bài 17: Option Fixture extraHTTPHeaders, httpCredentials