Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 6x 6x 6x 6x 60x 60x 6x 6x 6x 1x 1x 1x 1x 1x 1x 7x 7x 6x 6x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 1x 1x 1x 7x 1x 1x 1x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 1x 1x 1x 10x 10x 10x 10x 1x 1x 1x 4x 4x 1x 1x 1x 2x 1x 1x 1x 1x 1x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x | import { getCachableResponse } from '@/helpers/request';
import JSZip from 'jszip';
interface CapturedEntry {
contentType: string;
content: string;
url: string;
method: string;
timestamp: number;
}
/**
* Query parameter names that contain dynamic/session-specific data.
* These are stripped from stored URLs so that mock matching works
* across different sessions and timestamps.
* @source
*/
const DYNAMIC_PARAMS = [
'timestampe', // Macklin API (their typo for "timestamp")
'timestamp',
'_t',
'_',
't',
'nocache',
'cachebust',
'nonce',
'rnd',
'rand',
];
/**
* Strips dynamic query parameters from a URL so mock files can be
* matched across sessions regardless of timestamps, nonces, etc.
* @source
*/
function stripDynamicParams(url: string): string {
try {
const parsed = new URL(url);
for (const param of DYNAMIC_PARAMS) {
parsed.searchParams.delete(param);
}
return String(parsed);
} catch {
return url;
}
}
/**
* In-memory store for captured HTTP request/response pairs.
* Only active when built with `--mode=aggregate`.
* @source
*/
const captured = new Map<string, CapturedEntry>();
/**
* Capture a request/response pair and store it keyed by `{hostname}/{hash}.json`.
* Uses the same serialization as the MSW mock handler expects: `{ contentType, content }`.
*
* @category Helpers
* @param request - The original Request object
* @param response - A **cloned** Response (body must not be consumed)
* @source
*/
export async function addCapturedResponse(request: Request, response: Response): Promise<void> {
try {
const cacheResponse = await getCachableResponse(request, response);
const filePath = cacheResponse.hash.file; // e.g. "www.carolina.com/6dbb7f0d.json"
captured.set(filePath, {
contentType: cacheResponse.data.contentType ?? 'unknown',
content: cacheResponse.data.content ?? '',
url: request.url,
method: request.method,
timestamp: Date.now(),
});
const normalized = stripDynamicParams(request.url);
console.log(
`[ResponseAggregate] Captured ${request.method} ${request.url} → ${filePath} (${captured.size} total)`,
);
console.log(`[ResponseAggregate] normalized: ${request.method}:${normalized}`);
} catch (err) {
console.error('[ResponseAggregate] FAILED to capture:', request.method, request.url);
console.error('[ResponseAggregate] error:', err);
}
}
/**
* Download all captured responses as a zip file.
* The zip structure mirrors `src/__mocks__/responses/{hostname}/{hash}.json`.
* @category Helpers
* @source
*/
export async function downloadAsZip(): Promise<void> {
if (captured.size === 0) {
console.warn('[ResponseAggregate] No responses captured yet.');
return;
}
const zip = new JSZip();
for (const [filePath, entry] of captured) {
// Include request metadata alongside the MSW-compatible fields so
// the e2e mock route handler can match by URL without recomputing hashes.
const fileContent = JSON.stringify(
{
contentType: entry.contentType,
content: entry.content,
_request: {
url: entry.url,
method: entry.method,
},
},
null,
2,
);
zip.file(filePath, fileContent);
}
const blob = await zip.generateAsync({ type: 'blob' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `response-aggregate-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.zip`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log(`[ResponseAggregate] Downloaded zip with ${captured.size} responses.`);
}
/**
* List all captured response paths in the console.
* @category Helpers
* @source
*/
export function list(): string[] {
const paths = Array.from(captured.keys());
console.table(
Array.from(captured.entries()).map(([path, entry]) => ({
path,
method: entry.method,
url: entry.url,
contentType: entry.contentType,
})),
);
return paths;
}
/**
* Clear all captured responses.
* @category Helpers
* @source
*/
export function clear(): void {
const count = captured.size;
captured.clear();
console.log(`[ResponseAggregate] Cleared ${count} captured responses.`);
}
/**
* Get the number of captured responses.
* @category Helpers
* @source
*/
export function count(): number {
return captured.size;
}
/**
* Expose console API on `window.__responseAggregate` when in aggregate mode.
* @category Helpers
* @source
*/
export function initConsoleApi(): void {
if (typeof __RESPONSE_AGGREGATE__ !== 'undefined' && __RESPONSE_AGGREGATE__) {
const api = {
download: downloadAsZip,
list,
clear,
get count() {
return captured.size;
},
};
// window has no typed slot for our custom debug property; widen to a string-keyed record to attach it.
(window as unknown as Record<string, unknown>).__responseAggregate = api;
console.log(
'%c[ResponseAggregate] Capture mode enabled!',
'color: #4CAF50; font-weight: bold; font-size: 14px;',
);
console.log('Available commands:');
console.log(' window.__responseAggregate.download() - Download captured responses as zip');
console.log(' window.__responseAggregate.list() - List all captured responses');
console.log(' window.__responseAggregate.clear() - Clear captured responses');
console.log(' window.__responseAggregate.count - Number of captured responses');
}
}
|