UNPKG

msw-snapshot

Version:

Transparently create an API cache for testing.

161 lines 6.64 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.snapshot = exports.context = exports.DEFAULT_NAMESPACE = void 0; exports.getEntries = getEntries; exports.getSortedEntries = getSortedEntries; exports.toHashString = toHashString; const node_util_1 = require("node:util"); const msw_1 = require("msw"); const node_path_1 = require("node:path"); const node_fs_1 = require("node:fs"); const node_crypto_1 = require("node:crypto"); const pureFetch = globalThis.fetch; __exportStar(require("./mask.js"), exports); /** * Default namespace. */ exports.DEFAULT_NAMESPACE = 'default'; /** * Context of snapshotting. * You can use it for separating snapshot files with modifying context.namespace. */ exports.context = { namespace: exports.DEFAULT_NAMESPACE, }; /** * Create snapshot RequestHandler. */ const snapshot = (config) => { return msw_1.http.all(config.test ?? /.*/, async (mswInfo) => { const clonedInfo = () => { return { request: mswInfo.request.clone(), cookies: { ...mswInfo.cookies }, context: exports.context, }; }; const snapshotPath = (0, node_path_1.join)(config.basePath, await createSnapshotPath(clonedInfo(), config)); // Fetch from snapshot if (!config.ignoreSnapshots && (0, node_fs_1.existsSync)(snapshotPath)) { try { const snapshot = JSON.parse((0, node_fs_1.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((0, msw_1.bypass)(mswInfo.request.clone())); const snapshot = { request: { method: mswInfo.request.method, url: mswInfo.request.url, body: new node_util_1.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 node_util_1.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' && !(0, node_fs_1.existsSync)(snapshotPath); if (shouldUpdateSnapshots) { (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(snapshotPath), { recursive: true }); (0, node_fs_1.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, }); }); }; exports.snapshot = snapshot; /** * Get sorted array of [key, val] tuple from ***#entries. */ 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. */ function getSortedEntries(iter) { return getEntries(iter).sort(([a], [b]) => a.localeCompare(b)); } ; /** * Create hash string from object. */ function toHashString(object) { return (0, node_crypto_1.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 [ exports.context.namespace, info.request.method, url.origin, url.pathname, ].join('/') + '/' + toHashString([ getSortedEntries(url.searchParams), getSortedEntries(info.request.headers), getSortedEntries(info.cookies), new node_util_1.TextDecoder('utf-8').decode(await info.request.arrayBuffer()), ]); } ; //# sourceMappingURL=index.js.map