From f6e709c3c7d1ac60f99cac6699e83565d8401cd0 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Mon, 7 Mar 2022 00:48:01 +0100 Subject: [PATCH] fix(setupWorker): resolve the "start" promise after the worker has activated (#1146) --- src/setupWorker/start/createStartHandler.ts | 30 ++++++++-- .../msw-api/setup-worker/start/start.mocks.ts | 15 +++-- test/msw-api/setup-worker/start/start.test.ts | 57 +++++++++++++------ .../setup-worker/start/start.worker.js | 10 ++++ 4 files changed, 87 insertions(+), 25 deletions(-) create mode 100644 test/msw-api/setup-worker/start/start.worker.js diff --git a/src/setupWorker/start/createStartHandler.ts b/src/setupWorker/start/createStartHandler.ts index fc8f631f4..c0e3022e3 100644 --- a/src/setupWorker/start/createStartHandler.ts +++ b/src/setupWorker/start/createStartHandler.ts @@ -92,10 +92,6 @@ If this message still persists after updating, please report an issue: https://g `) } - await enableMocking(context, options).catch((err) => { - throw new Error(`Failed to enable mocking: ${err?.message}`) - }) - context.keepAliveInterval = window.setInterval( () => context.workerChannel.send('KEEPALIVE_REQUEST'), 5000, @@ -108,7 +104,31 @@ If this message still persists after updating, please report an issue: https://g return registration } - const workerRegistration = startWorkerInstance() + const workerRegistration = startWorkerInstance().then( + async (registration) => { + const pendingInstance = registration.installing || registration.waiting + + // Wait until the worker is activated. + // Assume the worker is already activated if there's no pending registration + // (i.e. when reloading the page after a successful activation). + if (pendingInstance) { + await new Promise((resolve) => { + pendingInstance.addEventListener('statechange', () => { + if (pendingInstance.state === 'activated') { + return resolve() + } + }) + }) + } + + // Print the activation message only after the worker has been activated. + await enableMocking(context, options).catch((error) => { + throw new Error(`Failed to enable mocking: ${error?.message}`) + }) + + return registration + }, + ) // Defer any network requests until the Service Worker instance is ready. // This prevents a race condition between the Service Worker registration diff --git a/test/msw-api/setup-worker/start/start.mocks.ts b/test/msw-api/setup-worker/start/start.mocks.ts index ceda36a76..80274cfe0 100644 --- a/test/msw-api/setup-worker/start/start.mocks.ts +++ b/test/msw-api/setup-worker/start/start.mocks.ts @@ -8,8 +8,15 @@ const worker = setupWorker( // @ts-ignore window.msw = { - registration: worker.start().then((reg) => { - console.log('Registration Promise resolved') - return reg.constructor.name - }), + async startWorker() { + await worker.start({ + serviceWorker: { + // Use a custom Service Worker for this test that intentionally + // delays the worker installation time. This allows us to test + // that the "worker.start()" Promise indeed resolves only after + // the worker has been activated and not just registered. + url: '/worker.js', + }, + }) + }, } diff --git a/test/msw-api/setup-worker/start/start.test.ts b/test/msw-api/setup-worker/start/start.test.ts index 1381f3c8a..313abf079 100644 --- a/test/msw-api/setup-worker/start/start.test.ts +++ b/test/msw-api/setup-worker/start/start.test.ts @@ -1,34 +1,59 @@ import * as path from 'path' import { pageWith } from 'page-with' import { SetupWorkerApi } from 'msw' +import { waitFor } from '../../../support/waitFor' declare namespace window { export const msw: { - registration: ReturnType + startWorker(): ReturnType } } -test('resolves the "start" Promise after the worker has been activated', async () => { - const { page, consoleSpy } = await pageWith({ +test('resolves the "start" Promise when the worker has been activated', async () => { + const runtime = await pageWith({ example: path.resolve(__dirname, 'start.mocks.ts'), + routes(app) { + app.get('/worker.js', (req, res) => { + res.sendFile(path.resolve(__dirname, 'start.worker.js')) + }) + }, }) - const resolvedPayload = await page.evaluate(() => { - return window.msw.registration - }) - expect(resolvedPayload).toBe('ServiceWorkerRegistration') + const events: string[] = [] + + const untilWorkerActivated = runtime.page + .evaluate(() => { + return new Promise((resolve) => + navigator.serviceWorker.addEventListener('controllerchange', resolve), + ) + }) + .then(() => { + events.push('worker activated') + }) - const activationMessageIndex = consoleSpy - .get('startGroupCollapsed') - .findIndex((text) => { - return text.includes('[MSW] Mocking enabled') + const untilStartResolved = runtime.page + .evaluate(() => { + return window.msw.startWorker() + }) + .then(() => { + events.push('start resolved') }) - const customMessageIndex = consoleSpy.get('log').findIndex((text) => { - return text.includes('Registration Promise resolved') + const untilActivationMessage = waitFor(() => { + expect(runtime.consoleSpy.get('startGroupCollapsed')).toContain( + '[MSW] Mocking enabled.', + ) + events.push('enabled message') }) - expect(activationMessageIndex).toBeGreaterThan(-1) - expect(customMessageIndex).toBeGreaterThan(-1) - expect(customMessageIndex).toBeGreaterThan(activationMessageIndex) + await Promise.all([ + untilActivationMessage, + untilWorkerActivated, + untilStartResolved, + ]) + + expect(events[0]).toEqual('worker activated') + expect(events[1]).toEqual('start resolved') + expect(events[2]).toEqual('enabled message') + expect(events).toHaveLength(3) }) diff --git a/test/msw-api/setup-worker/start/start.worker.js b/test/msw-api/setup-worker/start/start.worker.js new file mode 100644 index 000000000..4cbedf1a9 --- /dev/null +++ b/test/msw-api/setup-worker/start/start.worker.js @@ -0,0 +1,10 @@ +importScripts('/mockServiceWorker.js') + +self.addEventListener('install', (event) => { + event.waitUntil( + new Promise((resolve) => { + // Emulate long worker installation. + setTimeout(resolve, 500) + }), + ) +})