Danh sách bài viết

Bài 6: Built-in Fixture `playwright`

Bài 6 nhóm Fixtures Built-in. playwright fixture trả về PlaywrightAPI instance — root object cấp cao nhất của thư viện. Khác tất cả fixture đã học (page, context, browser, browserName, request): playwright không cung cấp sẵn 1 object đã khởi tạo mà cho phép truy cập trực tiếp vào playwright.chromium, playwright.firefox, playwright.webkit (BrowserType), playwright.devices (device descriptors), playwright.selectors (custom selector engine registry), playwright.request (API request module). Scope: worker — 1 instance dùng chung cho mọi test trong cùng worker, không reset giữa các test. Khác browser fixture: browser cung cấp Browser instance đã launch theo project config; playwright cung cấp API root để dev tự quyết định launch cái gì. Use case: launch browser thứ hai khác engine khi project dùng Chromium, connectOverCDP attach Chrome đang chạy (--remote-debugging-port), lấy device descriptor runtime (playwright.devices['iPhone 15 Pro']), đăng ký custom selector engine (playwright.selectors.register()), tạo standalone API request context không qua Test Runner config. Source fixture: inject trực tiếp module playwright. 4 pitfall: launch extra browser quên close gây OOM, selectors.register trong test scope worker ảnh hưởng test sau, nhầm playwright fixture với Playwright class import trong Library mode, dùng playwright.chromium.launch() thay vì browser fixture gây overhead thừa.

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

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

  • Hiểu playwright fixture trả về PlaywrightAPI instance — root object của toàn bộ thư viện.
  • Phân biệt scope worker của playwright fixture so với scope test của page, context, request.
  • Phân biệt playwright fixture với browser fixture — hai cấp độ khác nhau của API.
  • Biết 5 use case cụ thể: launch browser khác engine, connectOverCDP, devices descriptor, selectors.register, standalone API context.
  • Tránh 4 pitfall: browser leak, selectors global state, nhầm với Library mode import, dùng sai khi browser fixture đủ dùng.
2

playwright Fixture Là Gì

playwright là built-in fixture của @playwright/test, kiểu PlaywrightAPI. Đây là object cấp cao nhất đại diện cho toàn bộ thư viện — giống như gọi require('playwright') hoặc import { chromium, firefox, webkit, devices, selectors, request } from 'playwright' trong Library mode.

Các module có thể truy cập qua playwright fixture:

Property Kiểu Mô tả
playwright.chromium BrowserType Launch / connect Chromium browser
playwright.firefox BrowserType Launch / connect Firefox browser
playwright.webkit BrowserType Launch / connect WebKit browser
playwright.devices Record<string, DeviceDescriptor> Map device name → descriptor (viewport, UA, touch)
playwright.selectors Selectors Registry đăng ký custom selector engine
playwright.request APIRequest Module tạo APIRequestContext thủ công

Cú pháp cơ bản:

import { test } from '@playwright/test';

test('access playwright API', async ({ playwright }) => {
  // BrowserType — có thể launch bất kỳ engine nào
  const browser = await playwright.chromium.launch();

  // Device descriptor — không cần launch
  const device = playwright.devices['iPhone 15 Pro'];
  console.log(device.viewport); // { width: 393, height: 852 }

  await browser.close();
});
3

Source Code Fixture — Cơ Chế Inject

Định nghĩa fixture trong Playwright Test Runner cực kỳ đơn giản:

// Simplified từ @playwright/test internals
playwright: async ({}, use) => {
  await use(playwrightModule);
}

Fixture inject thẳng module playwright (cùng object mà Library mode import). Không có setup phức tạp — chỉ wrap module vào fixture pattern để có scope và dependency injection. Điều này có nghĩa là:

  • playwright fixture và import * as pw from 'playwright' trỏ tới cùng 1 module instance.
  • Không có state riêng — không có browser nào được launch sẵn.
  • Mọi thứ test làm với playwright fixture đều là thao tác thủ công: launch, connect, create context, dispose.

Điểm khác biệt so với page, context, browser: các fixture đó có setup/teardown rõ ràng (launch browser → create context → create page khi bắt đầu; close page → close context → close browser khi kết thúc). playwright fixture không có teardown — dev tự chịu trách nhiệm cleanup mọi resource tạo ra.

4

Worker Scope — Điểm Khác Biệt Cốt Lõi

playwright fixture có scope worker, không phải test. So sánh với các fixture đã học:

Fixture Scope Khởi tạo lại khi
page test Mỗi test
context test Mỗi test
request test Mỗi test
browser worker Mỗi worker process mới
playwright worker Mỗi worker process mới
browserName worker Mỗi worker process mới

Scope worker nghĩa là: tất cả test chạy trong cùng 1 worker process chia sẻ cùng 1 playwright instance. Vì playwright fixture chỉ là module reference (không có state mutable), scope worker không gây vấn đề gì với bản thân fixture.

Nhưng hệ quả quan trọng:

  • playwright.selectors.register() là global: đăng ký 1 lần ở test đầu → tất cả test sau trong cùng worker (và thực ra toàn bộ process) đều thấy selector engine đó. Không có cách unregister.
  • State tạo ra từ playwright không tự cleanup: browser launch trong test A không được playwright fixture tự đóng khi test A kết thúc — dev phải tự browser.close().
5

Khác browser Fixture

Đây là cặp fixture dễ nhầm nhất vì cả hai đều worker scope và đều liên quan đến browser:

Khía cạnh browser fixture playwright fixture
Kiểu trả về Browser — instance đã launch PlaywrightAPI — API root, chưa launch gì
Browser engine Theo project config (--project=chromium) Dev tự chọn: playwright.chromium / firefox / webkit
Lifecycle Playwright tự launch & close theo worker Dev tự launch & phải tự close
Khi nào dùng Test cần browser theo config project (99% trường hợp) Test cần browser khác project config, hoặc cần devices/selectors/CDP
Overhead Không — browser đã có sẵn từ worker Thêm 1 browser process nếu launch — tốn memory

Quy tắc chọn: nếu chỉ cần 1 browser instance theo engine của project config → dùng browser fixture. Chỉ dùng playwright fixture khi có nhu cầu rõ ràng: engine khác, CDP connect, devices descriptor, hay selectors registry.

6

Use Case 1: Launch Browser Khác Engine

Khi project config chạy Chromium nhưng 1 test cụ thể cần verify behavior trên Firefox (ví dụ: bug chỉ tái hiện trên Firefox, hoặc test compatibility WebRTC API). Thay vì tạo thêm project Firefox chỉ cho 1 test, dùng playwright fixture launch Firefox thủ công.

import { test, expect } from '@playwright/test';

test('verify CSS grid rendering on Firefox', async ({ playwright }) => {
  const browser = await playwright.firefox.launch();
  try {
    const context = await browser.newContext();
    const page = await context.newPage();
    await page.goto('https://example.com/grid-layout');

    const gridContainer = page.locator('.grid-container');
    await expect(gridContainer).toBeVisible();
    const box = await gridContainer.boundingBox();
    // Kiểm tra layout đúng trên Firefox
    expect(box?.width).toBeGreaterThan(0);
  } finally {
    await browser.close(); // BẮT BUỘC — không có fixture nào tự close
  }
});

Lưu ý try/finally: nếu test fail ở giữa mà không có finally, browser process tồn tại suốt phiên — tích lũy nhiều test sẽ OOM. Đây là pattern bắt buộc khi launch browser thủ công từ playwright fixture.

Nếu cần chạy nhiều test cùng cần Firefox, cách sạch hơn là tạo custom fixture wrapper thay vì repeat try/finally ở từng test (sẽ học ở nhóm A.3 Custom Fixtures).

7

Use Case 2: Connect Remote Browser (CDP)

playwright.chromium.connectOverCDP() attach vào Chrome hoặc Chromium đang chạy qua Chrome DevTools Protocol (CDP). Use case điển hình: Chrome đã được launch với --remote-debugging-port=9222, test cần attach vào session đó thay vì mở browser mới.

test('attach to existing Chrome session', async ({ playwright }) => {
  // Chrome đang chạy: google-chrome --remote-debugging-port=9222
  const browser = await playwright.chromium.connectOverCDP('http://localhost:9222');

  // Lấy context và page đã tồn tại thay vì tạo mới
  const context = browser.contexts()[0];
  const page = context.pages()[0];

  // Thao tác trên session hiện có
  console.log(await page.title());

  // KHÔNG close browser khi chỉ attach — sẽ đóng Chrome thật
  // Chỉ disconnect
  await browser.close(); // với connectOverCDP, close() = disconnect, không kill process
});

Ngoài ra, BrowserType.connect() (khác với connectOverCDP) dùng để kết nối Playwright server chạy riêng — phù hợp với cloud test providers (BrowserStack, LambdaTest, v.v.) hoặc Playwright Grid.

test('connect to cloud browser', async ({ playwright }) => {
  const browser = await playwright.chromium.connect('wss://cloud-provider.com/playwright');
  try {
    const context = await browser.newContext();
    const page = await context.newPage();
    await page.goto('https://myapp.com');
    // ...
  } finally {
    await browser.close();
  }
});

Chỉ chromium hỗ trợ connectOverCDP. Firefox và WebKit không có CDP — dùng connect() (Playwright protocol) nếu cần connect remote Firefox/WebKit.

8

Use Case 3: Device Descriptor Runtime

playwright.devices là map từ device name sang DeviceDescriptor — object chứa viewport, userAgent, deviceScaleFactor, isMobile, hasTouch. Dùng khi cần lấy device preset tại runtime thay vì hardcode trong config.

test('mobile emulation with device preset', async ({ playwright, browser }) => {
  // Dùng browser fixture (đã có sẵn) — không cần launch thêm
  const iPhone = playwright.devices['iPhone 15 Pro'];
  // iPhone = {
  //   viewport: { width: 393, height: 852 },
  //   userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS...',
  //   deviceScaleFactor: 3,
  //   isMobile: true,
  //   hasTouch: true,
  // }

  const context = await browser.newContext({ ...iPhone });
  const page = await context.newPage();
  await page.goto('/');

  // Verify mobile layout
  await expect(page.locator('.mobile-menu')).toBeVisible();
  await context.close();
});

Kết hợp playwright fixture (lấy device descriptor) với browser fixture (không cần launch thêm) là pattern hiệu quả — tận dụng browser đã có sẵn trong worker thay vì launch browser thứ hai.

Danh sách đầy đủ tên device:

// Xem tất cả device names
test('list devices', async ({ playwright }) => {
  const names = Object.keys(playwright.devices);
  console.log(names);
  // ['Blackberry PlayBook', 'Blackberry PlayBook landscape', 'BlackBerry Z30', ...]
  // ~70+ device descriptors
});

Cú pháp playwright.devices['...' ] chỉ đọc dữ liệu tĩnh, không tốn resource. Nếu tên device sai, trả về undefined thay vì throw — cần kiểm tra trước khi spread.

9

Use Case 4: Custom Selector Engine

playwright.selectors.register(name, options) đăng ký custom selector engine — cho phép dùng cú pháp engine=query trong page.locator(). Ví dụ đăng ký engine react để locate bằng component name.

import { test } from '@playwright/test';

// Đăng ký trong beforeAll — chạy 1 lần per worker
test.beforeAll(async ({ playwright }) => {
  await playwright.selectors.register('react', {
    // script chạy trong browser context
    script: `{
      query(root, selector) {
        // Tìm React component theo displayName
        return root.querySelector('[data-testid="' + selector + '"]');
      },
      queryAll(root, selector) {
        return Array.from(root.querySelectorAll('[data-testid="' + selector + '"]'));
      }
    }`,
  });
});

test('locate by custom engine', async ({ page }) => {
  await page.goto('/');
  // Dùng engine 'react' vừa đăng ký
  await page.locator('react=UserCard').click();
});

Điểm quan trọng về selectors.register():

  • Global và vĩnh viễn: đăng ký 1 lần là có hiệu lực với mọi Page tạo ra sau đó trong cùng worker process. Không có unregister().
  • Tên trùng sẽ throw: không thể đăng ký 2 engine cùng tên — sẽ throw Error: Selector engine with name "react" is already registered.
  • Dùng beforeAll: đặt trong test.beforeAll để tránh đăng ký nhiều lần (mỗi lần test chạy beforeEach sẽ gọi lại → throw).

Deep-dive về selector engine script, query API và ví dụ với React/Vue component tree sẽ được bài riêng trong nhóm D (Locators nâng cao) đề cập.

10

Use Case 5: Standalone API Request Context

playwright.requestAPIRequest module cho phép tạo APIRequestContext thủ công với config khác hoàn toàn config của Test Runner. Khác request fixture (đọc config từ playwright.config.ts), context tạo từ playwright.request.newContext() không bị ràng buộc config chung.

test('API context với config riêng', async ({ playwright }) => {
  const apiContext = await playwright.request.newContext({
    baseURL: 'https://api.example.com',
    extraHTTPHeaders: {
      'X-API-Key': process.env.PRIVATE_API_KEY!,
      'Accept': 'application/json',
    },
    ignoreHTTPSErrors: true, // override config global
  });

  try {
    const res = await apiContext.get('/internal/stats');
    expect(res.ok()).toBeTruthy();
    const data = await res.json();
    console.log(data);
  } finally {
    await apiContext.dispose(); // BẮT BUỘC — không tự cleanup
  }
});

Trong Test Runner, request fixture hầu như luôn đủ dùng. Chỉ cần dùng playwright.request.newContext() khi:

  • Cần API context với baseURL khác hoàn toàn config project (ví dụ gọi external service khác domain).
  • Cần nhiều API context song song với credentials khác nhau trong cùng 1 test.
  • Test đang chạy ở Library mode style bên trong Test Runner (pattern hybrid hiếm gặp).

Nếu chỉ cần gọi API với config từ playwright.config.ts → dùng request fixture. Bài 5 đã cover đầy đủ.

11

4 Pitfalls

Pitfall 1: Launch Extra Browser Quên Close — Memory Leak

Mỗi playwright.chromium.launch() / firefox.launch() tạo 1 browser process riêng. Không có teardown tự động. Nếu test fail trước browser.close() mà không có try/finally, browser process tồn tại đến khi worker tắt — nhiều test lặp lại sẽ tích lũy process, dẫn đến OOM.

// SAI — nếu test fail, browser không được close
test('bad pattern', async ({ playwright }) => {
  const browser = await playwright.firefox.launch();
  const page = await browser.newPage();
  await page.goto('/'); // nếu throw ở đây → browser leak
  await browser.close();
});

// ĐÚNG — finally đảm bảo luôn close dù pass hay fail
test('correct pattern', async ({ playwright }) => {
  const browser = await playwright.firefox.launch();
  try {
    const page = await browser.newPage();
    await page.goto('/');
    // ...
  } finally {
    await browser.close();
  }
});

Pitfall 2: selectors.register() Trong Test Body — Throw Ở Test Sau

Vì selector đăng ký là global và worker scope không reset giữa test, đặt selectors.register() trong test body (không phải beforeAll) sẽ throw already registered từ test thứ 2 trở đi chạy trên cùng worker.

// SAI — test đầu pass, test thứ 2 throw "already registered"
test('test A', async ({ playwright }) => {
  await playwright.selectors.register('my-engine', { script: '...' }); // ← đăng ký lần 1
  // ...
});

test('test B', async ({ playwright }) => {
  await playwright.selectors.register('my-engine', { script: '...' }); // ← throw!
  // Error: Selector engine with name "my-engine" is already registered
});

// ĐÚNG — đăng ký 1 lần trong beforeAll
test.beforeAll(async ({ playwright }) => {
  await playwright.selectors.register('my-engine', { script: '...' });
});

Pitfall 3: Nhầm playwright Fixture Với Library Mode Import

Trong Library mode (không dùng Test Runner), dev import trực tiếp từ 'playwright'. Trong Test Runner, playwright fixture là cách đúng để truy cập cùng object đó. Dùng cả hai trong cùng file test gây nhầm lẫn về naming.

// Có thể nhầm lẫn:
import { chromium } from 'playwright'; // Library mode import — KHÔNG nên trong Test Runner

test('confusing', async ({ playwright }) => {
  // 'playwright' param = fixture
  // 'chromium' từ import = module-level
  const b1 = await playwright.chromium.launch(); // fixture
  const b2 = await chromium.launch();            // import trực tiếp
  // Cả hai đều hoạt động nhưng import trực tiếp bỏ qua DI, không clean
});

// ĐÚNG trong Test Runner — chỉ dùng fixture
test('clean', async ({ playwright }) => {
  const browser = await playwright.chromium.launch();
  // ...
});

Pitfall 4: Dùng playwright.chromium.launch() Khi browser Fixture Đủ Dùng

Nếu test chỉ cần browser theo engine của project config, dùng browser fixture — không có overhead launch thêm. Dùng playwright.chromium.launch() trong trường hợp này tạo thêm 1 browser process không cần thiết, tăng thời gian chạy và tốn memory.

// KHÔNG CẦN — tạo browser thừa
test('unnecessary launch', async ({ playwright }) => {
  const browser = await playwright.chromium.launch(); // launch browser thứ 2!
  try {
    const context = await browser.newContext();
    const page = await context.newPage();
    await page.goto('/');
    await expect(page).toHaveTitle('Home');
  } finally {
    await browser.close();
  }
});

// ĐÚNG — dùng browser fixture đã có sẵn
test('correct', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveTitle('Home');
});
12

Quiz + Bài Tiếp

Quiz

Câu 1

Project config dùng Chromium. Test cần verify một CSS animation chỉ tái hiện bug trên Firefox. Cách nào đúng nhất?

  1. Dùng browser fixture — Playwright tự detect engine theo hint.
  2. Dùng playwright fixture, gọi playwright.firefox.launch() trong try/finally.
  3. Dùng browserName fixture để switch engine.
  4. Tạo project mới trong config với name: 'firefox-bug' — không cần playwright fixture.
Đáp án

B hoặc D đều hợp lý, tùy ngữ cảnh. B dùng khi chỉ có 1-2 test đơn lẻ cần Firefox và không muốn thêm project. D là cách sạch hơn nếu có nhiều test Firefox — project riêng tự manage lifecycle. C sai — browserName là read-only string. A sai — browser fixture không switch engine, luôn theo project config.

Câu 2

playwright fixture có scope gì, và điều đó ảnh hưởng thế nào đến playwright.selectors.register()?

  1. Test scope — mỗi test có selectors registry riêng, register trong test body là an toàn.
  2. Worker scope — register 1 lần là global cho toàn worker; register lại cùng tên ở test sau sẽ throw.
  3. Worker scope — register reset sau mỗi test, không cần lo.
  4. Global scope — register tồn tại qua nhiều worker process.
Đáp án

B. playwright fixture worker scope, selectors.register() là global trong process và không có unregister. Đặt trong test body → test thứ 2 trên cùng worker throw "already registered". Giải pháp: đặt trong test.beforeAll. C sai — không có reset. D sai — scope là worker process, không cross-process.

Câu 3

Đoạn code sau có vấn đề gì?

test('multi browser', async ({ playwright }) => {
  const browser = await playwright.firefox.launch();
  const page = await browser.newPage();
  await page.goto('/checkout');
  expect(await page.title()).toBe('Checkout');
  await browser.close();
});
  1. Không có vấn đề gì — code đúng.
  2. Thiếu try/finally — nếu page.goto() hoặc expect() throw, browser.close() không được gọi.
  3. Nên dùng browser fixture thay vì launch Firefox thủ công.
  4. Không thể dùng expect() với page.title().
Đáp án

B. Nếu page.goto() timeout hoặc expect() fail, test throw exception và nhảy qua browser.close() → browser process leak. Cần bọc trong try/finally. C đúng về nguyên tắc nhưng không phải "vấn đề" của đoạn code — đề bài không nói project config dùng Firefox, nên launch Firefox thủ công có thể là có lý do.

Câu 4

Khi nào nên dùng playwright.request.newContext() thay vì request fixture?

  1. Luôn luôn — playwright.request.newContext() cho phép kiểm soát hơn.
  2. Khi cần API context với baseURL hoặc headers khác hoàn toàn config playwright.config.ts, hoặc cần nhiều context với credentials khác nhau trong cùng 1 test.
  3. Khi test cần gọi API có authentication.
  4. Khi cần tránh timeout mặc định của Test Runner.
Đáp án

B. request fixture (Bài 5) đủ cho hầu hết trường hợp — đọc config tự động, cleanup tự động. Chỉ dùng playwright.request.newContext() khi cần config khác biệt so với project config, hoặc cần nhiều context độc lập. A sai — overhead thêm, phải tự dispose. C sai — request fixture có extraHTTPHeaders cho auth. D sai — timeout không liên quan.

Câu 5

playwright.chromium.connectOverCDP('http://localhost:9222') làm gì và khác playwright.chromium.launch() thế nào?

  1. Cả hai đều launch Chrome mới — chỉ khác port.
  2. connectOverCDP attach vào Chrome process đang chạy qua CDP; launch() tạo browser process mới.
  3. connectOverCDP chỉ hoạt động với Firefox.
  4. connectOverCDPconnect() là cùng một method.
Đáp án

B. connectOverCDP dùng CDP protocol để kết nối Chrome/Chromium đang chạy với --remote-debugging-port. launch() tạo browser process hoàn toàn mới. A sai — connectOverCDP không launch gì mới. C sai — chỉ Chromium hỗ trợ CDP. D sai — connect() dùng Playwright protocol (khác CDP), dùng cho Playwright server/Grid.

Bài Tiếp Theo

Nhóm A.1 Fixtures Built-in kết thúc tại đây. Bài tiếp theo mở nhóm A.2 Fixtures Options:

Bài 7: Option Fixture baseURLbaseURL option fixture cho phép dùng path tương đối trong page.goto()request fixture. Cấu hình per-project, override trong test, resolve logic, và tương tác với page.goto('/') vs page.goto('/path').