msw-snapshot
Version:
Transparently create an API cache for testing.
161 lines • 6.64 kB
JavaScript
;
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