UNPKG

msw

Version:

Seamless REST/GraphQL API mocking library for browser and Node.js.

919 lines (797 loc) • 26.9 kB
import * as fs from 'node:fs' import nodeHttp from 'node:http' import { chromium } from '@playwright/test' import { createTeardown } from 'fs-teardown' import { build, createServer, createServerModuleRunner, preview } from 'vite' import { fromRoot, mswExports } from '../../test/support/alias' import { fromTemp } from '../../test/support/utils' import type { network } from 'virtual:msw' import { msw } from './plugin' const fsMock = createTeardown({ rootDir: fromTemp('vite/plugin'), }) beforeAll(async () => { await fsMock.prepare() }) beforeEach(async () => { await fsMock.reset() }) afterEach(() => { vi.unstubAllEnvs() }) afterAll(async () => { await fsMock.cleanup() }) test('loads the packaged plugin without installing its development dependencies', async () => { await fsMock.create({ 'plugin.mjs': fs.readFileSync(fromRoot('lib/vite/index.js'), 'utf8'), 'verify.mjs': ` import { msw } from './plugin.mjs' console.log(msw().name) `, }) const { stdout } = await fsMock.exec('node ./verify.mjs') expect(stdout.trim()).toBe('msw') }) test('excludes user-guarded mocking from a production client bundle', async () => { vi.stubEnv('NODE_ENV', 'production') await fsMock.create({ 'index.html': '<output></output><script type="module" src="/entry.ts"></script>', 'handlers.ts': ` import { http, HttpResponse } from 'msw/http' console.log('MOCK_ONLY_HANDLER_MODULE') export const handlers = [http.get('/resource', () => HttpResponse.text('MOCK_ONLY_RESPONSE'))] `, 'shared.ts': ` import { http, HttpResponse } from 'msw/http' export const appValue = 'Application ready' export function createHandler() { return http.get('/override', () => HttpResponse.text('MOCK_ONLY_OVERRIDE')) } `, 'side-effect.ts': "document.body.dataset.sideEffect = 'preserved'", 'entry.ts': ` import { appValue, createHandler } from './shared' import './side-effect' if (import.meta.env.DEV) { const { network: mocking } = await import('virtual:msw') const { handlers } = await import('./handlers') mocking.configure({ handlers }) await mocking.enable() mocking.use(createHandler()) } function start(network: { enable(): string }) { return network.enable() } document.querySelector('output')!.textContent = appValue + start({ enable() { return ' with local network' }, }) `, }) await build({ configFile: false, root: fsMock.resolve('.'), logLevel: 'silent', plugins: [msw({ mode: 'auto' })], resolve: { alias: { 'msw/experimental': fromRoot('lib/core/experimental/index.js'), ...mswExports, }, }, build: { minify: false, rollupOptions: { output: { entryFileNames: 'app.js' } }, }, }) const output = fs.readFileSync(fsMock.resolve('dist/app.js'), 'utf8') expect(output).not.toContain('MOCK_ONLY') expect(output).not.toContain('virtual:msw') expect(output).not.toContain('defineNetwork') expect(output).not.toContain('mockServiceWorker') expect(output).not.toContain('HttpResponse') expect(fs.existsSync(fsMock.resolve('public'))).toBe(false) expect(fs.existsSync(fsMock.resolve('dist/mockServiceWorker.js'))).toBe(false) const server = await preview({ configFile: false, root: fsMock.resolve('.'), logLevel: 'silent', preview: { host: '127.0.0.1', port: 0 }, }) await using serverResource = { [Symbol.asyncDispose]: server.close.bind(server), } const browser = await chromium.launch() await using browserResource = { [Symbol.asyncDispose]: browser.close.bind(browser), } const page = await browser.newPage() await page.goto(server.resolvedUrls.local[0]) await expect .poll(() => page.locator('output').textContent()) .toBe('Application ready with local network') await expect( page.locator('body').getAttribute('data-side-effect'), ).resolves.toBe('preserved') }, 20_000) test('excludes user-guarded mocking from a production server bundle', async () => { vi.stubEnv('NODE_ENV', 'production') const { msw: builtMsw } = await import('../../lib/vite/index.js') await fsMock.create({ 'package.json': '{"type":"module"}', 'handlers.js': ` import { http, HttpResponse } from 'msw/http' console.log('MOCK_ONLY_HANDLER_MODULE') export const handlers = [http.get('/resource', () => HttpResponse.text('MOCK_ONLY_RESPONSE'))] `, 'entry.js': ` if (import.meta.env.DEV) { const { network } = await import('virtual:msw') const { handlers } = await import('./handlers.js') network.configure({ handlers }) await network.enable() } console.log('Application ready') `, }) await build({ configFile: false, root: fsMock.resolve('.'), logLevel: 'silent', plugins: [builtMsw()], build: { ssr: 'entry.js', outDir: 'server', minify: false }, }) const output = fs.readFileSync(fsMock.resolve('server/entry.js'), 'utf8') expect(output).not.toContain('MOCK_ONLY') expect(output).not.toMatch(/(?:from|import)\s*["'](?:msw|virtual:msw)/) expect(output).not.toContain('defineNetwork') expect(output).not.toContain('network') expect(output).not.toContain('handlers') expect(fs.existsSync(fsMock.resolve('public'))).toBe(false) expect(fs.existsSync(fsMock.resolve('server/mockServiceWorker.js'))).toBe( false, ) const { stdout } = await fsMock.exec('node ./server/entry.js') expect(stdout.trim()).toBe('Application ready') }) test('serves the worker when the dev server runs in production', async () => { vi.stubEnv('NODE_ENV', 'production') const server = await createServer({ configFile: false, root: fsMock.resolve('.'), appType: 'custom', logLevel: 'silent', plugins: [msw()], server: { host: '127.0.0.1', port: 0 }, }) await using serverResource = { [Symbol.asyncDispose]: server.close.bind(server), } await server.listen() const response = await fetch( new URL('/mockServiceWorker.js', server.resolvedUrls?.local[0]), ) expect(response.status).toBe(200) }) test('restores cached server mocking after hot updates and replaces it on full reloads', async () => { const { msw: builtMsw } = await import('../../lib/vite/index.js') await fsMock.create({ 'entry.js': ` import { network } from 'virtual:msw' import { http, HttpResponse } from 'msw/http' network.configure({ handlers: [http.get('http://msw.test/resource', () => HttpResponse.text('mocked'))], }) await network.enable() export let completedUpdates = 0 export let disabledBeforeUpdate = false if (import.meta.hot) { import.meta.hot.on('vite:beforeUpdate', () => { disabledBeforeUpdate = network.readyState === 0 }) import.meta.hot.on('vite:afterUpdate', () => { completedUpdates += 1 }) } export { network } `, }) const originalClientRequest = nodeHttp.ClientRequest const server = await createServer({ configFile: false, root: fsMock.resolve('.'), logLevel: 'silent', plugins: [builtMsw()], resolve: { alias: { 'msw/experimental': fromRoot('lib/core/experimental/index.js'), ...mswExports, }, }, server: { middlewareMode: true, watch: null }, }) await using serverResource = { [Symbol.asyncDispose]: server.close.bind(server), } const error = vi.fn() const runner = createServerModuleRunner(server.environments.ssr, { hmr: { logger: { debug: vi.fn(), error } }, }) await using runnerResource = { [Symbol.asyncDispose]: runner.close.bind(runner), } const integration = await runner.import<{ network: typeof network completedUpdates: number disabledBeforeUpdate: boolean }>('/entry.js') await using interception = { [Symbol.asyncDispose]: async () => { const current = await runner.import<{ network: typeof network }>( '/entry.js', ) if (current.network.readyState === 1) { await current.network.disable() } }, } expect(nodeHttp.ClientRequest).not.toBe(originalClientRequest) server.environments.ssr.hot.send({ type: 'update', updates: [] }) await expect.poll(() => integration.completedUpdates).toBe(1) expect(integration.disabledBeforeUpdate).toBe(true) await expect.poll(() => integration.network.readyState).toBe(1) expect(nodeHttp.ClientRequest).not.toBe(originalClientRequest) const updatedResponse = await fetch('http://msw.test/resource') await expect(updatedResponse.text()).resolves.toBe('mocked') await integration.network.disable() server.environments.ssr.hot.send({ type: 'update', updates: [] }) await expect.poll(() => integration.completedUpdates).toBe(2) expect(integration.network.readyState).toBe(0) expect(nodeHttp.ClientRequest).toBe(originalClientRequest) await integration.network.enable() server.environments.ssr.hot.send({ type: 'full-reload' }) await expect .poll(async () => { const current = await runner.import<{ network: typeof network }>( '/entry.js', ) return ( current.network !== integration.network && current.network.readyState === 1 ) }) .toBe(true) expect(integration.network.readyState).toBe(0) expect(error).not.toHaveBeenCalled() const response = await fetch('http://msw.test/resource') await expect(response.text()).resolves.toBe('mocked') }) test('replays server setup and retires replaced networks during reloads', async () => { const { msw: builtMsw } = await import('../../lib/vite/index.js') await fsMock.create({ 'entry.js': ` import { network } from 'virtual:msw' import { http, HttpResponse } from 'msw/http' network.configure({ handlers: [http.get('http://msw.test/resource', () => HttpResponse.text('mocked'))] }) await network.enable() export { network } `, }) const server = await createServer({ configFile: false, root: fsMock.resolve('.'), logLevel: 'silent', plugins: [builtMsw()], resolve: { alias: { 'msw/experimental': fromRoot('lib/core/experimental/index.js'), ...mswExports, }, }, server: { middlewareMode: true, watch: null }, }) await using serverResource = { [Symbol.asyncDispose]: server.close.bind(server), } const runner = createServerModuleRunner(server.environments.ssr) await using runnerResource = { [Symbol.asyncDispose]: runner.close.bind(runner), } const original = await runner.import<{ network: typeof network }>('/entry.js') await using originalInterception = { [Symbol.asyncDispose]: async () => { if (original.network.readyState === 1) { await original.network.disable() } }, } const entryModule = runner.evaluatedModules.getModuleByUrl('/entry.js') expect(entryModule).toBeDefined() runner.evaluatedModules.invalidateModule(entryModule!) const replayed = await runner.import<{ network: typeof network }>('/entry.js') expect(replayed.network).toBe(original.network) // An overlapping SSR import can evaluate the replacement before HMR cleanup runs. runner.evaluatedModules.clear() const replacement = await runner.import<{ network: typeof network }>( '/entry.js', ) await using replacementInterception = { [Symbol.asyncDispose]: async () => { if (replacement.network.readyState === 1) { await replacement.network.disable() } }, } expect(replacement.network).not.toBe(original.network) expect(original.network.readyState).toBe(0) await original.network.enable() expect(original.network.readyState).toBe(0) expect(replacement.network.readyState).toBe(1) const response = await fetch('http://msw.test/resource') await expect(response.text()).resolves.toBe('mocked') }) test('creates one disabled network on the server and lets the user enable it', async () => { await fsMock.create({ 'entry.js': ` import { network } from 'virtual:msw' import { http, HttpResponse } from 'msw/http' network.configure({ handlers: [http.get('http://msw.test/resource', () => HttpResponse.text('mocked'))], }) export { network } `, }) const originalFetch = globalThis.fetch const server = await createServer({ configFile: false, root: fsMock.resolve('.'), logLevel: 'silent', plugins: [msw()], resolve: { alias: { 'msw/experimental': fromRoot('lib/core/experimental/index.js'), ...mswExports, }, }, server: { middlewareMode: true }, }) await using _ = { [Symbol.asyncDispose]: server.close.bind(server), } const integration: { network: typeof network } = await server.ssrLoadModule('/entry.js') const repeatedImport: { network: typeof network } = await server.ssrLoadModule('virtual:msw') expect(integration.network).toBe(repeatedImport.network) expect(integration.network.readyState).toBe(0) expect(globalThis.fetch).toBe(originalFetch) await integration.network.enable() await using interception = { [Symbol.asyncDispose]: async () => { await integration.network.disable() }, } const response = await fetch('http://msw.test/resource') await expect(response.text()).resolves.toBe('mocked') }) test('preserves browser interception through hot updates under a base path', async () => { const { msw: builtMsw } = await import('../../lib/vite/index.js') await fsMock.create({ 'index.html': '<button>Enable</button><output></output><script type="module" src="/entry.js"></script>', 'entry.js': ` import { network } from 'virtual:msw' import { http, HttpResponse } from 'msw/http' network.configure({ handlers: [http.get('/resource', () => HttpResponse.text('mocked'))], }) document.querySelector('output').textContent = String(network.readyState) let completedUpdates = 0 if (import.meta.hot) { import.meta.hot.on('vite:afterUpdate', () => { document.body.dataset.completedUpdates = String(++completedUpdates) }) } document.querySelector('button').onclick = async () => { await network.enable() const response = await fetch('/resource') document.querySelector('output').textContent = await response.text() } `, }) const server = await createServer({ configFile: false, base: '/app/', root: fsMock.resolve('.'), logLevel: 'silent', plugins: [builtMsw()], resolve: { alias: { 'msw/experimental': fromRoot('lib/core/experimental/index.js'), ...mswExports, }, }, server: { host: '127.0.0.1', port: 0, watch: null }, }) await using _ = { [Symbol.asyncDispose]: server.close.bind(server), } await server.listen() const serverIntegration: { network: typeof network } = await server.ssrLoadModule('virtual:msw') const browser = await chromium.launch() await using browserResource = { [Symbol.asyncDispose]: browser.close.bind(browser), } const page = await browser.newPage() const connected = page.waitForEvent('console', { predicate: (message) => message.text().includes('[vite] connected.'), }) await page.goto(new URL('/app/nested/', server.resolvedUrls!.local[0]).href, { waitUntil: 'networkidle', }) await connected await expect.poll(() => page.locator('output').textContent()).toBe('0') server.environments.client.hot.send({ type: 'update', updates: [] }) await expect .poll(() => page.locator('body').getAttribute('data-completed-updates')) .toBe('1') await expect( page.evaluate(async () => { const registrations = await navigator.serviceWorker.getRegistrations() return registrations.length }), ).resolves.toBe(0) await page.getByRole('button', { name: 'Enable' }).click() await expect.poll(() => page.locator('output').textContent()).toBe('mocked') server.environments.client.hot.send({ type: 'update', updates: [] }) await expect .poll(() => page.locator('body').getAttribute('data-completed-updates')) .toBe('2') await expect( page.evaluate(async () => { const response = await fetch('/resource') return response.text() }), ).resolves.toBe('mocked') await expect( page.evaluate(async () => { const registrations = await navigator.serviceWorker.getRegistrations() return registrations[0].active?.scriptURL }), ).resolves.toBe( new URL('/app/mockServiceWorker.js', server.resolvedUrls!.local[0]).href, ) expect(fs.existsSync(fsMock.resolve('public'))).toBe(false) expect(serverIntegration.network.readyState).toBe(0) }, 20_000) test('creates a server network in development builds', async () => { vi.stubEnv('NODE_ENV', 'development') await fsMock.create({ 'package.json': '{"type":"module"}', 'verify.js': ` import assert from 'node:assert/strict' const originalFetch = globalThis.fetch const { network } = await import('./server/entry.js') assert.equal(network.readyState, 0) assert.equal(globalThis.fetch, originalFetch) await network.enable() const response = await fetch('http://msw.test/resource') console.log(await response.text()) await network.disable() assert.equal(globalThis.fetch, originalFetch) `, 'entry.js': ` import { network } from 'virtual:msw' import { http, HttpResponse } from 'msw/http' network.configure({ handlers: [http.get('http://msw.test/resource', () => HttpResponse.text('mocked'))], }) export { network } `, }) fs.mkdirSync(fsMock.resolve('node_modules')) fs.symlinkSync(fromRoot('.'), fsMock.resolve('node_modules/msw'), 'dir') using dependencyLink = { [Symbol.dispose]: () => { fs.unlinkSync(fsMock.resolve('node_modules/msw')) }, } await build({ configFile: false, root: fsMock.resolve('.'), logLevel: 'silent', plugins: [msw()], ssr: { external: ['msw/experimental', 'msw/node', 'msw/http'] }, build: { ssr: 'entry.js', outDir: 'server' }, }) const { stdout } = await fsMock.exec('node ./verify.js') expect(stdout.trim()).toBe('mocked') }) test('creates a browser network in development builds without enabling it automatically', async () => { vi.stubEnv('NODE_ENV', 'development') await fsMock.create({ 'index.html': '<button>Enable</button><output></output><script type="module" src="/entry.js"></script>', 'entry.js': ` import { network } from 'virtual:msw' import { http, HttpResponse } from 'msw/http' network.configure({ handlers: [http.get('/resource', () => HttpResponse.text('mocked'))], }) document.querySelector('output').textContent = String(network.readyState) document.querySelector('button').onclick = async () => { await network.enable() network.use(http.get('/resource', () => HttpResponse.text('override'))) const response = await fetch('/resource') document.querySelector('output').textContent = await response.text() } `, }) await build({ configFile: false, root: fsMock.resolve('.'), logLevel: 'silent', plugins: [msw()], resolve: { alias: { 'msw/experimental': fromRoot('lib/core/experimental/index.js'), ...mswExports, }, }, }) const server = await preview({ configFile: false, root: fsMock.resolve('.'), logLevel: 'silent', preview: { host: '127.0.0.1', port: 0 }, }) await using _ = { [Symbol.asyncDispose]: server.close.bind(server), } const browser = await chromium.launch() await using browserResource = { [Symbol.asyncDispose]: browser.close.bind(browser), } const page = await browser.newPage() await page.goto(server.resolvedUrls.local[0]) await expect.poll(() => page.locator('output').textContent()).toBe('0') await expect( page.evaluate(async () => { const registrations = await navigator.serviceWorker.getRegistrations() return registrations.length }), ).resolves.toBe(0) await page.getByRole('button', { name: 'Enable' }).click() await expect.poll(() => page.locator('output').textContent()).toBe('override') }, 20_000) test('serves the worker script without writing files during development', async () => { const server = await createServer({ configFile: false, root: fsMock.resolve('.'), logLevel: 'silent', plugins: [msw()], server: { host: '127.0.0.1', port: 0, }, }) await using _ = { [Symbol.asyncDispose]: server.close.bind(server), } await server.listen() const serverUrl = server.resolvedUrls?.local[0] expect(serverUrl).toBeDefined() const response = await fetch(new URL('/mockServiceWorker.js', serverUrl)) expect(response.status).toBe(200) expect(fs.existsSync(fsMock.resolve('public'))).toBe(false) expect(response.headers.get('content-type')).toBe( 'application/javascript; charset=utf-8', ) await expect(response.text()).resolves.toBe( fs.readFileSync( new URL('../mockServiceWorker.js', import.meta.url), 'utf8', ), ) }) test('serves the worker at the root for a relative development base', async () => { const server = await createServer({ configFile: false, base: './', root: fsMock.resolve('.'), logLevel: 'silent', plugins: [msw()], server: { host: '127.0.0.1', port: 0, }, }) await using _ = { [Symbol.asyncDispose]: server.close.bind(server), } await server.listen() const serverUrl = server.resolvedUrls?.local[0] expect(serverUrl).toBeDefined() const response = await fetch(new URL('/mockServiceWorker.js', serverUrl)) expect(response.status).toBe(200) expect(fs.existsSync(fsMock.resolve('public'))).toBe(false) expect(response.headers.get('content-type')).toBe( 'application/javascript; charset=utf-8', ) await expect(response.text()).resolves.toBe( fs.readFileSync( new URL('../mockServiceWorker.js', import.meta.url), 'utf8', ), ) }) test('serves only the worker in worker-only mode with an absolute base URL', async () => { const server = await createServer({ configFile: false, base: 'https://cdn.example.com/app/', root: fsMock.resolve('.'), publicDir: 'static', logLevel: 'silent', plugins: [msw({ mode: 'worker-only' })], server: { host: '127.0.0.1', port: 0, }, }) await using _ = { [Symbol.asyncDispose]: server.close.bind(server), } await server.listen() const serverUrl = server.resolvedUrls?.local[0] expect(serverUrl).toBeDefined() const response = await fetch(new URL('/app/mockServiceWorker.js', serverUrl)) expect(response.status).toBe(200) expect(response.headers.get('service-worker-allowed')).toBe('/') await expect( fetch(new URL('/mockServiceWorker.js', serverUrl)), ).resolves.toHaveProperty('status', 404) expect(fs.existsSync(fsMock.resolve('static'))).toBe(false) await expect(response.text()).resolves.toContain('* Mock Service Worker.') await expect( server.environments.client.pluginContainer.resolveId('virtual:msw'), ).resolves.toBeNull() await expect( server.environments.ssr.pluginContainer.resolveId('virtual:msw/options'), ).resolves.toBeNull() }) test('preserves user integrations during production builds in worker-only mode', async () => { vi.stubEnv('NODE_ENV', 'production') await fsMock.create({ 'package.json': '{"type":"module"}', 'entry.js': ` import { network } from 'virtual:msw' import { handlers } from './handlers.js' network.configure({ handlers }) await network.enable() `, 'handlers.js': "export const handlers = ['USER_DEFINED_HANDLERS']", }) await build({ configFile: false, root: fsMock.resolve('.'), logLevel: 'silent', plugins: [msw({ mode: 'worker-only' })], build: { ssr: 'entry.js', minify: false, rollupOptions: { external: ['virtual:msw'] }, }, }) const output = fs.readFileSync(fsMock.resolve('dist/entry.js'), 'utf8') expect(output).toContain('virtual:msw') expect(output).toContain('network.configure(') expect(output).toContain('await network.enable()') expect(output).toContain('USER_DEFINED_HANDLERS') expect(fs.existsSync(fsMock.resolve('public'))).toBe(false) }) test('does not emit the worker script for builds without the network', async () => { vi.stubEnv('NODE_ENV', 'production') await fsMock.create({ 'index.html': '<html><body>Example app</body></html>', }) await build({ configFile: false, root: fsMock.resolve('.'), logLevel: 'silent', plugins: [msw()], }) expect(fs.existsSync(fsMock.resolve('public'))).toBe(false) expect(fs.existsSync(fsMock.resolve('dist/mockServiceWorker.js'))).toBe(false) }) test('emits the worker script for production builds that import the network', async () => { vi.stubEnv('NODE_ENV', 'production') await fsMock.create({ 'index.html': '<script type="module" src="/entry.js"></script>', 'entry.js': ` import { network } from 'virtual:msw' await network.enable() `, }) await build({ configFile: false, root: fsMock.resolve('.'), logLevel: 'silent', plugins: [msw()], resolve: { alias: { 'msw/experimental': fromRoot('lib/core/experimental/index.js'), ...mswExports, }, }, }) expect(fs.existsSync(fsMock.resolve('public'))).toBe(false) expect( fs.readFileSync(fsMock.resolve('dist/mockServiceWorker.js'), 'utf8'), ).toBe( fs.readFileSync( new URL('../mockServiceWorker.js', import.meta.url), 'utf8', ), ) }) test('emits the worker to the client output instead of the public directory', async () => { vi.stubEnv('NODE_ENV', 'development') await fsMock.create({ 'index.html': '<script type="module" src="/entry.js"></script>', 'entry.js': "import 'virtual:msw'", 'static/example.txt': 'Public asset', }) await build({ configFile: false, base: '/app/', root: fsMock.resolve('.'), publicDir: 'static', logLevel: 'silent', plugins: [msw()], resolve: { alias: { 'msw/experimental': fromRoot('lib/core/experimental/index.js'), ...mswExports, }, }, build: { outDir: 'build/client', assetsDir: 'bundled', copyPublicDir: false, }, }) expect(fs.existsSync(fsMock.resolve('static/mockServiceWorker.js'))).toBe( false, ) expect(fs.existsSync(fsMock.resolve('static/app'))).toBe(false) expect( fs.readFileSync( fsMock.resolve('build/client/mockServiceWorker.js'), 'utf8', ), ).toBe( fs.readFileSync( new URL('../mockServiceWorker.js', import.meta.url), 'utf8', ), ) }) test('emits the worker in worker-only mode without the virtual modules', async () => { vi.stubEnv('NODE_ENV', 'production') await fsMock.create({ 'index.html': '<html><body>Example app</body></html>', }) await build({ configFile: false, root: fsMock.resolve('.'), publicDir: false, logLevel: 'silent', plugins: [msw({ mode: 'worker-only' })], }) expect(fs.existsSync(fsMock.resolve('public'))).toBe(false) expect(fs.existsSync(fsMock.resolve('dist/mockServiceWorker.js'))).toBe(true) })