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