Mục lục
- Mục Tiêu Bài Học
- Option Fixture Là Gì
baseURLLà Option Fixture- Scope Hierarchy — Project → File → Describe
- Inject
baseURLVào Test Signature - Phối Hợp Với
requestFixture - Env-Driven Pattern
- Multi-Tenant Pattern
- Multi-Project Per Env
- Annotation baseURL Vào Trace
- URL Absolute Vs Relative — Recap Nhanh
- 4 Pitfall Thực Tế
- Quiz
Mục Tiêu Bài Học
- Hiểu khái niệm Option Fixture — fixture có thể configure qua
use: { ... }ở mọi scope (project, file, describe). - Phân biệt bài này với Series Cơ Bản bài 216: bài đó đề cập
baseURLnhư config; bài này đề cậpbaseURLnhư một fixture inject được. - Nắm scope hierarchy của
test.use({ baseURL }): file-level override project config, describe-level override file-level. - Biết cách inject
baseURLvào test signature để đọc giá trị runtime, kiểm tra môi trường đang chạy, gắn annotation vào trace. - Hiểu cả
page.goto('/path')vàrequest.get('/api/path')đều resolve cùngbaseURL. - Áp dụng được env-driven pattern, multi-tenant pattern, multi-project pattern vào project thực.
- Tránh 4 pitfall phổ biến khi override
baseURLtại các scope khác nhau.
Option Fixture Là Gì
Playwright phân loại fixtures thành nhiều nhóm. Nhóm Built-in Fixtures (A.1) cung cấp object như page, context, browser, request, browserName — mỗi fixture là một instance sẵn dùng, không cần config thêm để hoạt động.
Nhóm Option Fixtures (A.2) khác ở điểm cốt lõi: chúng không phải object phức tạp mà là giá trị cấu hình ảnh hưởng đến cách Playwright khởi tạo built-in fixtures (browser context, page, API request context). Option fixture được khai báo trong block use: { ... } của config hoặc qua test.use({ ... }) trong file test.
Danh sách option fixtures bao gồm baseURL, viewport, headless, locale, colorScheme, geolocation, permissions, storageState, extraHTTPHeaders, v.v. Bài này tập trung vào baseURL.
Điểm phân biệt quan trọng: option fixture có thể inject vào test signature giống như built-in fixture. Ví dụ, không chỉ có ({ page }) mà còn có ({ page, baseURL }) để đọc giá trị baseURL đang áp dụng cho test đó.
baseURL Là Option Fixture
baseURL trong Playwright có kiểu string | undefined. Khi được set, nó trở thành URL gốc được các API navigation và request dùng để resolve relative path.
Series Cơ Bản bài 216 đã đề cập cách khai báo baseURL ở top-level config. Bài này tập trung vào khía cạnh khác: baseURL là fixture, có nghĩa là nó tham gia vào hệ thống dependency injection của Playwright Test.
Điều này dẫn đến hai khả năng mà config đơn thuần không có:
- Override tại bất kỳ scope nào — không cần sửa config, chỉ cần gọi
test.use({ baseURL: '...' })trong file hoặc describe block. - Inject vào test để đọc runtime —
async ({ page, baseURL }) => { ... }cho phép test biết mình đang chạy trênbaseURLnào.
Cơ chế hoạt động: khi Playwright tạo BrowserContext và APIRequestContext cho mỗi test, nó đọc giá trị baseURL từ fixture scope hiện tại (đã merge qua hierarchy) và truyền vào constructor. Do đó, cả page lẫn request trong cùng test đều dùng chung một baseURL.
Scope Hierarchy — Project → File → Describe
baseURL có thể được set và override tại ba scope theo thứ tự ưu tiên tăng dần:
Scope 1 — Project Config (ưu tiên thấp nhất)
// playwright.config.ts
export default defineConfig({
use: { baseURL: 'https://staging.app.com' },
});
Áp dụng cho mọi test trong mọi file, trừ khi bị override ở scope cao hơn.
Scope 2 — File Level
// admin.spec.ts
import { test } from '@playwright/test';
// Override cho toàn bộ file này
test.use({ baseURL: 'https://admin.app.com' });
test('admin login', async ({ page }) => {
await page.goto('/login'); // https://admin.app.com/login
});
test('admin dashboard', async ({ page }) => {
await page.goto('/dashboard'); // https://admin.app.com/dashboard
});
test.use() ở top-level file (ngoài describe) áp dụng cho mọi test trong file đó, ghi đè giá trị từ project config.
Scope 3 — Describe Level (ưu tiên cao nhất)
// multi-tenant.spec.ts
import { test } from '@playwright/test';
test.describe('Tenant A', () => {
test.use({ baseURL: 'https://tenant-a.app.com' });
test('home page', async ({ page }) => {
await page.goto('/'); // https://tenant-a.app.com/
});
});
test.describe('Tenant B', () => {
test.use({ baseURL: 'https://tenant-b.app.com' });
test('home page', async ({ page }) => {
await page.goto('/'); // https://tenant-b.app.com/
});
});
// Test ngoài describe — dùng project config baseURL
test('shared infra', async ({ page }) => {
await page.goto('/status'); // dùng baseURL từ playwright.config.ts
});
Describe-level override chỉ áp dụng trong phạm vi describe block đó. Các describe lồng nhau (nested) kế thừa scope của describe cha, trừ khi tự override lại.
Bảng Tóm Tắt Scope
| Scope | Cách khai báo | Phạm vi áp dụng | Ưu tiên |
|---|---|---|---|
| Project config | defineConfig({ use: { baseURL } }) |
Mọi test trong project | Thấp nhất |
| File level | test.use({ baseURL }) ngoài describe |
Mọi test trong file | Trung bình |
| Describe level | test.use({ baseURL }) trong describe |
Các test trong describe block đó | Cao nhất |
Không có test-level test.use() — test.use() chỉ hoạt động ở file hoặc describe scope. Nếu cần URL khác nhau cho từng test riêng lẻ, dùng pattern inject baseURL và xây URL thủ công trong test (xem mục 5).
Inject baseURL Vào Test Signature
baseURL là fixture — có thể đặt tên nó trong destructuring của test function, giống page hay request.
test('verify environment', async ({ page, baseURL }) => {
// baseURL có kiểu: string | undefined
console.log('Running against:', baseURL);
// Kiểm tra đang chạy đúng môi trường
expect(baseURL).toContain('staging');
await page.goto('/');
});
TypeScript type của baseURL fixture là string | undefined. Cần guard nếu dùng string operation:
test('build custom URL', async ({ page, baseURL }) => {
if (!baseURL) throw new Error('baseURL is not configured');
// Xây URL thủ công khi cần nhiều segment động
const url = new URL('/users/42/settings', baseURL);
url.searchParams.set('tab', 'security');
await page.goto(url.toString());
// Navigate: baseURL + /users/42/settings?tab=security
});
Dùng Trong beforeEach Để Log Môi Trường
test.beforeEach(async ({ baseURL }) => {
test.info().annotations.push({
type: 'environment',
description: baseURL ?? 'unset',
});
});
Annotation này xuất hiện trong Playwright HTML report và trace viewer, giúp debug khi xem lại report sau khi CI chạy.
Pattern Assert Môi Trường Trước Khi Chạy
// shared-setup.ts — dùng chung qua test.extend hoặc beforeAll
test.beforeAll(async ({ baseURL }) => {
// Fail sớm nếu baseURL không khớp môi trường mong muốn
if (process.env.EXPECTED_ENV === 'staging') {
expect(baseURL, 'baseURL must point to staging').toContain('staging');
}
});
Pattern này bảo vệ khỏi tình huống CI set sai BASE_URL mà test vẫn chạy âm thầm trên môi trường sai.
Phối Hợp Với request Fixture
request fixture (APIRequestContext) và page fixture đều đọc baseURL từ cùng một nguồn — option fixture scope hiện tại. Do đó, khi override baseURL tại file hoặc describe level, cả page.goto('/path') lẫn request.get('/api/path') đều dùng chung URL gốc đó.
// api-integration.spec.ts
test.use({ baseURL: 'https://staging.app.com' });
test('seed user via API then test UI', async ({ page, request }) => {
// request.post dùng baseURL → POST https://staging.app.com/api/users
const res = await request.post('/api/users', {
data: { email: '[email protected]', role: 'viewer' },
});
expect(res.ok()).toBeTruthy();
const { id } = await res.json();
// page.goto dùng cùng baseURL → GET https://staging.app.com/users/id
await page.goto(`/users/${id}`);
await expect(page.getByRole('heading', { name: '[email protected]' })).toBeVisible();
});
Tính nhất quán này loại bỏ class bug "API call đến staging, UI navigate đến prod" — hai fixture cùng nhận baseURL từ một nguồn, không thể lệch nhau.
Trường Hợp Cần Tách baseURL
Một số kiến trúc có API gateway ở URL khác với frontend (vd https://api.app.com vs https://app.com). Trong trường hợp đó, không thể dùng chung một baseURL:
const BASE_UI = process.env.BASE_UI ?? 'https://app.com';
const BASE_API = process.env.BASE_API ?? 'https://api.app.com';
// baseURL cho page (UI)
test.use({ baseURL: BASE_UI });
test('call separate API domain', async ({ page, request }) => {
// page.goto dùng BASE_UI
await page.goto('/login');
// request phải dùng absolute URL vì domain khác
const res = await request.post(`${BASE_API}/auth/login`, {
data: { email: '[email protected]', password: 'pass' },
});
expect(res.ok()).toBeTruthy();
});
Khi API và UI ở domain khác, đặt baseURL = UI domain, còn API call dùng absolute URL với biến môi trường riêng.
Env-Driven Pattern
Pattern cơ bản (process.env.BASE_URL || 'http://localhost:3000') đã được trình bày ở Series Cơ Bản. Mục này tập trung vào các pattern nâng cao hơn.
Pattern 1 — Env Var Per File Override
Một số file test nhắm vào domain khác (vd admin panel). Thay vì sửa config, đọc env var khác trong file đó:
// admin.spec.ts
import { test } from '@playwright/test';
// ADMIN_URL có thể khác BASE_URL
const adminBase = process.env.ADMIN_URL ?? process.env.BASE_URL ?? 'http://localhost:3001';
test.use({ baseURL: adminBase });
test('admin can manage users', async ({ page }) => {
await page.goto('/users'); // adminBase + /users
});
Pattern 2 — Runtime Env Detection
Phát hiện môi trường đang chạy và set baseURL tương ứng trong config:
// playwright.config.ts
const env = process.env.TEST_ENV ?? 'local';
const BASE_URLS: Record<string, string> = {
local: 'http://localhost:3000',
staging: 'https://staging.app.com',
prod: 'https://app.com',
};
const baseURL = process.env.BASE_URL ?? BASE_URLS[env];
if (!baseURL) {
throw new Error(`Unknown TEST_ENV: ${env}. Valid: ${Object.keys(BASE_URLS).join(', ')}`);
}
export default defineConfig({
use: { baseURL },
});
# Chạy staging
TEST_ENV=staging npx playwright test
# Hoặc override hoàn toàn bằng URL bất kỳ
BASE_URL=https://pr-456.preview.app.com npx playwright test
Pattern 3 — Validate baseURL Format Trước Khi Chạy
// playwright.config.ts
const rawBase = process.env.BASE_URL ?? 'http://localhost:3000';
// Kiểm tra có scheme hợp lệ
try {
const parsed = new URL(rawBase);
if (!['http:', 'https:'].includes(parsed.protocol)) {
throw new Error(`baseURL must use http or https, got: ${parsed.protocol}`);
}
} catch {
throw new Error(`Invalid BASE_URL: "${rawBase}". Must be a valid URL with http/https scheme.`);
}
export default defineConfig({
use: { baseURL: rawBase },
});
Validate sớm trong config file giúp báo lỗi rõ ràng ngay khi Playwright load config, thay vì test fail với message không liên quan.
Multi-Tenant Pattern
SaaS app multi-tenant thường có mỗi tenant ở subdomain riêng (acme.app.com, globex.app.com). Cần test tính năng giống nhau nhưng trên URL khác nhau. Describe-level baseURL cho phép làm điều này trong một file duy nhất:
// tenant-isolation.spec.ts
import { test, expect } from '@playwright/test';
const tenants = [
{ name: 'Acme', baseURL: 'https://acme.app.com' },
{ name: 'Globex', baseURL: 'https://globex.app.com' },
{ name: 'Initech', baseURL: 'https://initech.app.com' },
] as const;
for (const tenant of tenants) {
test.describe(`Tenant: ${tenant.name}`, () => {
test.use({ baseURL: tenant.baseURL });
test('login page accessible', async ({ page }) => {
await page.goto('/login');
await expect(page.getByRole('heading', { name: 'Sign in' })).toBeVisible();
});
test('branding matches tenant', async ({ page, baseURL }) => {
await page.goto('/');
// Kiểm tra tenant name xuất hiện trong tiêu đề
const title = await page.title();
expect(title.toLowerCase()).toContain(tenant.name.toLowerCase());
// Log cho trace
test.info().annotations.push({ type: 'tenant', description: baseURL ?? '' });
});
});
}
Vòng lặp for...of tạo một describe block per tenant. Playwright chạy tất cả describe song song (nếu fullyParallel: true) hoặc tuần tự. Mỗi describe có baseURL độc lập.
Multi-Domain Trong Cùng Tenant
Trường hợp tenant có nhiều subdomain cần test cùng nhau (login tại auth.app.com, app tại app.app.com): baseURL đơn không đủ, cần URL absolute cho domain khác:
test.describe('Auth flow cross-domain', () => {
test.use({ baseURL: 'https://app.acme.com' });
test('login via auth domain then redirect to app', async ({ page }) => {
// Đến auth domain — absolute URL vì khác baseURL
await page.goto('https://auth.acme.com/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill('secret');
await page.getByRole('button', { name: 'Sign in' }).click();
// Sau khi login, redirect về app domain (baseURL)
await page.waitForURL('/dashboard');
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
});
Multi-Project Per Env
Khi cần chạy cùng bộ test trên nhiều môi trường trong một lần npx playwright test, cấu trúc projects thay thế cho env var:
// playwright.config.ts
export default defineConfig({
projects: [
{
name: 'staging',
use: { baseURL: 'https://staging.app.com' },
},
{
name: 'prod',
use: { baseURL: 'https://app.com' },
// Chỉ chạy test có tag @smoke khi chạy prod
grep: /@smoke/,
},
],
});
# Chạy chỉ staging
npx playwright test --project=staging
# Chạy chỉ prod (chỉ test @smoke vì config grep)
npx playwright test --project=prod
# Chạy tất cả projects (staging full + prod smoke)
npx playwright test
Mỗi project chạy với baseURL riêng. Test inject baseURL sẽ nhận giá trị của project đang chạy:
test('smoke: home page loads @smoke', async ({ page, baseURL }) => {
await page.goto('/');
// Test biết đang chạy trên staging hay prod
test.info().annotations.push({ type: 'baseURL', description: baseURL ?? '' });
await expect(page).toHaveTitle(/My App/);
});
Lợi thế so với env-driven: có thể chạy song song staging và prod trong một pipeline, report tách biệt per project.
Annotation baseURL Vào Trace
Trace viewer và HTML report của Playwright hiển thị annotations. Gắn baseURL vào annotation giúp debug report sau CI dễ hơn: nhìn vào test fail ngay thấy "đang chạy trên môi trường nào".
// Trong beforeEach — áp dụng cho mọi test trong file
test.beforeEach(async ({ baseURL }) => {
test.info().annotations.push({
type: 'baseURL',
description: baseURL ?? 'unset',
});
});
Ngoài ra, test.info() có field project chứa project config — có thể kết hợp để annotation đầy đủ hơn:
test.beforeEach(async ({ baseURL }) => {
const info = test.info();
info.annotations.push({
type: 'environment',
description: [
`project: ${info.project.name}`,
`baseURL: ${baseURL ?? 'unset'}`,
].join(' | '),
});
});
Annotation có thể kết hợp với custom reporter (xem bài nhóm Reporters) để tự động alert khi test fail trên prod environment.
URL Absolute Vs Relative — Recap Nhanh
Một vài điểm cần nhớ về cách baseURL resolve URL — nhanh, không lặp lại chi tiết Series Cơ Bản:
| URL truyền vào | baseURL | Kết quả |
|---|---|---|
'/login' |
https://staging.app.com |
https://staging.app.com/login |
'https://other.com/x' |
https://staging.app.com |
https://other.com/x (baseURL bị bỏ qua) |
'login' (không có /) |
https://staging.app.com |
https://staging.app.com/login (thêm / vào origin) |
'login' (không có /) |
https://staging.app.com/admin/ |
https://staging.app.com/admin/login (ghép tiếp path baseURL) |
Khuyến nghị: luôn dùng path bắt đầu bằng '/' để hành vi predictable, không phụ thuộc trailing slash của baseURL.
Validation: baseURL phải có scheme (http:// hoặc https://). Nếu không có scheme, Playwright throw lỗi khi khởi tạo context.
4 Pitfall Thực Tế
Pitfall 1 — Quên / Đầu Relative URL
Khi baseURL là origin đơn giản (https://app.com), page.goto('login') (không có /) vẫn resolve đúng thành https://app.com/login. Nhưng khi baseURL có path phụ (https://app.com/admin/), page.goto('users') resolve thành https://app.com/admin/users — có thể đúng hoặc sai tùy ý đồ. Nếu sau đó baseURL thay đổi từ https://app.com/admin/ thành https://app.com, test bỗng resolve sai:
// Không ổn — hành vi phụ thuộc trailing path của baseURL
await page.goto('users');
// Ổn — predictable với mọi dạng baseURL
await page.goto('/users');
Pitfall 2 — baseURL Không Có Scheme
Truyền baseURL: 'staging.app.com' (không có https://) → Playwright throw khi tạo context:
// Sai — thiếu scheme
test.use({ baseURL: 'staging.app.com' });
// → Error: browserContext.newPage: Protocol error: net::ERR_INVALID_URL
// Đúng
test.use({ baseURL: 'https://staging.app.com' });
Lỗi này thường xảy ra khi đọc từ env var bị set sai, hoặc strip scheme khi copy URL từ dashboard. Pattern validate new URL(rawBase) trong config (mục 7) bắt được lỗi này sớm hơn.
Pitfall 3 — Nhầm Scope File Vs Describe
Gọi test.use() bên trong hàm test thay vì ở describe scope sẽ không có tác dụng — Playwright chỉ đọc test.use() ở top-level scope lúc load file, không phải runtime:
test('wrong scope', async ({ page }) => {
test.use({ baseURL: 'https://other.com' }); // KHÔNG có tác dụng
await page.goto('/');
// page.goto vẫn dùng baseURL từ config/file/describe scope
});
Nếu cần URL động per-test, inject baseURL fixture và dùng new URL(path, baseURL) với absolute URL tự xây.
Pitfall 4 — Test Staging Nhưng baseURL Trỏ Prod
Test write data (tạo user, đặt hàng, gửi email) chạy trên môi trường sai gây hậu quả thật. Không có cảnh báo nào từ Playwright khi URL absolute khớp production:
// playwright.config.ts — có thể do nhầm
use: { baseURL: 'https://app.com' }, // prod!
// test/checkout.spec.ts
test('place order', async ({ page }) => {
await page.goto('/checkout');
// → Đang order thật trên prod
});
Khắc phục:
- Inject
baseURLvà assert trongbeforeAllrằng URL không chứa production domain khi chạy trong CI staging. - Thêm CI job config rõ ràng: staging job set
BASE_URL=staging, không dùng default config prod. - Với test write data, thêm guard
test.skip(baseURL?.includes('app.com') && !baseURL.includes('staging'), 'Skip write tests on prod').
Quiz
Câu 1
Project config có use: { baseURL: 'https://staging.app.com' }. File test có test.use({ baseURL: 'https://prod.app.com' }). Một describe block trong file đó có test.use({ baseURL: 'https://admin.app.com' }). Một test trong describe đó gọi await page.goto('/login'). URL thực tế là gì?
https://staging.app.com/loginhttps://prod.app.com/loginhttps://admin.app.com/login- Throw lỗi vì có nhiều
baseURL
Đáp án
C. Scope describe-level có ưu tiên cao nhất — override cả file-level và project config. Playwright dùng scope gần nhất (innermost) khi merge fixture options. A sai (project config bị file override). B sai (file bị describe override). D sai — không có conflict, có hierarchy.
Câu 2
Gọi test.use({ baseURL: 'https://other.com' }) bên trong hàm test('name', async ({ page }) => { ... }). Kết quả?
- Override baseURL cho test này
- Không có tác dụng —
test.use()chỉ hoạt động ở describe/file scope - Throw error vì gọi
test.use()sai nơi - Override baseURL cho tất cả test sau đó trong file
Đáp án
B. test.use() phải được gọi ở top-level file hoặc trong body của test.describe(), không phải bên trong hàm test. Playwright đọc các test.use() khi parse file, không phải runtime. Không throw error nhưng hoàn toàn không có tác dụng.
Câu 3
Trong một test có ({ page, request, baseURL }), page.goto('/api') và request.get('/api') dùng cùng baseURL không?
- Không —
pagevàrequestcóbaseURLriêng - Có — cả hai đọc
baseURLtừ cùng option fixture scope - Chỉ
pagedùngbaseURL,requestkhông hỗ trợ - Phụ thuộc vào cách khai báo trong config
Đáp án
B. Cả BrowserContext (tạo ra page) và APIRequestContext (tạo ra request) đều được khởi tạo với cùng giá trị baseURL từ option fixture. A sai (chung một nguồn). C sai (request cũng hỗ trợ baseURL — đây là điểm mạnh của pattern hybrid UI+API test). D sai (không phụ thuộc cách khai báo trong config, chỉ phụ thuộc scope hiện tại).
Câu 4
Muốn test hai tenant (https://acme.app.com và https://globex.app.com) với cùng test cases, cách nào phù hợp nhất?
- Tạo hai file test riêng, mỗi file một
test.use({ baseURL }) - Vòng lặp
for...oftạotest.describevớitest.use({ baseURL })per tenant - Set
BASE_URLenv var và chạynpx playwright testhai lần - Dùng hai
projectstrong config
Đáp án
B và D đều đúng, tuỳ context. B phù hợp khi muốn cùng file, chạy song song trong một lần, report gộp. D phù hợp khi muốn tách project để run chọn lọc (--project=acme), hoặc có các config khác nhau ngoài baseURL. A dư thừa (lặp code). C không tiện với nhiều tenant. Nếu chỉ được chọn một: B cho multi-tenant trong cùng file là pattern phổ biến nhất.
Câu 5
Inject baseURL vào test signature. TypeScript type của nó là gì?
stringURLstring | undefinedstring | null
Đáp án
C. Type là string | undefined vì baseURL không có default — nếu không được config ở bất kỳ scope nào, giá trị là undefined. Cần guard (if (!baseURL) hoặc baseURL ?? 'fallback') trước khi dùng string operation. A sai (thiếu undefined). B sai (không phải URL object). D sai (null không phải type của Playwright fixture — Playwright dùng undefined cho fixture không được set).
