Mục lục
- Mục Tiêu Bài Học
- Per-Route Delay Vs. CDP Throttle
- CDP Session Là Gì?
- Network.emulateNetworkConditions — Cú Pháp Cơ Bản
- Preset Bandwidth Chuẩn
- Test Loading UX Trên Slow 3G
- Test Timeout Trên Mạng Chậm
- Test Performance Budget (LCP/FCP)
- Offline Simulation Với context.setOffline
- Offline Simulation Với CDP
- setOffline Vs. CDP Offline — So Sánh
- Pattern Reset Throttle
- CPU Throttling (Mention Qua)
- Common Pitfalls
- Tổng Kết
- Bài Tập Củng Cố
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau khi hoàn thành bài này, bạn sẽ:
- Phân biệt per-route delay (bài 120) với CDP bandwidth throttle.
- Tạo CDP session và gọi
Network.emulateNetworkConditionsđể throttle toàn page. - Dùng preset Slow 3G, Fast 3G, offline đúng đơn vị bytes/s.
- Test loading UX (skeleton, spinner, progressive load) trên mạng chậm.
- Simulate offline bằng
context.setOfflinevà biết khi nào dùng CDP thay thế. - Reset throttle đúng cách sau mỗi test.
- Tránh 4 pitfall phổ biến nhất với CDP throttle.
Per-Route Delay Vs. CDP Throttle
Bài 120 dùng page.route() + setTimeout để inject delay vào từng API endpoint — phù hợp khi muốn test loading state của một route cụ thể. Cách này không ảnh hưởng JS bundle, CSS, hay ảnh.
CDP throttle hoạt động ở tầng thấp hơn: toàn bộ traffic của page — HTML, JS, CSS, API, WebSocket — đều bị giới hạn bandwidth và tăng latency. Đây là cách gần với thực tế người dùng trên mạng kém nhất.
| Đặc điểm | per-route delay (bài 120) | CDP throttle (bài này) |
|---|---|---|
| Phạm vi | Chọn lọc theo pattern | Toàn bộ network của page |
| Loại mô phỏng | Delay time thuần | Bandwidth (bytes/s) + latency (RTT) |
| Static assets | Không bị ảnh hưởng | Bị ảnh hưởng (JS, CSS, ảnh chậm) |
| Trình duyệt | Chromium, Firefox, WebKit | Chromium only |
| Độ phức tạp | Vài dòng | Cần CDP session |
| Dùng khi | Test loading state của API cụ thể | Test toàn bộ app trên mạng yếu |
CDP Session Là Gì?
CDP (Chrome DevTools Protocol) là giao thức giao tiếp trực tiếp với Chromium. Playwright expose CDP thông qua context.newCDPSession(page) — trả về một CDPSession object, cho phép gọi bất kỳ CDP domain nào (Network, Emulation, Performance, ...).
CDP session được tạo per-page, không phải per-context:
// Tạo CDP session cho page cụ thể
const client = await page.context().newCDPSession(page);
// Gọi CDP command
await client.send('Network.emulateNetworkConditions', { ... });
// Lắng nghe CDP event
client.on('Network.responseReceived', (params) => {
console.log(params.response.url);
});
Nếu test cần session cho nhiều page, tạo session riêng cho mỗi page. CDP session không share giữa các page.
Network.emulateNetworkConditions — Cú Pháp Cơ Bản
Network.emulateNetworkConditions nhận 4 field bắt buộc:
const client = await page.context().newCDPSession(page);
await client.send('Network.emulateNetworkConditions', {
offline: false,
downloadThroughput: 750 * 1024 / 8, // 750 kbps → bytes/s
uploadThroughput: 250 * 1024 / 8, // 250 kbps → bytes/s
latency: 100, // 100ms RTT
});
Điểm dễ nhầm nhất là đơn vị: downloadThroughput và uploadThroughput nhận bytes/s, không phải bits/s. Bandwidth thường đo bằng kbps (kilobits/s), nên công thức chuyển đổi là:
// kbps → bytes/s
const bytesPerSec = kbps * 1024 / 8;
// Ví dụ: 400 kbps
const slow3gDown = 400 * 1024 / 8; // = 51200 bytes/s
Field latency là RTT tính bằng milliseconds — thêm vào mỗi request trên mạng. RTT 400ms nghĩa là ngay cả packet nhỏ nhất cũng mất 400ms trước khi server nhận được.
Preset Bandwidth Chuẩn
Chrome DevTools dùng các preset sau (tương ứng với Network panel throttle). Có thể dùng làm reference:
const networkPresets = {
'Slow 3G': {
offline: false,
downloadThroughput: 400 * 1024 / 8, // 400 kbps
uploadThroughput: 400 * 1024 / 8, // 400 kbps
latency: 400, // 400ms RTT
},
'Fast 3G': {
offline: false,
downloadThroughput: 1.5 * 1024 * 1024 / 8, // 1.5 Mbps
uploadThroughput: 750 * 1024 / 8, // 750 kbps
latency: 150, // 150ms RTT
},
'Slow WiFi': {
offline: false,
downloadThroughput: 2 * 1024 * 1024 / 8, // 2 Mbps
uploadThroughput: 1 * 1024 * 1024 / 8, // 1 Mbps
latency: 40, // 40ms RTT
},
'Offline': {
offline: true,
downloadThroughput: 0,
uploadThroughput: 0,
latency: 0,
},
} as const;
type NetworkPreset = keyof typeof networkPresets;
// Helper dùng trong test
async function applyNetworkPreset(page: Page, preset: NetworkPreset) {
const client = await page.context().newCDPSession(page);
await client.send('Network.emulateNetworkConditions', networkPresets[preset]);
return client; // Trả về client để reset sau
}
Giữ lại client từ helper để có thể reset throttle sau test (xem bước 12).
Test Loading UX Trên Slow 3G
Với CDP throttle, không chỉ API mà cả JS bundle và CSS cũng tải chậm — đây là điều kiện gần thực tế hơn khi user truy cập trên 3G. Test pattern kiểm tra skeleton loader xuất hiện trong lúc page tải:
test('progressive load on Slow 3G', async ({ page }) => {
// Throttle TRƯỚC khi goto — để ngay cả HTML/JS cũng chậm
const client = await page.context().newCDPSession(page);
await client.send('Network.emulateNetworkConditions', {
offline: false,
downloadThroughput: 400 * 1024 / 8, // 400 kbps
uploadThroughput: 400 * 1024 / 8,
latency: 400,
});
await page.goto('/');
// Skeleton visible trong khi content tải
await expect(page.getByTestId('skeleton')).toBeVisible();
// Content xuất hiện sau khi tải xong (timeout lớn hơn do mạng chậm)
await expect(page.getByTestId('main-content')).toBeVisible({ timeout: 30_000 });
await expect(page.getByTestId('skeleton')).toBeHidden({ timeout: 30_000 });
});
Lưu ý test timeout: Với Slow 3G (400 kbps), một page JS bundle 500KB mất khoảng 10 giây chỉ để download. Đặt test.setTimeout và expect timeout đủ lớn:
test('skeleton on slow network', async ({ page }) => {
test.setTimeout(60_000); // Tăng test timeout
// ...
});
Test spinner với progressive load — kiểm tra từng phase load:
test('shows loading phases on Slow 3G', async ({ page }) => {
test.setTimeout(60_000);
const client = await page.context().newCDPSession(page);
await client.send('Network.emulateNetworkConditions', {
offline: false,
downloadThroughput: 400 * 1024 / 8,
uploadThroughput: 400 * 1024 / 8,
latency: 400,
});
await page.goto('/dashboard');
// Phase 1: global spinner khi chưa có JS
await expect(page.getByTestId('global-spinner')).toBeVisible();
// Phase 2: skeleton sau khi React mount
await expect(page.getByTestId('skeleton-card')).toBeVisible({ timeout: 15_000 });
// Phase 3: content thật
await expect(page.getByTestId('dashboard-content')).toBeVisible({ timeout: 30_000 });
});
Test Timeout Trên Mạng Chậm
Kết hợp CDP throttle với route abort để test app xử lý timeout: throttle làm request chậm, route abort sau một khoảng thời gian giả lập timeout thật.
test('shows error when API times out on slow network', async ({ page }) => {
test.setTimeout(60_000);
// Throttle toàn page
const client = await page.context().newCDPSession(page);
await client.send('Network.emulateNetworkConditions', {
offline: false,
downloadThroughput: 400 * 1024 / 8,
uploadThroughput: 400 * 1024 / 8,
latency: 400,
});
// Route abort API sau 10s — giả lập server không respond
await page.route('**/api/critical-data', async (route) => {
await new Promise(r => setTimeout(r, 10_000));
await route.abort();
});
await page.goto('/');
// App phải hiển thị error message
await expect(page.getByRole('alert')).toBeVisible({ timeout: 20_000 });
await expect(page.getByText(/không thể tải/i)).toBeVisible({ timeout: 20_000 });
});
CDP throttle và per-route delay có thể dùng cùng nhau: throttle cung cấp realistic bandwidth cho toàn page, route handler xử lý behavior cụ thể của một endpoint.
Test Performance Budget (LCP/FCP)
Playwright có thể đo Web Vitals qua CDP Performance domain hoặc qua page.evaluate với PerformanceObserver. Kết hợp với throttle, bạn kiểm tra được LCP/FCP trên điều kiện mạng thực tế:
test('LCP under 5s on Slow 3G', async ({ page }) => {
test.setTimeout(60_000);
const client = await page.context().newCDPSession(page);
await client.send('Network.emulateNetworkConditions', {
offline: false,
downloadThroughput: 400 * 1024 / 8,
uploadThroughput: 400 * 1024 / 8,
latency: 400,
});
// Lắng nghe LCP qua PerformanceObserver trong page
const lcpPromise = page.evaluate(() => new Promise((resolve) => {
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
resolve(lastEntry.startTime);
}).observe({ type: 'largest-contentful-paint', buffered: true });
}));
await page.goto('/');
// Chờ page ổn định
await page.waitForLoadState('networkidle', { timeout: 45_000 });
const lcp = await lcpPromise;
// Budget: LCP phải dưới 5000ms trên Slow 3G
expect(lcp).toBeLessThan(5_000);
});
Loại test này thường chạy trong suite riêng (performance CI), không phải functional test hàng ngày — kết quả có thể biến động nhỏ giữa các lần chạy do đặc tính của throttle simulation.
Offline Simulation Với context.setOffline
context.setOffline(true) là API built-in của Playwright, hoạt động trên cả Chromium, Firefox, WebKit. Nó cắt toàn bộ network của context — không chỉ một page:
test('PWA shows offline page', async ({ page, context }) => {
await page.goto('/');
// Đảm bảo service worker đã cài
await page.waitForLoadState('networkidle');
// Chuyển sang offline
await context.setOffline(true);
// Navigate lại — service worker phải serve offline page
await page.goto('/');
await expect(page.getByText('No internet connection')).toBeVisible();
// Khôi phục
await context.setOffline(false);
});
Pattern kiểm tra offline fallback khi user đang dùng app rồi mất mạng:
test('shows offline banner when connection drops', async ({ page, context }) => {
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
// Mất mạng đột ngột
await context.setOffline(true);
// Trigger một action cần network
await page.getByRole('button', { name: 'Refresh data' }).click();
// App phải báo offline
await expect(page.getByTestId('offline-banner')).toBeVisible();
// Khôi phục mạng
await context.setOffline(false);
// Banner biến mất
await expect(page.getByTestId('offline-banner')).toBeHidden({ timeout: 5_000 });
});
Offline Simulation Với CDP
CDP cũng có thể set offline thông qua Network.emulateNetworkConditions với offline: true. Điều này hữu ích khi bạn muốn chuyển qua lại giữa throttle và offline trong cùng một CDP session:
test('transition from slow to offline', async ({ page }) => {
test.setTimeout(60_000);
const client = await page.context().newCDPSession(page);
// Bắt đầu với Slow 3G
await client.send('Network.emulateNetworkConditions', {
offline: false,
downloadThroughput: 400 * 1024 / 8,
uploadThroughput: 400 * 1024 / 8,
latency: 400,
});
await page.goto('/');
await expect(page.getByTestId('skeleton')).toBeVisible();
// Sau khi app load, cắt mạng hoàn toàn
await client.send('Network.emulateNetworkConditions', {
offline: true,
downloadThroughput: 0,
uploadThroughput: 0,
latency: 0,
});
await page.getByRole('button', { name: 'Load more' }).click();
await expect(page.getByTestId('offline-banner')).toBeVisible();
});
Dùng CDP cho offline khi cần chuyển trạng thái dynamic (slow → offline → normal) trong cùng test; dùng context.setOffline khi chỉ cần on/off đơn giản và cần cross-browser.
setOffline Vs. CDP Offline — So Sánh
| Đặc điểm | context.setOffline() | CDP offline: true |
|---|---|---|
| Trình duyệt hỗ trợ | Chromium, Firefox, WebKit | Chromium only |
| Phạm vi | Toàn context (tất cả page) | Per-page (page của CDP session) |
| Bandwidth control | Không (chỉ on/off) | Có (cùng emulateNetworkConditions) |
| Chuyển đổi dynamic | Gọi lại setOffline(false) | Gọi lại emulateNetworkConditions |
| Kết hợp throttle | Không (riêng biệt) | Có (same session) |
Nếu cần cross-browser test offline scenario, dùng context.setOffline. Nếu cần Chromium và cần chuyển qua lại giữa slow network và offline, dùng CDP.
Pattern Reset Throttle
CDP throttle không tự reset khi test kết thúc — session tồn tại suốt vòng đời page. Nếu không reset, test tiếp theo trong cùng worker có thể bị ảnh hưởng (tùy cấu hình Playwright reuse context hay không). Best practice là luôn reset:
// Reset về không throttle: downloadThroughput = -1, uploadThroughput = -1, latency = 0
await client.send('Network.emulateNetworkConditions', {
offline: false,
downloadThroughput: -1, // -1 = no throttle (unlimited)
uploadThroughput: -1,
latency: 0,
});
Giá trị -1 là sentinel value của CDP — nghĩa là "unlimited bandwidth". 0 trong latency nghĩa là không thêm latency nhân tạo.
Pattern dùng afterEach để đảm bảo reset luôn chạy kể cả khi test fail:
import { test, expect, Page } from '@playwright/test';
let cdpClient: Awaited> | null = null;
test.afterEach(async ({ page }) => {
if (cdpClient) {
await cdpClient.send('Network.emulateNetworkConditions', {
offline: false,
downloadThroughput: -1,
uploadThroughput: -1,
latency: 0,
});
cdpClient = null;
}
});
test('slow 3G test', async ({ page }) => {
cdpClient = await page.context().newCDPSession(page);
await cdpClient.send('Network.emulateNetworkConditions', {
offline: false,
downloadThroughput: 400 * 1024 / 8,
uploadThroughput: 400 * 1024 / 8,
latency: 400,
});
// ... test logic
});
Với context.setOffline, reset tương tự: await context.setOffline(false) trong afterEach.
CPU Throttling (Mention Qua)
Ngoài network, CDP còn cung cấp Emulation.setCPUThrottlingRate để giả lập CPU yếu — hữu ích khi test app trên thiết bị mobile mid-range:
// 4x slower CPU (giả lập mid-range mobile)
await client.send('Emulation.setCPUThrottlingRate', { rate: 4 });
// Reset CPU throttle
await client.send('Emulation.setCPUThrottlingRate', { rate: 1 });
rate: 1 là baseline (không throttle), rate: 4 là 4x chậm hơn. Thường dùng kết hợp với network throttle khi test performance trên điều kiện thiết bị thực tế.
CPU throttling sẽ được cover đầy đủ hơn trong chương Emulation Nâng Cao của series. Ở đây chỉ cần biết nó cùng CDP session với network throttle.
Common Pitfalls
Pitfall 1: Chạy CDP throttle trên Firefox / WebKit
CDP chỉ hoạt động với Chromium. Khi test chạy trên Firefox hoặc WebKit, context.newCDPSession(page) sẽ throw error hoặc trả về session không hỗ trợ Network.emulateNetworkConditions.
// BAD: Sẽ fail trên Firefox/WebKit
const client = await page.context().newCDPSession(page);
await client.send('Network.emulateNetworkConditions', { ... });
// GOOD: Skip trên non-Chromium
test('slow 3G loading', async ({ page, browserName }) => {
test.skip(browserName !== 'chromium', 'CDP throttle — Chromium only');
// ... CDP throttle logic
});
Nếu cần cross-browser network throttle, dùng context.setOffline cho offline và per-route delay (bài 120) cho slow API.
Pitfall 2: Quên reset throttle sau test
CDP throttle tồn tại suốt vòng đời page. Nếu không reset, test tiếp theo trên cùng page/context sẽ bị throttle — gây test chậm không rõ nguyên nhân, hoặc timeout ở những chỗ không ngờ.
// GOOD: Luôn reset trong afterEach
test.afterEach(async ({ page }) => {
const client = await page.context().newCDPSession(page);
await client.send('Network.emulateNetworkConditions', {
offline: false,
downloadThroughput: -1,
uploadThroughput: -1,
latency: 0,
});
});
Pitfall 3: Tính sai đơn vị bandwidth
CDP nhận bytes/s, nhưng bandwidth thường được biểu diễn bằng kbps hoặc Mbps (bits/s). Nhầm đơn vị dẫn đến throttle sai giá trị 8 lần.
// BAD: nhầm kbps với bytes/s
downloadThroughput: 400, // 400 bytes/s = ~3.2 kbps — quá chậm
// BAD: nhầm Mbps với bytes/s
downloadThroughput: 1.5, // 1.5 bytes/s — gần như offline
// GOOD: chuyển đổi đúng
downloadThroughput: 400 * 1024 / 8, // 400 kbps = 51200 bytes/s
downloadThroughput: 1.5 * 1024 * 1024 / 8, // 1.5 Mbps = 196608 bytes/s
Pitfall 4: Confuse setOffline với CDP bandwidth control
context.setOffline(true) chỉ on/off, không điều chỉnh bandwidth hay latency. Muốn simulate "mạng chậm nhưng không offline", phải dùng CDP emulateNetworkConditions. Dùng setOffline để test slow app là sai — app sẽ thấy network failure thật, không phải slow network.
// BAD: setOffline không phải slow network
await context.setOffline(true); // App thấy network error, không phải slow 3G
// GOOD: CDP cho slow network
await client.send('Network.emulateNetworkConditions', {
offline: false, // Không offline — chỉ slow
downloadThroughput: 400 * 1024 / 8,
uploadThroughput: 400 * 1024 / 8,
latency: 400,
});
Tổng Kết
- CDP
Network.emulateNetworkConditionsthrottle toàn bộ traffic của page (HTML, JS, CSS, API) — khác per-route delay chỉ ảnh hưởng route match. downloadThroughputvàuploadThroughputnhận bytes/s — chuyển đổi từ kbps bằng công thứckbps * 1024 / 8.- CDP chỉ hoạt động trên Chromium — luôn dùng
test.skip(browserName !== 'chromium', ...)để bảo vệ cross-browser test. - Reset throttle sau mỗi test bằng
downloadThroughput: -1, uploadThroughput: -1, latency: 0trongafterEach. context.setOfflinelà cách đơn giản nhất cho offline testing — cross-browser, phạm vi context-level, nhưng không có bandwidth control.- CDP offline và CDP throttle dùng cùng session — tiện để chuyển đổi dynamic trong một test.
Bài Tập Củng Cố
Câu 1
Đoạn code sau có lỗi gì?
const client = await page.context().newCDPSession(page);
await client.send('Network.emulateNetworkConditions', {
offline: false,
downloadThroughput: 400, // 400 kbps
uploadThroughput: 400,
latency: 400,
});
Đáp án
Sai đơn vị: downloadThroughput nhận bytes/s, không phải kbps. Giá trị 400 là 400 bytes/s ≈ 3.2 kbps — gần như offline, không phải Slow 3G. Đúng phải là 400 * 1024 / 8 = 51200 bytes/s. Tương tự uploadThroughput.
Câu 2
Test chạy ổn trên Chromium nhưng fail trên Firefox với error "CDP session not supported". Cách sửa?
Đáp án
CDP chỉ hỗ trợ Chromium. Thêm skip guard ở đầu test: test.skip(browserName !== 'chromium', 'CDP throttle — Chromium only');. Nếu cần cross-browser slow network test, thay CDP bằng per-route delay (bài 120) cho API hoặc context.setOffline cho offline scenario.
Câu 3
Team notice rằng test suite chạy chậm hẳn sau khi thêm một test CDP throttle, dù test đó đã pass. Nguyên nhân và cách khắc phục?
Đáp án
CDP throttle không tự reset sau test. Test tiếp theo trên cùng page/context (nếu Playwright reuse context) bị throttle theo. Khắc phục: thêm test.afterEach reset throttle với downloadThroughput: -1, uploadThroughput: -1, latency: 0.
Câu 4
Bạn muốn test app hiển thị offline banner khi user đang dùng app và mất mạng đột ngột. Nên dùng context.setOffline hay CDP offline? Tại sao?
Đáp án
Cả hai đều được, nhưng context.setOffline là lựa chọn đơn giản hơn vì: cross-browser, ít code hơn, không cần CDP session. Dùng CDP offline khi test đó đã có CDP session cho throttle trước đó — tránh tạo thêm session. Nếu test cần chuyển từ Slow 3G → Offline trong cùng test, CDP là lựa chọn tự nhiên hơn vì dùng cùng session.
Câu 5
Test sau sẽ pass hay fail? Giải thích.
test('offline UX', async ({ page, context }) => {
await context.setOffline(true);
await page.goto('/');
// App có service worker và offline page tại '/offline.html'
await expect(page.getByText('You are offline')).toBeVisible();
});
Đáp án
Có thể fail vì service worker cần được cài đặt trước khi set offline. Service worker chỉ active sau lần first visit (và đôi khi cần reload). Nếu setOffline(true) trước goto, browser không thể load trang ban đầu để cài service worker. Đúng pattern: (1) goto('/') và chờ networkidle để service worker cài xong, (2) rồi mới setOffline(true), (3) rồi navigate lại để test offline page.
Bài Tiếp Theo
Bài 122 chuyển sang conditional mocking — dùng predicate function để quyết định có intercept request hay không dựa trên header, body, hay query string.
