Danh sách bài viết

Bài 333: page.on('worker') — Listener

page.on('worker') fire mỗi khi page spawn một dedicated Web Worker mới. Bài trình bày cú pháp đăng ký listener, các use case log/inspect/hook worker, pattern wait-for-worker dùng Promise.all, pattern filter theo URL, pattern đếm worker, worker close event, phân biệt với context.on('serviceworker'), pattern listen across pages, pitfall thường gặp và quiz củng cố.

15/06/2026
11 phút đọc
2 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ẽ:

  • Đăng ký page.on('worker') đúng thứ tự — trước khi navigate.
  • Dùng worker.url() để xác định script file của worker đã spawn.
  • Viết pattern wait-for-worker với Promise.allpage.waitForEvent('worker').
  • Filter worker theo URL, đếm số lượng worker spawn trong một flow.
  • Lắng nghe worker.on('close') để verify worker terminate đúng thời điểm.
  • Phân biệt page.on('worker') (dedicated worker) với context.on('serviceworker') (Service Worker).
  • Tránh 4–5 pitfall phổ biến khi dùng worker listener.
2

Cú Pháp page.on('worker')

page.on('worker', handler) fire mỗi khi page tạo một dedicated Web Worker mới (qua new Worker('script.js') trong browser). Handler nhận một Worker object — Playwright wrapper đại diện cho worker đó.

import { test } from '@playwright/test';

test('observe worker spawn', async ({ page }) => {
  // Đăng ký TRƯỚC khi navigate
  page.on('worker', worker => {
    console.log('Worker spawned:', worker.url());

    // Lắng nghe khi worker đóng
    worker.on('close', () => console.log('Worker closed:', worker.url()));
  });

  await page.goto('https://app.example.com');
});

worker.url() trả về URL tuyệt đối của script file. Ví dụ: https://app.example.com/workers/compute.worker.js. Đây là điểm nhận dạng duy nhất có sẵn ngay khi worker spawn — worker chưa expose state hay output gì thêm tại thời điểm event fire.

Playwright Worker object (từ @playwright/test) không phải Web API Worker trong browser. Nó là Node.js-side wrapper để test code inspect và communicate với worker.

3

Use Cases Chính

Ba nhóm use case phổ biến nhất khi dùng page.on('worker'):

Log Worker Activity

Ghi lại thông tin mỗi worker spawn và đóng — hữu ích khi debug tại sao performance test cho kết quả bất ngờ hoặc khi app spawn quá nhiều worker:

page.on('worker', worker => {
  console.log('[SPAWN]', worker.url(), new Date().toISOString());
  worker.on('close', () => {
    console.log('[CLOSE]', worker.url(), new Date().toISOString());
  });
});

Inspect Worker Khi Spawn

Verify rằng app spawn đúng worker file, đúng URL, đúng thời điểm. Trong bài 334 sẽ cover cách evaluate code trong worker để inspect state — nhưng ngay tại thời điểm spawn, URL là thứ có thể verify ngay:

page.on('worker', worker => {
  // Verify worker file path đúng
  if (!worker.url().includes('/workers/')) {
    console.warn('Unexpected worker URL:', worker.url());
  }
});

Hook Để Test Worker Behavior

Dùng listener như entry point để setup các assertion hoặc collect data trước khi worker làm việc. Pattern collect toàn bộ workers rồi assert ở cuối test:

const spawnedWorkers: import('@playwright/test').Worker[] = [];
page.on('worker', w => spawnedWorkers.push(w));

await page.goto('/');
// ... thực hiện các action

// Assert sau khi flow hoàn tất
expect(spawnedWorkers.map(w => w.url())).toContain(
  expect.stringContaining('compute.worker.js')
);
4

Setup Listener Trước page.goto

Worker có thể spawn ngay khi page load — ví dụ app khởi tạo Web Worker trong DOMContentLoaded handler hoặc trong top-level module code. Nếu đăng ký listener sau page.goto() resolve, event đã fire và listener không nhận được:

// BAD: Worker có thể đã spawn trong quá trình page load
await page.goto('/app');
page.on('worker', worker => {
  console.log(worker.url()); // Có thể không bao giờ chạy
});

// GOOD: Đăng ký listener TRƯỚC navigate
page.on('worker', worker => {
  console.log(worker.url()); // Bắt được worker spawn lúc load
});
await page.goto('/app');

page.goto() mặc định đợi đến load event. Nếu worker spawn sau load (ví dụ: do user action hay setTimeout), listener đăng ký trước goto vẫn hoạt động tốt vì listener là persistent — nó không tự unregister sau khi navigate hoàn tất.

Tuy nhiên, nếu worker chỉ spawn khi user click một button, bạn không nhất thiết phải đặt listener trước goto — chỉ cần trước action trigger worker đó là đủ. Để chắc chắn, thói quen tốt là luôn đăng ký listener trước navigate.

5

Pattern: Wait For Worker

Khi cần await worker object để dùng ngay (ví dụ: evaluate trong worker ở bài 334), dùng page.waitForEvent('worker') kết hợp Promise.all:

import { test, expect } from '@playwright/test';

test('worker spawns on page load', async ({ page }) => {
  const [worker] = await Promise.all([
    page.waitForEvent('worker'),
    page.goto('https://app.example.com'),
  ]);

  console.log('Worker URL:', worker.url());
  expect(worker.url()).toContain('compute.worker.js');
});

Promise.all khởi tạo cả hai promise cùng lúc: waitForEvent('worker') bắt đầu lắng nghe và goto trigger navigation. Khi worker spawn, Promise đầu tiên resolve; khi navigation hoàn tất, Promise thứ hai resolve. await Promise.all đợi cả hai, trả về array — destructure để lấy worker object.

Nếu worker spawn sau khi navigate (do user action), không cần Promise.all — chỉ cần đặt waitForEvent trước action trigger:

test('worker spawns on button click', async ({ page }) => {
  await page.goto('/app');

  // Chờ worker VÀ click cùng lúc
  const [worker] = await Promise.all([
    page.waitForEvent('worker'),
    page.getByRole('button', { name: 'Start processing' }).click(),
  ]);

  expect(worker.url()).toContain('processor.worker.js');
});

waitForEvent có default timeout 30s. Điều chỉnh nếu worker khởi động chậm:

const [worker] = await Promise.all([
  page.waitForEvent('worker', { timeout: 10_000 }),
  page.goto('/app'),
]);
6

Pattern: Filter Theo URL

Khi app spawn nhiều worker (mỗi loại một script file khác nhau), handler nhận tất cả. Dùng worker.url() để xác định worker cụ thể cần quan tâm:

page.on('worker', worker => {
  if (worker.url().includes('compute.worker.js')) {
    console.log('Compute worker spawned');
    // Xử lý riêng cho compute worker
  }

  if (worker.url().includes('image-resize.worker.js')) {
    console.log('Image resize worker spawned');
  }
});

Để await một worker cụ thể bằng waitForEvent, dùng predicate:

const [computeWorker] = await Promise.all([
  page.waitForEvent('worker', {
    predicate: w => w.url().includes('compute.worker.js'),
  }),
  page.goto('/app'),
]);

console.log('Got compute worker:', computeWorker.url());

predicate nhận worker object và trả về boolean. waitForEvent bỏ qua các worker không match, chỉ resolve khi tìm được worker match điều kiện. Nếu app spawn nhiều worker và chỉ một trong số đó match predicate, các worker khác vẫn được spawn bình thường — predicate không block chúng.

7

Pattern: Worker Count Tracking

Verify số lượng worker spawn đúng với kỳ vọng — ví dụ: app pool 2 worker cho xử lý ảnh song song:

import { test, expect, Worker } from '@playwright/test';

test('app spawns exactly 2 workers on load', async ({ page }) => {
  const workers: Worker[] = [];
  page.on('worker', w => workers.push(w));

  await page.goto('/image-editor');
  // Đợi page ổn định
  await page.waitForLoadState('networkidle');

  expect(workers.length).toBe(2);

  // Verify URL của từng worker đúng
  const urls = workers.map(w => w.url());
  expect(urls.some(u => u.includes('decode.worker.js'))).toBe(true);
  expect(urls.some(u => u.includes('encode.worker.js'))).toBe(true);
});

Nếu chỉ cần đếm số lượng mà không cần inspect:

let workerCount = 0;
page.on('worker', () => workerCount++);

await page.goto('/app');
await page.waitForLoadState('networkidle');

expect(workerCount).toBe(2);

Chú ý: waitForLoadState('networkidle') giúp đảm bảo page không còn network activity trước khi assert — giảm flakiness khi worker spawn sau khi tải xong một số resource.

8

Worker Close Event

Worker object expose close event — fire khi worker bị terminate (do worker.terminate() phía browser, hay khi page unload). Lắng nghe để verify worker lifecycle:

page.on('worker', worker => {
  console.log('Worker started:', worker.url());

  worker.on('close', () => {
    console.log('Worker terminated:', worker.url());
  });
});

Dùng close event để verify worker terminate khi page navigate sang route khác:

test('worker closes when navigating away', async ({ page }) => {
  let closed = false;

  page.on('worker', worker => {
    worker.on('close', () => {
      closed = true;
    });
  });

  await page.goto('/processing');
  // Đảm bảo worker đã spawn
  await page.waitForEvent('worker');

  // Navigate sang trang khác — browser terminate worker
  await page.goto('/home');

  expect(closed).toBe(true);
});

worker.on('close') phải được đăng ký trong callback của page.on('worker') — không thể đăng ký trước khi có worker object. Pattern nested listener này là chuẩn.

9

Khác Với context.on('serviceworker')

Hai loại worker trong browser có API khác nhau trong Playwright:

Đặc điểm page.on('worker') context.on('serviceworker')
Loại worker Dedicated Web Worker Service Worker
Tạo bằng new Worker('script.js') navigator.serviceWorker.register('sw.js')
Scope Gắn với một page cụ thể Gắn với browser context (nhiều page)
Lifetime Sống cùng page, terminate khi page đóng Tồn tại độc lập, có thể sống qua nhiều page
Playwright API page.on('worker', cb) context.on('serviceworker', cb)
Object trả về Worker Worker (cùng type, khác instance)
// Dedicated Web Worker — gắn với page
page.on('worker', worker => {
  console.log('Dedicated worker:', worker.url());
});

// Service Worker — gắn với context (nhiều page)
context.on('serviceworker', worker => {
  console.log('Service worker:', worker.url());
});

Nếu app dùng cả hai loại, bạn cần đăng ký cả hai listener. page.on('worker') không bắt Service Worker và ngược lại.

Dấu hiệu nhận biết: nếu app có file sw.js hoặc service-worker.js và dùng navigator.serviceWorker.register(), bạn cần context.on('serviceworker'). Nếu app dùng new Worker() để tính toán nền hoặc xử lý file, dùng page.on('worker').

10

Pattern: Listen Across Pages

Playwright không có context.on('worker') trực tiếp cho dedicated worker. Nếu test cần bắt worker từ nhiều tab (ví dụ: main tab mở popup tab và popup cũng dùng worker), pattern là lắng nghe context.on('page') rồi attach page.on('worker') cho mỗi page mới:

import { test } from '@playwright/test';

test('observe workers across all pages', async ({ page, context }) => {
  const allWorkers: string[] = [];

  // Attach worker listener cho mọi page mới trong context
  context.on('page', newPage => {
    newPage.on('worker', w => {
      allWorkers.push(w.url());
      console.log('Worker on new page:', w.url());
    });
  });

  // Đăng ký cho page ban đầu (không trigger context.on('page'))
  page.on('worker', w => {
    allWorkers.push(w.url());
    console.log('Worker on main page:', w.url());
  });

  await page.goto('/app');

  // App mở popup tab — context.on('page') sẽ attach worker listener cho popup
  const [popup] = await Promise.all([
    context.waitForEvent('page'),
    page.getByRole('button', { name: 'Open in new window' }).click(),
  ]);

  await popup.waitForLoadState();

  console.log('All workers across pages:', allWorkers);
});

Lưu ý quan trọng: context.on('page') chỉ fire cho các page mới mở sau khi listener được đăng ký — không fire cho page ban đầu (page được tạo khi fixture khởi tạo). Vì vậy phải attach listener riêng cho page ban đầu.

11

Use Cases Debug

Hai use case debug phổ biến khi dùng page.on('worker'):

Verify Worker Không Leak

Worker leak xảy ra khi app tạo worker mới mỗi lần user thực hiện một action mà không terminate worker cũ. Theo thời gian, số worker tăng lên, tiêu tốn memory và CPU:

test('no worker leak after repeated actions', async ({ page }) => {
  let spawnCount = 0;
  let closeCount = 0;

  page.on('worker', worker => {
    spawnCount++;
    worker.on('close', () => closeCount++);
  });

  await page.goto('/app');

  // Thực hiện action nhiều lần
  for (let i = 0; i < 5; i++) {
    await page.getByRole('button', { name: 'Process' }).click();
    await page.waitForTimeout(500);
  }

  await page.waitForLoadState('networkidle');

  // Nếu app tạo 1 worker rồi tái dụng: spawnCount = 1
  // Nếu leak: spawnCount = 5, closeCount = 0
  console.log(`Spawned: ${spawnCount}, Closed: ${closeCount}`);
  expect(spawnCount - closeCount).toBeLessThanOrEqual(1);
});

Verify Worker Terminate Khi Page Navigate

Dedicated worker phải terminate khi page navigate sang URL khác. Nếu app dùng SharedWorker hay worker được giữ sống bằng cách nào đó, close event có thể không fire — đây là dấu hiệu cần kiểm tra lại thiết kế:

test('workers terminate on navigation', async ({ page }) => {
  const closedUrls: string[] = [];

  page.on('worker', worker => {
    worker.on('close', () => closedUrls.push(worker.url()));
  });

  await page.goto('/heavy-computation');
  // Đợi worker spawn
  await page.waitForEvent('worker');

  // Navigate away
  await page.goto('/about');

  // Worker phải đã terminate
  expect(closedUrls.length).toBeGreaterThan(0);
});
12

Common Pitfalls

Pitfall 1: Listener Attach Sau Spawn — Miss Event

Đây là pitfall phổ biến nhất. Event worker fire một lần tại thời điểm spawn — listener đăng ký sau đó không nhận được retroactively:

// BAD
await page.goto('/app'); // Worker spawn trong quá trình load
page.on('worker', w => console.log(w.url())); // Không fire

// GOOD
page.on('worker', w => console.log(w.url())); // Đăng ký trước
await page.goto('/app');

Pitfall 2: Worker URL Có Hash Ngẫu Nhiên — Khó Match

Một số bundler (Webpack, Vite) thêm content hash vào tên file: compute.worker.abc123.js. Nếu dùng exact string match, test sẽ fail sau mỗi lần build:

// BAD: Fail khi hash thay đổi
expect(worker.url()).toBe('https://app.com/compute.worker.abc123.js');

// GOOD: Dùng includes hoặc regex để bỏ qua hash
expect(worker.url()).toContain('compute.worker');
// hoặc
expect(worker.url()).toMatch(/compute\.worker\.[a-z0-9]+\.js$/);

Pitfall 3: close Event Không Fire Nếu Page Crash

Nếu page crash (JavaScript error nghiêm trọng, OOM, hoặc test kill browser đột ngột), close event của worker có thể không fire. Không dùng close event làm điều kiện duy nhất để kết thúc test — kết hợp với timeout hoặc điều kiện khác:

// Không nên dùng pattern này khi không có safeguard
let closed = false;
worker.on('close', () => { closed = true; });
while (!closed) { /* có thể loop mãi nếu crash */ }

Pitfall 4: Cùng URL, Nhiều Lần Spawn — Handler Fire Nhiều Lần

Nếu app khởi tạo lại worker (sau error, sau user action), page.on('worker') fire mỗi lần. Nếu handler có side effect (push vào array, tăng counter), kết quả tích lũy theo số lần spawn:

// Handler fire 3 lần nếu worker spawn 3 lần, dù cùng URL
page.on('worker', w => {
  console.log(w.url()); // Cùng URL, 3 lần log
});

Nếu chỉ quan tâm tới worker spawn đầu tiên của một URL cụ thể, filter trong handler:

const seenUrls = new Set<string>();
page.on('worker', w => {
  if (!seenUrls.has(w.url())) {
    seenUrls.add(w.url());
    console.log('First spawn:', w.url());
  }
});

Pitfall 5: Nhầm Worker Event Với ServiceWorker Event

page.on('worker') không bắt Service Worker. Nếu kiểm tra Service Worker bằng page.on('worker'), listener sẽ không bao giờ fire và test sẽ timeout hoặc cho kết quả sai. Đúng phải dùng context.on('serviceworker').

13

Tổng Kết

  • page.on('worker', cb) fire mỗi khi page tạo dedicated Web Worker — listener phải đăng ký trước action trigger spawn.
  • worker.url() là thuộc tính duy nhất có ngay khi event fire; dùng includes() thay vì exact match để tránh vỡ khi hash thay đổi.
  • Dùng Promise.all([page.waitForEvent('worker'), page.goto(...)]) khi cần await worker object ngay sau navigate.
  • Dùng predicate trong waitForEvent khi app spawn nhiều worker và cần chờ một worker cụ thể.
  • worker.on('close') phải được đăng ký bên trong callback của page.on('worker') — dùng để verify worker terminate đúng thời điểm.
  • Không có context.on('worker') — để listen across pages, dùng context.on('page') rồi attach page.on('worker') trong đó.
  • page.on('worker') chỉ bắt dedicated Web Worker; context.on('serviceworker') mới bắt Service Worker.
14

Bài Tập Củng Cố

Câu 1

Đoạn test sau có vấn đề gì?

test('verify worker spawns', async ({ page }) => {
  await page.goto('/app');
  const workers: string[] = [];
  page.on('worker', w => workers.push(w.url()));
  await page.waitForLoadState('networkidle');
  expect(workers.length).toBe(1);
});
Đáp án

Listener được đăng ký sau page.goto(). Nếu worker spawn trong quá trình tải page (DOMContentLoaded hoặc load event), event đã fire và listener bỏ lỡ. Kết quả: workers.length là 0, test fail. Sửa: đặt page.on('worker', ...) trước await page.goto('/app').

Câu 2

App spawn 3 worker khi tải: auth.worker.js, cache.worker.js, render.worker.js. Viết code dùng waitForEvent để chờ đúng render.worker.js.

Đáp án
const [renderWorker] = await Promise.all([
  page.waitForEvent('worker', {
    predicate: w => w.url().includes('render.worker'),
  }),
  page.goto('/app'),
]);
expect(renderWorker.url()).toContain('render.worker');

Không dùng waitForEvent không có predicate vì nó sẽ resolve với worker đầu tiên (auth.worker.js).

Câu 3

Làm sao verify worker terminate khi user navigate sang trang khác? Viết test skeleton.

Đáp án
test('worker closes on navigation', async ({ page }) => {
  let closed = false;

  page.on('worker', worker => {
    worker.on('close', () => { closed = true; });
  });

  await page.goto('/heavy');
  await page.waitForEvent('worker'); // Đảm bảo worker đã spawn

  await page.goto('/home');

  expect(closed).toBe(true);
});

Câu 4

Phân biệt page.on('worker')context.on('serviceworker'): loại worker nào mỗi API bắt được? Khi nào nên dùng loại nào?

Đáp án

page.on('worker') bắt dedicated Web Worker — tạo bằng new Worker(), gắn với một page, terminate khi page đóng. context.on('serviceworker') bắt Service Worker — tạo bằng navigator.serviceWorker.register(), gắn với browser context, có thể tồn tại qua nhiều page và navigate. Dùng page.on('worker') khi app dùng worker cho tính toán nền (image processing, crypto, data parsing). Dùng context.on('serviceworker') khi app dùng Service Worker cho offline caching, push notification, hoặc intercepting fetch.

Câu 5

App tạo worker mới mỗi lần user bấm "Refresh". Sau 5 lần bấm, bạn muốn verify không có worker leak (mỗi worker cũ đã đóng trước khi worker mới spawn). Viết phần kiểm tra bằng page.on('worker')worker.on('close').

Đáp án
test('no worker leak on repeated refresh', async ({ page }) => {
  let activeWorkers = 0;

  page.on('worker', worker => {
    activeWorkers++;
    worker.on('close', () => { activeWorkers--; });
  });

  await page.goto('/app');

  for (let i = 0; i < 5; i++) {
    await page.getByRole('button', { name: 'Refresh' }).click();
    await page.waitForTimeout(300); // Đợi app terminate worker cũ
  }

  await page.waitForLoadState('networkidle');

  // Nếu không leak: chỉ có đúng 1 worker active tại một thời điểm
  expect(activeWorkers).toBeLessThanOrEqual(1);
});
15

Bài Tiếp Theo

Bài 334 đi sâu vào worker.evaluate() — chạy code trong context của worker để đọc state, inspect biến, hay trigger hàm nội bộ từ test.

Bài 334: worker.evaluate() — Chạy Code Trong Worker Context