Mục lục
- Mục Tiêu Bài Học
playwrightFixture Là Gì- Source Code Fixture — Cơ Chế Inject
- Worker Scope — Điểm Khác Biệt Cốt Lõi
- Khác
browserFixture - Use Case 1: Launch Browser Khác Engine
- Use Case 2: Connect Remote Browser (CDP)
- Use Case 3: Device Descriptor Runtime
- Use Case 4: Custom Selector Engine
- Use Case 5: Standalone API Request Context
- 4 Pitfalls
- Quiz + Bài Tiếp
Mục Tiêu Bài Học
- Hiểu
playwrightfixture trả vềPlaywrightAPIinstance — root object của toàn bộ thư viện. - Phân biệt scope worker của
playwrightfixture so với scope test củapage,context,request. - Phân biệt
playwrightfixture vớibrowserfixture — 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
browserfixture đủ dùng.
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();
});
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à:
playwrightfixture 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
playwrightfixture đề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.
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ừ
playwrightkhông tự cleanup: browser launch trong test A không đượcplaywrightfixture tự đóng khi test A kết thúc — dev phải tựbrowser.close().
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.
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).
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.
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.
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
Pagetạ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 trongtest.beforeAllđể tránh đăng ký nhiều lần (mỗi lần test chạybeforeEachsẽ 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.
Use Case 5: Standalone API Request Context
playwright.request là APIRequest 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
baseURLkhá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 đủ.
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');
});
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?
- Dùng
browserfixture — Playwright tự detect engine theo hint. - Dùng
playwrightfixture, gọiplaywright.firefox.launch()trongtry/finally. - Dùng
browserNamefixture để switch engine. - Tạo project mới trong config với
name: 'firefox-bug'— không cầnplaywrightfixture.
Đá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()?
- Test scope — mỗi test có selectors registry riêng, register trong test body là an toàn.
- Worker scope — register 1 lần là global cho toàn worker; register lại cùng tên ở test sau sẽ throw.
- Worker scope — register reset sau mỗi test, không cần lo.
- 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();
});
- Không có vấn đề gì — code đúng.
- Thiếu
try/finally— nếupage.goto()hoặcexpect()throw,browser.close()không được gọi. - Nên dùng
browserfixture thay vì launch Firefox thủ công. - Không thể dùng
expect()vớipage.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?
- Luôn luôn —
playwright.request.newContext()cho phép kiểm soát hơn. - 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. - Khi test cần gọi API có authentication.
- 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?
- Cả hai đều launch Chrome mới — chỉ khác port.
connectOverCDPattach vào Chrome process đang chạy qua CDP;launch()tạo browser process mới.connectOverCDPchỉ hoạt động với Firefox.connectOverCDPvàconnect()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 baseURL — baseURL option fixture cho phép dùng path tương đối trong page.goto() và 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').
