Danh sách bài viết

Bài 121: Network Throttling Qua Route & CDP

Simulate mạng chậm (Slow 3G, Fast 3G, offline) cho toàn bộ page bằng CDP Network.emulateNetworkConditions và context.setOffline. Bài phân biệt rõ per-route delay (bài 120) với CDP throttle toàn page, trình bày preset bandwidth chuẩn, pattern reset, offline PWA testing, pitfall Chromium-only, và 5 câu quiz.

28/05/2026
12 phút đọc
0 lượt xem
1

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.setOffline và 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.
2

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
3

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.

4

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ị: downloadThroughputuploadThroughput 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.

5

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).

6

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.setTimeoutexpect 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 });
});
7

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.

8

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.

9

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 });
});
10

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.

11

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.

12

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.

13

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.

14

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,
});
15

Tổng Kết

  • CDP Network.emulateNetworkConditions throttle toàn bộ traffic của page (HTML, JS, CSS, API) — khác per-route delay chỉ ảnh hưởng route match.
  • downloadThroughputuploadThroughput nhận bytes/s — chuyển đổi từ kbps bằng công thức kbps * 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: 0 trong afterEach.
  • context.setOffline là 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.
16

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.

17

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.

Bài 122: Conditional Mocking Với Predicate