UNPKG

msw-snapshot

Version:

Transparently create an API cache for testing.

140 lines 5.53 kB
import { TextDecoder } from 'node:util'; import { http, bypass } from "msw"; import { dirname, join } from 'node:path'; import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { createHash } from 'node:crypto'; const pureFetch = globalThis.fetch; export * from './mask.js'; /** * Default namespace. */ export const DEFAULT_NAMESPACE = 'default'; /** * Context of snapshotting. * You can use it for separating snapshot files with modifying context.namespace. */ export const context = { namespace: DEFAULT_NAMESPACE, }; /** * Create snapshot RequestHandler. */ export const snapshot = (config) => { return http.all(config.test ?? /.*/, async (mswInfo) => { const clonedInfo = () => { return { request: mswInfo.request.clone(), cookies: { ...mswInfo.cookies }, context: context, }; }; const snapshotPath = join(config.basePath, await createSnapshotPath(clonedInfo(), config)); // Fetch from snapshot if (!config.ignoreSnapshots && existsSync(snapshotPath)) { try { const snapshot = JSON.parse(readFileSync(snapshotPath).toString('utf8')); config.onFetchFromSnapshot?.(clonedInfo(), snapshot); const filteredHeaders = snapshot.response.headers.filter(([key, _]) => !key.toLowerCase().includes('content-encoding') && !key.toLowerCase().includes('transfer-encoding')); return new Response(new TextEncoder().encode(snapshot.response.body), { headers: new Headers(filteredHeaders), status: snapshot.response.status, statusText: snapshot.response.statusText, }); } catch (e) { console.error(`Can't parse snapshot file: ${snapshotPath}`, e); } } // Fetch from server const response = await pureFetch(bypass(mswInfo.request.clone())); const snapshot = { request: { method: mswInfo.request.method, url: mswInfo.request.url, body: new TextDecoder('utf-8').decode(await mswInfo.request.clone().arrayBuffer()), headers: getSortedEntries(mswInfo.request.headers), cookies: getSortedEntries(mswInfo.cookies), }, response: { status: response.status, statusText: response.statusText, body: new TextDecoder('utf-8').decode(await response.arrayBuffer()), headers: getSortedEntries(response.headers), } }; config.onFetchFromServer?.(clonedInfo(), snapshot); // Update snapshot if needed let shouldUpdateSnapshots = false; shouldUpdateSnapshots = shouldUpdateSnapshots || config.updateSnapshots === true; shouldUpdateSnapshots = shouldUpdateSnapshots || config.updateSnapshots === 'all'; shouldUpdateSnapshots = shouldUpdateSnapshots || config.updateSnapshots === 'missing' && !existsSync(snapshotPath); if (shouldUpdateSnapshots) { mkdirSync(dirname(snapshotPath), { recursive: true }); writeFileSync(snapshotPath, JSON.stringify(snapshot, undefined, 2)); config.onSnapshotUpdated?.(clonedInfo(), snapshot); } // Filter out compression-related headers since we're manually handling the body const filteredHeaders = snapshot.response.headers.filter(([key, _]) => !key.toLowerCase().includes('content-encoding') && !key.toLowerCase().includes('transfer-encoding')); return new Response(new TextEncoder().encode(snapshot.response.body), { headers: new Headers(filteredHeaders), status: snapshot.response.status, statusText: snapshot.response.statusText, }); }); }; /** * Get sorted array of [key, val] tuple from ***#entries. */ export function getEntries(iter) { if (iter instanceof Headers) { const entries = []; iter.forEach((v, k) => entries.push([k, v])); return entries; } else if (iter instanceof FormData) { const entries = []; iter.forEach((v, k) => entries.push([k, v.toString()])); return entries; } else if (iter instanceof URLSearchParams) { return Array.from(iter.entries()); } return Object.entries(iter).map(([k, v]) => [k, Array.isArray(v) ? v.join(',') : v]); } /** * Get sorted array of [key, val] tuple from ***#entries. */ export function getSortedEntries(iter) { return getEntries(iter).sort(([a], [b]) => a.localeCompare(b)); } ; /** * Create hash string from object. */ export function toHashString(object) { return createHash('md5').update(JSON.stringify(object), 'binary').digest('hex'); } /** * Create snapshot name from request. */ async function createSnapshotPath(info, config) { if (config.createSnapshotPath) { return await config.createSnapshotPath(info); } const url = new URL(info.request.url); return [ context.namespace, info.request.method, url.origin, url.pathname, ].join('/') + '/' + toHashString([ getSortedEntries(url.searchParams), getSortedEntries(info.request.headers), getSortedEntries(info.cookies), new TextDecoder('utf-8').decode(await info.request.arrayBuffer()), ]); } ; //# sourceMappingURL=index.js.map