Mục lục
- Mục Tiêu Bài Học
- browser là gì — worker-scope
- So Sánh Scope: browser vs context vs page
- Vòng Đời browser
- Cú Pháp Cơ Bản
- Use Case 1: Multi-context Trong 1 Test
- Use Case 2: Pattern beforeAll Với browser
- Các Method Quan Trọng
- Configure qua launchOptions
- browser Trong Multi-Project Config
- Override browser Fixture (Rare)
- Pitfall Thường Gặp
- Tổng Kết
- Quiz
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau bài này bạn sẽ:
- Hiểu
browserfixture là worker-scope — tức 1 instance per worker, reuse qua nhiều test, khác hoàn toàn vớipage/contextlà test-scope. - Biết khi nào cần dùng
browsertrực tiếp thay vì chỉ dùngpage/context: multi-context trong 1 test,beforeAllcần navigate. - Viết đúng pattern
beforeAll+afterAllvớibrowser, tránh state leak. - Biết các method hay dùng của
Browserobject và cách configure qualaunchOptions. - Nhận diện và tránh 4 pitfall liên quan đến worker-scope sharing.
browser là gì — worker-scope
Fixture browser trả về một Browser instance — object đại diện cho toàn bộ browser process (Chromium, Firefox, hoặc WebKit tuỳ project). Đây là built-in fixture của @playwright/test, được cung cấp tự động mà không cần khai báo thêm.
Điểm phân biệt cốt lõi so với page và context: browser là worker-scope.
Worker-scope nghĩa là:
- Playwright khởi tạo
browsermột lần khi worker process bắt đầu xử lý test. - Tất cả test chạy trên cùng worker đó dùng chung instance
browsernày. - Khi worker kết thúc (xong tất cả test được phân công),
browsermới được đóng.
Ngược lại, context và page là test-scope — mỗi test nhận một instance mới hoàn toàn, không liên quan đến test trước.
So Sánh Scope: browser vs context vs page
| Fixture | Scope | Tần suất tạo mới | Cô lập |
|---|---|---|---|
browser |
Worker | 1 lần per worker (nhiều test dùng chung) | Không cô lập giữa test |
context |
Test | Mỗi test nhận context mới, fresh | Cô lập hoàn toàn: cookies, localStorage, session |
page |
Test | Mỗi test nhận page mới trong context mới | Cô lập hoàn toàn |
Trong thực tế hầu hết test chỉ cần page hoặc context. Fixture browser hữu ích trong các tình huống cụ thể mà fixture test-scope không đáp ứng được — sẽ đề cập ở các mục use case.
Bài 1 (fixture page) và bài 2 (fixture context) đã đi chi tiết vào test-scope. Bài này tập trung vào điểm khác biệt do worker-scope tạo ra.
Vòng Đời browser
Vòng đời của browser gắn với worker process, không phải với từng test:
Worker khởi động
↓
Test 1 request fixture `browser`
→ Playwright gọi chromium.launch() (hoặc firefox / webkit tuỳ project)
→ Browser instance được tạo, cache trong worker
→ Test 1 chạy xong (browser KHÔNG đóng)
↓
Test 2 request fixture `browser`
→ Playwright trả cùng instance đã cache
→ Test 2 chạy xong (browser vẫn KHÔNG đóng)
↓
... (các test tiếp theo trong worker)
↓
Worker kết thúc (xong tất cả test được phân công)
→ Playwright gọi browser.close() tự động
Điểm quan trọng: fixture không đóng browser sau mỗi test. Fixture chỉ quản lý lifecycle ở cấp worker. Đây là lý do context và page được tạo mới mỗi test — để bù đắp cho việc browser được reuse, Playwright tạo context fresh để đảm bảo cô lập.
Nếu trong một test bạn tự gọi browser.close(), test sau trong cùng worker sẽ nhận được một browser đã đóng và fail. Playwright có cơ chế phát hiện khi browser bị đóng sớm và thường tạo lại, nhưng hành vi này không được đảm bảo và không nên dựa vào.
Cú Pháp Cơ Bản
Inject fixture browser qua destructuring, giống các fixture khác:
import { test, expect } from '@playwright/test';
test('test with browser', async ({ browser }) => {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com');
await expect(page).toHaveTitle(/Example/);
// Phải close manual — fixture không track context/page tạo trong test
await context.close();
});
Khi tự tạo context từ browser, bạn cũng phải tự close. Fixture không tự động cleanup các context tạo trong body test — đây là điểm khác biệt quan trọng so với dùng fixture context trực tiếp (fixture context tự close sau mỗi test).
Use Case 1: Multi-context Trong 1 Test
Trường hợp phổ biến nhất cần browser: 1 test cần 2 user đồng thời. Mỗi user cần context riêng (session riêng biệt). Không thể dùng fixture context vì fixture chỉ cung cấp 1 context per test.
Ví dụ kiểm tra tính năng chat — user A gửi tin, user B nhận:
import { test, expect } from '@playwright/test';
test('chat A → B', async ({ browser }) => {
// Tạo 2 context riêng biệt — 2 session độc lập
const userA = await browser.newContext({ storageState: 'playwright/.auth/userA.json' });
const userB = await browser.newContext({ storageState: 'playwright/.auth/userB.json' });
const pageA = await userA.newPage();
const pageB = await userB.newPage();
await pageA.goto('/chat');
await pageA.getByPlaceholder('Type...').fill('Hello B');
await pageA.getByRole('button', { name: 'Send' }).click();
await pageB.goto('/chat');
await expect(pageB.getByText('Hello B')).toBeVisible();
// Close cả 2 context sau khi xong
await userA.close();
await userB.close();
});
Pattern này phù hợp cho các tình huống:
- Chat / messaging — kiểm tra real-time delivery.
- Collaborative editing — 2 user edit cùng document.
- Permission test — admin thay đổi role, user thấy thay đổi ngay.
- Notification test — action từ user A trigger notification cho user B.
Lưu ý: luôn close cả 2 context sau test. Context không được close sẽ tồn tại trong browser process (worker-scope), có thể ảnh hưởng đến test sau nếu browser chạm giới hạn tài nguyên.
Use Case 2: Pattern beforeAll Với browser
Hook beforeAll chạy ở worker-scope — chỉ các fixture worker-scope mới khả dụng. Fixture page và context là test-scope, không inject được vào beforeAll.
Khi cần navigate trong beforeAll (ví dụ seed data qua UI, login lấy token), phải tự tạo page từ browser:
import { test, expect, Page } from '@playwright/test';
test.describe('Tests sharing seed data', () => {
let setupPage: Page;
test.beforeAll(async ({ browser }) => {
setupPage = await browser.newPage();
await setupPage.goto('/admin/setup');
await setupPage.getByRole('button', { name: 'Seed Test Data' }).click();
await expect(setupPage.getByText('Seed complete')).toBeVisible();
// Không close ngay — đóng trong afterAll
});
test.afterAll(async () => {
await setupPage.close();
});
test('user sees seeded product', async ({ page }) => {
await page.goto('/products');
await expect(page.getByText('Test Product A')).toBeVisible();
});
test('user can add to cart', async ({ page }) => {
await page.goto('/products');
await page.getByText('Test Product A').click();
await page.getByRole('button', { name: 'Add to Cart' }).click();
await expect(page.getByText('Added to cart')).toBeVisible();
});
});
Điểm cần chú ý trong pattern này:
setupPagekhai báo ngoàibeforeAllđểafterAlltruy cập được.afterAllclosesetupPage— không để leak qua worker kế tiếp.- Các test bên trong vẫn dùng fixture
pageriêng (test-scope, fresh mỗi test) — không dùngsetupPage. setupPagechỉ dùng cho setup, không share với test body.
Nếu cần pattern đơn giản hơn (không cần navigate trong beforeAll), dùng API calls hoặc database client trực tiếp mà không cần browser. Dùng browser trong beforeAll khi UI là cách duy nhất để setup dữ liệu.
Các Method Quan Trọng
browser.newContext(options?)
Tạo một BrowserContext mới — cô lập hoàn toàn (cookies, localStorage, session). Nhận cùng options với context fixture: storageState, viewport, userAgent, locale, v.v.
const context = await browser.newContext({
storageState: 'playwright/.auth/admin.json',
locale: 'en-US',
viewport: { width: 1280, height: 720 },
});
browser.newPage(options?)
Shortcut tạo một context mới + page mới trong context đó. Không cần gọi newContext() riêng:
// Tương đương browser.newContext().then(ctx => ctx.newPage())
const page = await browser.newPage();
await page.goto('/dashboard');
await page.close(); // close page cũng close context ẩn bên dưới
Dùng newPage khi chỉ cần 1 page đơn và không cần tham chiếu context. Dùng newContext khi cần nhiều page trong cùng session (cùng cookies).
browser.contexts()
Trả về mảng tất cả BrowserContext đang mở. Hữu ích để debug hoặc kiểm tra xem có context nào bị leak không:
const openContexts = browser.contexts();
console.log(`Open contexts: ${openContexts.length}`);
// Nếu con số này tăng qua các test → có leak
browser.isConnected()
Trả về boolean — browser process còn alive hay không. Ít dùng trong test thông thường, nhưng cần khi override fixture với connectOverCDP để kiểm tra connection trước khi dùng:
if (!browser.isConnected()) {
throw new Error('Browser disconnected unexpectedly');
}
browser.version()
Trả về version string của browser. Dùng để log hoặc kiểm tra version trong test matrix:
console.log(browser.version()); // vd: "131.0.6778.204" (Chromium)
browser.close()
Đóng browser process, cleanup tất cả context. Trong Test Runner mode, không gọi browser.close() trong test — fixture quản lý lifecycle này. Gọi thủ công chỉ khi override fixture.
Configure qua launchOptions
Fixture browser không nhận options trực tiếp khi inject. Thay vào đó, cấu hình browser thông qua option fixture launchOptions trong playwright.config.ts:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
launchOptions: {
headless: false, // chạy có UI — debug
slowMo: 500, // chậm mỗi action 500ms — debug
args: ['--disable-web-security'], // Chromium flag
},
},
});
launchOptions áp dụng ở cấp project. Nội dung của option này truyền thẳng vào chromium.launch(options) (hoặc firefox/webkit) bởi fixture infrastructure.
Override per-test bằng test.use():
test.describe('Debug group', () => {
test.use({
launchOptions: {
headless: false,
slowMo: 1000,
},
});
test('slow debug test', async ({ page }) => {
// test này chạy với headed + slowMo 1000ms
});
});
Quan trọng: launchOptions là option fixture (bài A.2 đề cập chi tiết), không phải custom fixture. Không nhầm với việc truyền options vào browser.newContext() — context options đặt trong contextOptions hoặc use: { ... }, không phải launchOptions.
Nội dung chi tiết về từng launchOptions (args, channel, executablePath, proxy) đã được đề cập trong Series Cơ Bản bài 408 (chromium.launch()). Bài này không lặp lại.
browser Trong Multi-Project Config
Khi config có nhiều project (Chromium, Firefox, WebKit), mỗi project chạy trên worker process riêng. Điều này có nghĩa:
- Worker xử lý project Chromium → fixture
browserlà Chromium instance. - Worker xử lý project Firefox → fixture
browserlà Firefox instance. - Hai worker không share
browser.
// playwright.config.ts
export default defineConfig({
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});
Kiểm tra tên browser hiện tại qua fixture browserName (bài 4 trong nhóm này) hoặc browser.browserType().name():
test('check browser type', async ({ browser }) => {
console.log(browser.browserType().name()); // 'chromium' | 'firefox' | 'webkit'
});
Một worker xử lý đúng 1 project tại 1 thời điểm — không có trường hợp 1 worker mix Chromium và Firefox.
Override browser Fixture (Rare)
Trong hầu hết trường hợp, không cần override fixture browser. Tuy nhiên, một số use case đặc biệt yêu cầu custom launch logic — ví dụ attach vào Chrome đang chạy qua CDP thay vì spawn fresh:
// fixtures.ts
import { test as base, Browser } from '@playwright/test';
export const test = base.extend<{}, { browser: Browser }>({
browser: [async ({ playwright }, use) => {
// Thay vì launch mới, attach vào CDP endpoint
const browser = await playwright.chromium.connectOverCDP('http://localhost:9222');
if (!browser.isConnected()) {
throw new Error('CDP connection failed — is Chrome running with --remote-debugging-port=9222?');
}
await use(browser);
await browser.close();
}, { scope: 'worker' }],
});
Chú ý khi override:
- Phải khai báo
scope: 'worker'để giữ worker-scope. - Phải gọi
browser.close()sauuse()— không còn fixture mặc định xử lý. - Nếu
connectOverCDPfail (port không mở, Chrome không khởi động), error xảy ra silently nếu không kiểm traisConnected().
Deep dive test.extend thuộc bài A.3 (Custom Fixtures). Bài này chỉ mô tả pattern ở mức nhận biết.
Pitfall Thường Gặp
Pitfall 1: Tạo context trong test, quên close — leak sang test sau
// SAI ❌ — context bị leak
test('test A', async ({ browser }) => {
const ctx = await browser.newContext();
const page = await ctx.newPage();
await page.goto('/');
// Thiếu ctx.close() → context tồn tại trong browser của worker
// Test sau có thể bị ảnh hưởng nếu context giữ background listeners
});
// ĐÚNG ✅
test('test A', async ({ browser }) => {
const ctx = await browser.newContext();
try {
const page = await ctx.newPage();
await page.goto('/');
} finally {
await ctx.close(); // luôn close, kể cả khi test fail
}
});
Verify số context đang mở sau mỗi test: browser.contexts().length phải trở về 0 sau khi test kết thúc.
Pitfall 2: Nhầm browser-scope với test-scope
// SAI ❌ — browser là worker-scope, context thay đổi trong test này
// có thể ảnh hưởng test sau nếu không cleanup
test('test B', async ({ browser }) => {
// Mở context, không close
const ctx = await browser.newContext();
await ctx.addCookies([{ name: 'admin', value: '1', domain: 'localhost', path: '/' }]);
// ctx vẫn tồn tại với cookie admin=1 sau khi test kết thúc
});
Trong khi đó, nếu dùng fixture context trực tiếp, context được tạo fresh và auto-close mỗi test — không có vấn đề này.
Pitfall 3: beforeAll quên close manual page → state leak
// SAI ❌ — setupPage không bao giờ được close
test.describe('Suite', () => {
test.beforeAll(async ({ browser }) => {
const setupPage = await browser.newPage();
await setupPage.goto('/setup');
// Thiếu afterAll để close setupPage
});
// setupPage tồn tại trong browser suốt phần còn lại của worker
// Worker kết thúc → browser.close() → OK, nhưng
// nếu setupPage có background request đang chạy → có thể gây noise
});
// ĐÚNG ✅
test.describe('Suite', () => {
let setupPage: Page;
test.beforeAll(async ({ browser }) => {
setupPage = await browser.newPage();
await setupPage.goto('/setup');
});
test.afterAll(async () => {
await setupPage.close(); // cleanup rõ ràng
});
});
Pitfall 4: Override browser fixture với connectOverCDP — silent error khi fail
// SAI ❌ — không kiểm tra connection
browser: [async ({ playwright }, use) => {
const browser = await playwright.chromium.connectOverCDP('http://localhost:9222');
// Nếu Chrome không chạy, connectOverCDP throw → test fail với lỗi khó đọc
await use(browser);
await browser.close();
}, { scope: 'worker' }],
// TỐT HƠN ✅ — thêm error message rõ ràng
browser: [async ({ playwright }, use) => {
let browser;
try {
browser = await playwright.chromium.connectOverCDP('http://localhost:9222');
} catch (err) {
throw new Error(
'Cannot connect to Chrome CDP endpoint. ' +
'Start Chrome with: google-chrome --remote-debugging-port=9222\n' +
`Original error: ${err}`
);
}
await use(browser);
await browser.close();
}, { scope: 'worker' }],
Tổng Kết
- Fixture
browserlà worker-scope: 1Browserinstance per worker, share giữa tất cả test trong worker. Đóng khi worker kết thúc — không phải khi mỗi test kết thúc. - Khác với
contextvàpage(test-scope): mỗi test nhận instance mới, auto-close sau test.browserkhông cô lập giữa test. - Dùng
browserkhi cần: (1) nhiều context trong 1 test (multi-user simulation), (2) navigate trongbeforeAll(test-scope fixture không khả dụng trongbeforeAll). - Context và page tạo từ
browsertrong test body phải close thủ công — fixture không track chúng. - Configure browser qua option fixture
launchOptionstrong config, không phải qua tham số fixture trực tiếp. - Trong multi-project config, mỗi project chạy trên worker riêng → mỗi worker có browser instance của project đó (Chromium / Firefox / WebKit).
- Override fixture
browserrất ít khi cần — chỉ dùng khi cần custom launch logic nhưconnectOverCDP.
Quiz
-
File test có 4 test, tất cả chạy trên cùng 1 worker. Fixture
browserđược khởi tạo bao nhiêu lần?Đáp án
1 lần. Fixture
browserlà worker-scope — 1 instance được tạo khi worker bắt đầu, reuse cho tất cả 4 test, đóng khi worker kết thúc. -
Đoạn code sau có vấn đề gì?
test.beforeAll(async ({ page }) => { await page.goto('/admin/seed'); });Đáp án
pagelà test-scope fixture, không khả dụng trongbeforeAll(worker-scope). TypeScript báo error. Cần dùng{ browser }và tự tạo page:const page = await browser.newPage(). -
Test tạo
const ctx = await browser.newContext()nhưng không gọictx.close()cuối test. Điều gì xảy ra?Đáp án
Context tồn tại trong browser process suốt phần còn lại của worker. Các test sau trong cùng worker có thể bị ảnh hưởng nếu context giữ background listeners, open connections, hoặc gây resource pressure. Context chỉ được cleanup khi worker kết thúc và browser đóng.
-
Cần viết 1 test kiểm tra user A gửi tin nhắn cho user B, cả 2 đã đăng nhập với session khác nhau. Approach nào đúng?
Đáp án
Dùng fixture
browser, tạo 2 context riêng vớibrowser.newContext({ storageState: '...' }). Không thể dùng fixturecontext(chỉ cung cấp 1 context) hay fixturepage(1 page trong 1 context). Close cả 2 context cuối test. -
Muốn cấu hình browser chạy có UI (
headless: false) cho toàn bộ test. Đặt option này ở đâu?Đáp án
Trong
playwright.config.tstạiuse: { launchOptions: { headless: false } }. Không thể truyền trực tiếp vào fixturebrowserkhi inject — fixture không nhận runtime options.
Bài Tiếp Theo
Bài 4: Built-in Fixture browserName — fixture string trả về tên browser hiện tại, cách dùng để branch logic và skip test theo browser.
