@ricsam/isolate-test-utils 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/fetch-context.cjs +66 -0
- package/dist/cjs/fetch-context.cjs.map +10 -0
- package/dist/cjs/fs-context.cjs +73 -0
- package/dist/cjs/fs-context.cjs.map +10 -0
- package/dist/cjs/index.cjs +121 -0
- package/dist/cjs/index.cjs.map +10 -0
- package/{src/mock-fs.ts → dist/cjs/mock-fs.cjs} +63 -97
- package/dist/cjs/mock-fs.cjs.map +10 -0
- package/dist/cjs/native-input-test.cjs +347 -0
- package/dist/cjs/native-input-test.cjs.map +10 -0
- package/dist/cjs/package.json +5 -0
- package/dist/cjs/runtime-context.cjs +97 -0
- package/dist/cjs/runtime-context.cjs.map +10 -0
- package/dist/cjs/server.cjs +109 -0
- package/dist/cjs/server.cjs.map +10 -0
- package/dist/mjs/fetch-context.mjs +23 -0
- package/dist/mjs/fetch-context.mjs.map +10 -0
- package/dist/mjs/fs-context.mjs +30 -0
- package/dist/mjs/fs-context.mjs.map +10 -0
- package/dist/mjs/index.mjs +78 -0
- package/dist/mjs/index.mjs.map +10 -0
- package/dist/mjs/mock-fs.mjs +181 -0
- package/dist/mjs/mock-fs.mjs.map +10 -0
- package/{src/native-input-test.ts → dist/mjs/native-input-test.mjs} +60 -169
- package/dist/mjs/native-input-test.mjs.map +10 -0
- package/dist/mjs/package.json +5 -0
- package/dist/mjs/runtime-context.mjs +67 -0
- package/dist/mjs/runtime-context.mjs.map +10 -0
- package/dist/mjs/server.mjs +79 -0
- package/dist/mjs/server.mjs.map +10 -0
- package/dist/types/fetch-context.d.ts +7 -0
- package/dist/types/fs-context.d.ts +30 -0
- package/dist/types/index.d.ts +88 -0
- package/dist/types/mock-fs.d.ts +59 -0
- package/dist/types/native-input-test.d.ts +29 -0
- package/dist/types/runtime-context.d.ts +73 -0
- package/dist/types/server.d.ts +53 -0
- package/package.json +45 -18
- package/CHANGELOG.md +0 -119
- package/src/fetch-context.ts +0 -33
- package/src/fs-context.ts +0 -65
- package/src/index.test.ts +0 -472
- package/src/index.ts +0 -177
- package/src/runtime-context.ts +0 -148
- package/src/server.ts +0 -150
|
@@ -1,50 +1,14 @@
|
|
|
1
|
+
// packages/test-utils/src/native-input-test.ts
|
|
1
2
|
import ivm from "isolated-vm";
|
|
2
|
-
|
|
3
|
-
export interface TestRuntime {
|
|
4
|
-
logs: Record<string, unknown>;
|
|
5
|
-
result: unknown;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface TestRunner {
|
|
9
|
-
input(inputs: Record<string, unknown>): TestRuntime;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Run isolate code with native objects as input and capture logs
|
|
14
|
-
*
|
|
15
|
-
* This utility allows testing whether native objects passed INTO the isolate
|
|
16
|
-
* behave like isolate instances. It converts native web API classes (Headers,
|
|
17
|
-
* Request, Response, URL, Blob, File, FormData) to their isolate equivalents
|
|
18
|
-
* before executing the test code.
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* const runtime = runTestCode(ctx.context, `
|
|
22
|
-
* const headers = testingInput.headers;
|
|
23
|
-
* log("instanceof", headers instanceof Headers);
|
|
24
|
-
* log("contentType", headers.get("content-type"));
|
|
25
|
-
* `).input({
|
|
26
|
-
* headers: new Headers({ "content-type": "application/json" })
|
|
27
|
-
* });
|
|
28
|
-
*
|
|
29
|
-
* expect(runtime.logs.instanceof).toBe(true);
|
|
30
|
-
* expect(runtime.logs.contentType).toBe("application/json");
|
|
31
|
-
*/
|
|
32
|
-
export function runTestCode(context: ivm.Context, code: string): TestRunner {
|
|
3
|
+
function runTestCode(context, code) {
|
|
33
4
|
return {
|
|
34
|
-
input(inputs
|
|
35
|
-
const logs
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
(tag: string, valueJson: string) => {
|
|
41
|
-
const value = JSON.parse(valueJson);
|
|
42
|
-
logs[tag] = unmarshalFromJson(value);
|
|
43
|
-
}
|
|
44
|
-
);
|
|
5
|
+
input(inputs) {
|
|
6
|
+
const logs = {};
|
|
7
|
+
const logCallback = new ivm.Callback((tag, valueJson) => {
|
|
8
|
+
const value = JSON.parse(valueJson);
|
|
9
|
+
logs[tag] = unmarshalFromJson(value);
|
|
10
|
+
});
|
|
45
11
|
context.global.setSync("__log_callback__", logCallback);
|
|
46
|
-
|
|
47
|
-
// Create a wrapper log function that serializes values
|
|
48
12
|
context.evalSync(`
|
|
49
13
|
globalThis.log = function(tag, value) {
|
|
50
14
|
__log_callback__(tag, JSON.stringify(__serializeForLog__(value)));
|
|
@@ -120,16 +84,11 @@ export function runTestCode(context: ivm.Context, code: string): TestRunner {
|
|
|
120
84
|
return value;
|
|
121
85
|
};
|
|
122
86
|
`);
|
|
123
|
-
|
|
124
|
-
// Marshal inputs with special handling for native web API classes
|
|
125
87
|
marshalInputs(context, inputs);
|
|
126
|
-
|
|
127
|
-
// Run the code
|
|
128
|
-
let returnValue: unknown = undefined;
|
|
88
|
+
let returnValue = undefined;
|
|
129
89
|
try {
|
|
130
90
|
returnValue = context.evalSync(code);
|
|
131
91
|
} catch (error) {
|
|
132
|
-
// Clean up before re-throwing
|
|
133
92
|
context.evalSync(`
|
|
134
93
|
delete globalThis.testingInput;
|
|
135
94
|
delete globalThis.log;
|
|
@@ -138,59 +97,34 @@ export function runTestCode(context: ivm.Context, code: string): TestRunner {
|
|
|
138
97
|
`);
|
|
139
98
|
throw error;
|
|
140
99
|
}
|
|
141
|
-
|
|
142
|
-
// Cleanup
|
|
143
100
|
context.evalSync(`
|
|
144
101
|
delete globalThis.testingInput;
|
|
145
102
|
delete globalThis.log;
|
|
146
103
|
delete globalThis.__log_callback__;
|
|
147
104
|
delete globalThis.__serializeForLog__;
|
|
148
105
|
`);
|
|
149
|
-
|
|
150
106
|
return { logs, result: returnValue };
|
|
151
|
-
}
|
|
107
|
+
}
|
|
152
108
|
};
|
|
153
109
|
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Marshal inputs into the isolate, converting native web API classes
|
|
157
|
-
*/
|
|
158
|
-
function marshalInputs(
|
|
159
|
-
context: ivm.Context,
|
|
160
|
-
inputs: Record<string, unknown>
|
|
161
|
-
): void {
|
|
162
|
-
// Create the testingInput object in the isolate
|
|
110
|
+
function marshalInputs(context, inputs) {
|
|
163
111
|
context.evalSync(`globalThis.testingInput = {};`);
|
|
164
|
-
|
|
165
112
|
for (const [key, value] of Object.entries(inputs)) {
|
|
166
113
|
marshalValue(context, `testingInput.${key}`, value);
|
|
167
114
|
}
|
|
168
115
|
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Marshal a single value into the isolate at the given path
|
|
172
|
-
*/
|
|
173
|
-
function marshalValue(
|
|
174
|
-
context: ivm.Context,
|
|
175
|
-
path: string,
|
|
176
|
-
value: unknown
|
|
177
|
-
): void {
|
|
178
|
-
// Check for native Headers
|
|
116
|
+
function marshalValue(context, path, value) {
|
|
179
117
|
if (value instanceof Headers) {
|
|
180
|
-
const pairs
|
|
118
|
+
const pairs = [];
|
|
181
119
|
value.forEach((v, k) => pairs.push([k, v]));
|
|
182
120
|
const pairsJson = JSON.stringify(pairs);
|
|
183
121
|
context.evalSync(`${path} = new Headers(${pairsJson});`);
|
|
184
122
|
return;
|
|
185
123
|
}
|
|
186
|
-
|
|
187
|
-
// Check for native Request
|
|
188
124
|
if (value instanceof Request) {
|
|
189
|
-
|
|
190
|
-
const headerPairs: [string, string][] = [];
|
|
125
|
+
const headerPairs = [];
|
|
191
126
|
value.headers.forEach((v, k) => headerPairs.push([k, v]));
|
|
192
127
|
const headersJson = JSON.stringify(headerPairs);
|
|
193
|
-
|
|
194
128
|
const urlJson = JSON.stringify(value.url);
|
|
195
129
|
const methodJson = JSON.stringify(value.method);
|
|
196
130
|
const modeJson = JSON.stringify(value.mode);
|
|
@@ -200,7 +134,6 @@ function marshalValue(
|
|
|
200
134
|
const referrerJson = JSON.stringify(value.referrer);
|
|
201
135
|
const referrerPolicyJson = JSON.stringify(value.referrerPolicy);
|
|
202
136
|
const integrityJson = JSON.stringify(value.integrity);
|
|
203
|
-
|
|
204
137
|
context.evalSync(`
|
|
205
138
|
${path} = new Request(${urlJson}, {
|
|
206
139
|
method: ${methodJson},
|
|
@@ -216,16 +149,12 @@ function marshalValue(
|
|
|
216
149
|
`);
|
|
217
150
|
return;
|
|
218
151
|
}
|
|
219
|
-
|
|
220
|
-
// Check for native Response
|
|
221
152
|
if (value instanceof Response) {
|
|
222
|
-
const headerPairs
|
|
153
|
+
const headerPairs = [];
|
|
223
154
|
value.headers.forEach((v, k) => headerPairs.push([k, v]));
|
|
224
155
|
const headersJson = JSON.stringify(headerPairs);
|
|
225
|
-
|
|
226
156
|
const statusJson = JSON.stringify(value.status);
|
|
227
157
|
const statusTextJson = JSON.stringify(value.statusText);
|
|
228
|
-
|
|
229
158
|
context.evalSync(`
|
|
230
159
|
${path} = new Response(null, {
|
|
231
160
|
status: ${statusJson},
|
|
@@ -235,16 +164,12 @@ function marshalValue(
|
|
|
235
164
|
`);
|
|
236
165
|
return;
|
|
237
166
|
}
|
|
238
|
-
|
|
239
|
-
// Check for native FormData
|
|
240
167
|
if (value instanceof FormData) {
|
|
241
168
|
context.evalSync(`${path} = new FormData();`);
|
|
242
|
-
|
|
243
169
|
for (const [key, entryValue] of value.entries()) {
|
|
244
170
|
const keyJson = JSON.stringify(key);
|
|
245
|
-
|
|
246
171
|
if (typeof entryValue !== "string") {
|
|
247
|
-
const file = entryValue
|
|
172
|
+
const file = entryValue;
|
|
248
173
|
const nameJson = JSON.stringify(file.name);
|
|
249
174
|
const typeJson = JSON.stringify(file.type);
|
|
250
175
|
const lastModifiedJson = JSON.stringify(file.lastModified);
|
|
@@ -252,162 +177,128 @@ function marshalValue(
|
|
|
252
177
|
${path}.append(${keyJson}, new File([], ${nameJson}, { type: ${typeJson}, lastModified: ${lastModifiedJson} }));
|
|
253
178
|
`);
|
|
254
179
|
} else {
|
|
255
|
-
const
|
|
256
|
-
context.evalSync(`${path}.append(${keyJson}, ${
|
|
180
|
+
const valueJson2 = JSON.stringify(entryValue);
|
|
181
|
+
context.evalSync(`${path}.append(${keyJson}, ${valueJson2});`);
|
|
257
182
|
}
|
|
258
183
|
}
|
|
259
184
|
return;
|
|
260
185
|
}
|
|
261
|
-
|
|
262
|
-
// Check for native URL
|
|
263
186
|
if (value instanceof URL) {
|
|
264
187
|
const hrefJson = JSON.stringify(value.href);
|
|
265
188
|
context.evalSync(`${path} = new URL(${hrefJson});`);
|
|
266
189
|
return;
|
|
267
190
|
}
|
|
268
|
-
|
|
269
|
-
// Check for native File (before Blob, since File extends Blob)
|
|
270
191
|
if (value instanceof File) {
|
|
271
192
|
const nameJson = JSON.stringify(value.name);
|
|
272
193
|
const typeJson = JSON.stringify(value.type);
|
|
273
194
|
const lastModifiedJson = JSON.stringify(value.lastModified);
|
|
274
|
-
context.evalSync(
|
|
275
|
-
`${path} = new File([], ${nameJson}, { type: ${typeJson}, lastModified: ${lastModifiedJson} });`
|
|
276
|
-
);
|
|
195
|
+
context.evalSync(`${path} = new File([], ${nameJson}, { type: ${typeJson}, lastModified: ${lastModifiedJson} });`);
|
|
277
196
|
return;
|
|
278
197
|
}
|
|
279
|
-
|
|
280
|
-
// Check for native Blob
|
|
281
198
|
if (value instanceof Blob) {
|
|
282
199
|
const typeJson = JSON.stringify(value.type);
|
|
283
200
|
context.evalSync(`${path} = new Blob([], { type: ${typeJson} });`);
|
|
284
201
|
return;
|
|
285
202
|
}
|
|
286
|
-
|
|
287
|
-
// Handle arrays recursively
|
|
288
203
|
if (Array.isArray(value)) {
|
|
289
204
|
context.evalSync(`${path} = [];`);
|
|
290
|
-
for (let i = 0;
|
|
205
|
+
for (let i = 0;i < value.length; i++) {
|
|
291
206
|
marshalValue(context, `${path}[${i}]`, value[i]);
|
|
292
207
|
}
|
|
293
208
|
return;
|
|
294
209
|
}
|
|
295
|
-
|
|
296
|
-
// Handle plain objects recursively
|
|
297
210
|
if (value && typeof value === "object" && value.constructor === Object) {
|
|
298
211
|
context.evalSync(`${path} = {};`);
|
|
299
212
|
for (const [key, val] of Object.entries(value)) {
|
|
300
|
-
// Use bracket notation for safe property access
|
|
301
213
|
marshalValue(context, `${path}[${JSON.stringify(key)}]`, val);
|
|
302
214
|
}
|
|
303
215
|
return;
|
|
304
216
|
}
|
|
305
|
-
|
|
306
|
-
// For primitives, set directly via JSON
|
|
307
217
|
const valueJson = JSON.stringify(value);
|
|
308
218
|
context.evalSync(`${path} = ${valueJson};`);
|
|
309
219
|
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Unmarshal a value from JSON, converting special __type__ markers back to native instances
|
|
313
|
-
*/
|
|
314
|
-
function unmarshalFromJson(value: unknown): unknown {
|
|
220
|
+
function unmarshalFromJson(value) {
|
|
315
221
|
if (value === null || value === undefined) {
|
|
316
222
|
return value;
|
|
317
223
|
}
|
|
318
|
-
|
|
319
224
|
if (Array.isArray(value)) {
|
|
320
225
|
return value.map((v) => unmarshalFromJson(v));
|
|
321
226
|
}
|
|
322
|
-
|
|
323
227
|
if (typeof value === "object") {
|
|
324
|
-
const obj = value
|
|
325
|
-
|
|
326
|
-
// Check for special type markers
|
|
228
|
+
const obj = value;
|
|
327
229
|
if (obj.__type__ === "Headers") {
|
|
328
|
-
const pairs = obj.pairs
|
|
329
|
-
const headers = new Headers
|
|
230
|
+
const pairs = obj.pairs;
|
|
231
|
+
const headers = new Headers;
|
|
330
232
|
for (const [k, v] of pairs) {
|
|
331
233
|
headers.append(k, v);
|
|
332
234
|
}
|
|
333
235
|
return headers;
|
|
334
236
|
}
|
|
335
|
-
|
|
336
237
|
if (obj.__type__ === "Request") {
|
|
337
|
-
const headers = new Headers
|
|
338
|
-
for (const [k, v] of obj.headers
|
|
238
|
+
const headers = new Headers;
|
|
239
|
+
for (const [k, v] of obj.headers) {
|
|
339
240
|
headers.append(k, v);
|
|
340
241
|
}
|
|
341
|
-
return new Request(obj.url
|
|
342
|
-
method: obj.method
|
|
242
|
+
return new Request(obj.url, {
|
|
243
|
+
method: obj.method,
|
|
343
244
|
headers,
|
|
344
|
-
mode: obj.mode
|
|
345
|
-
credentials: obj.credentials
|
|
346
|
-
cache: obj.cache
|
|
347
|
-
redirect: obj.redirect
|
|
348
|
-
referrer: obj.referrer
|
|
349
|
-
referrerPolicy: obj.referrerPolicy
|
|
350
|
-
integrity: obj.integrity
|
|
245
|
+
mode: obj.mode,
|
|
246
|
+
credentials: obj.credentials,
|
|
247
|
+
cache: obj.cache,
|
|
248
|
+
redirect: obj.redirect,
|
|
249
|
+
referrer: obj.referrer,
|
|
250
|
+
referrerPolicy: obj.referrerPolicy,
|
|
251
|
+
integrity: obj.integrity
|
|
351
252
|
});
|
|
352
253
|
}
|
|
353
|
-
|
|
354
254
|
if (obj.__type__ === "Response") {
|
|
355
|
-
const headers = new Headers
|
|
356
|
-
for (const [k, v] of obj.headers
|
|
255
|
+
const headers = new Headers;
|
|
256
|
+
for (const [k, v] of obj.headers) {
|
|
357
257
|
headers.append(k, v);
|
|
358
258
|
}
|
|
359
259
|
return new Response(null, {
|
|
360
|
-
status: obj.status
|
|
361
|
-
statusText: obj.statusText
|
|
362
|
-
headers
|
|
260
|
+
status: obj.status,
|
|
261
|
+
statusText: obj.statusText,
|
|
262
|
+
headers
|
|
363
263
|
});
|
|
364
264
|
}
|
|
365
|
-
|
|
366
265
|
if (obj.__type__ === "FormData") {
|
|
367
|
-
const formData = new FormData
|
|
368
|
-
for (const [k, v] of obj.entries
|
|
369
|
-
if (
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
formData.append(
|
|
376
|
-
k,
|
|
377
|
-
new File([], fileObj.name as string, {
|
|
378
|
-
type: fileObj.type as string,
|
|
379
|
-
lastModified: fileObj.lastModified as number,
|
|
380
|
-
})
|
|
381
|
-
);
|
|
266
|
+
const formData = new FormData;
|
|
267
|
+
for (const [k, v] of obj.entries) {
|
|
268
|
+
if (typeof v === "object" && v !== null && v.__type__ === "File") {
|
|
269
|
+
const fileObj = v;
|
|
270
|
+
formData.append(k, new File([], fileObj.name, {
|
|
271
|
+
type: fileObj.type,
|
|
272
|
+
lastModified: fileObj.lastModified
|
|
273
|
+
}));
|
|
382
274
|
} else {
|
|
383
|
-
formData.append(k, v
|
|
275
|
+
formData.append(k, v);
|
|
384
276
|
}
|
|
385
277
|
}
|
|
386
278
|
return formData;
|
|
387
279
|
}
|
|
388
|
-
|
|
389
280
|
if (obj.__type__ === "URL") {
|
|
390
|
-
return new URL(obj.href
|
|
281
|
+
return new URL(obj.href);
|
|
391
282
|
}
|
|
392
|
-
|
|
393
283
|
if (obj.__type__ === "File") {
|
|
394
|
-
return new File([], obj.name
|
|
395
|
-
type: obj.type
|
|
396
|
-
lastModified: obj.lastModified
|
|
284
|
+
return new File([], obj.name, {
|
|
285
|
+
type: obj.type,
|
|
286
|
+
lastModified: obj.lastModified
|
|
397
287
|
});
|
|
398
288
|
}
|
|
399
|
-
|
|
400
289
|
if (obj.__type__ === "Blob") {
|
|
401
|
-
return new Blob([], { type: obj.type
|
|
290
|
+
return new Blob([], { type: obj.type });
|
|
402
291
|
}
|
|
403
|
-
|
|
404
|
-
// Plain object - recursively unmarshal properties
|
|
405
|
-
const result: Record<string, unknown> = {};
|
|
292
|
+
const result = {};
|
|
406
293
|
for (const [k, v] of Object.entries(obj)) {
|
|
407
294
|
result[k] = unmarshalFromJson(v);
|
|
408
295
|
}
|
|
409
296
|
return result;
|
|
410
297
|
}
|
|
411
|
-
|
|
412
298
|
return value;
|
|
413
299
|
}
|
|
300
|
+
export {
|
|
301
|
+
runTestCode
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
//# debugId=EDB1526900BA89BD64756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/native-input-test.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import ivm from \"isolated-vm\";\n\nexport interface TestRuntime {\n logs: Record<string, unknown>;\n result: unknown;\n}\n\nexport interface TestRunner {\n input(inputs: Record<string, unknown>): TestRuntime;\n}\n\n/**\n * Run isolate code with native objects as input and capture logs\n *\n * This utility allows testing whether native objects passed INTO the isolate\n * behave like isolate instances. It converts native web API classes (Headers,\n * Request, Response, URL, Blob, File, FormData) to their isolate equivalents\n * before executing the test code.\n *\n * @example\n * const runtime = runTestCode(ctx.context, `\n * const headers = testingInput.headers;\n * log(\"instanceof\", headers instanceof Headers);\n * log(\"contentType\", headers.get(\"content-type\"));\n * `).input({\n * headers: new Headers({ \"content-type\": \"application/json\" })\n * });\n *\n * expect(runtime.logs.instanceof).toBe(true);\n * expect(runtime.logs.contentType).toBe(\"application/json\");\n */\nexport function runTestCode(context: ivm.Context, code: string): TestRunner {\n return {\n input(inputs: Record<string, unknown>): TestRuntime {\n const logs: Record<string, unknown> = {};\n\n // Setup log capture - log(tag, value) stores as logs[tag] = value\n // Values are unmarshalled back to native types for bidirectional testing\n const logCallback = new ivm.Callback(\n (tag: string, valueJson: string) => {\n const value = JSON.parse(valueJson);\n logs[tag] = unmarshalFromJson(value);\n }\n );\n context.global.setSync(\"__log_callback__\", logCallback);\n\n // Create a wrapper log function that serializes values\n context.evalSync(`\n globalThis.log = function(tag, value) {\n __log_callback__(tag, JSON.stringify(__serializeForLog__(value)));\n };\n\n globalThis.__serializeForLog__ = function(value) {\n if (typeof Headers !== 'undefined' && value instanceof Headers) {\n const pairs = [];\n for (const [k, v] of value) pairs.push([k, v]);\n return { __type__: 'Headers', pairs };\n }\n if (typeof Request !== 'undefined' && value instanceof Request) {\n const headers = [];\n for (const [k, v] of value.headers) headers.push([k, v]);\n return {\n __type__: 'Request',\n url: value.url,\n method: value.method,\n headers,\n mode: value.mode,\n credentials: value.credentials,\n cache: value.cache,\n redirect: value.redirect,\n referrer: value.referrer,\n referrerPolicy: value.referrerPolicy,\n integrity: value.integrity,\n };\n }\n if (typeof Response !== 'undefined' && value instanceof Response) {\n const headers = [];\n for (const [k, v] of value.headers) headers.push([k, v]);\n return {\n __type__: 'Response',\n status: value.status,\n statusText: value.statusText,\n ok: value.ok,\n headers,\n type: value.type,\n redirected: value.redirected,\n url: value.url,\n };\n }\n if (typeof FormData !== 'undefined' && value instanceof FormData) {\n const entries = [];\n for (const [k, v] of value) {\n if (typeof File !== 'undefined' && v instanceof File) {\n entries.push([k, { __type__: 'File', name: v.name, type: v.type, lastModified: v.lastModified }]);\n } else {\n entries.push([k, v]);\n }\n }\n return { __type__: 'FormData', entries };\n }\n if (value instanceof URL) {\n return { __type__: 'URL', href: value.href };\n }\n if (value instanceof File) {\n return { __type__: 'File', name: value.name, type: value.type, lastModified: value.lastModified };\n }\n if (value instanceof Blob) {\n return { __type__: 'Blob', type: value.type, size: value.size };\n }\n if (Array.isArray(value)) {\n return value.map(v => __serializeForLog__(v));\n }\n if (value && typeof value === 'object' && value.constructor === Object) {\n const result = {};\n for (const [k, v] of Object.entries(value)) {\n result[k] = __serializeForLog__(v);\n }\n return result;\n }\n return value;\n };\n `);\n\n // Marshal inputs with special handling for native web API classes\n marshalInputs(context, inputs);\n\n // Run the code\n let returnValue: unknown = undefined;\n try {\n returnValue = context.evalSync(code);\n } catch (error) {\n // Clean up before re-throwing\n context.evalSync(`\n delete globalThis.testingInput;\n delete globalThis.log;\n delete globalThis.__log_callback__;\n delete globalThis.__serializeForLog__;\n `);\n throw error;\n }\n\n // Cleanup\n context.evalSync(`\n delete globalThis.testingInput;\n delete globalThis.log;\n delete globalThis.__log_callback__;\n delete globalThis.__serializeForLog__;\n `);\n\n return { logs, result: returnValue };\n },\n };\n}\n\n/**\n * Marshal inputs into the isolate, converting native web API classes\n */\nfunction marshalInputs(\n context: ivm.Context,\n inputs: Record<string, unknown>\n): void {\n // Create the testingInput object in the isolate\n context.evalSync(`globalThis.testingInput = {};`);\n\n for (const [key, value] of Object.entries(inputs)) {\n marshalValue(context, `testingInput.${key}`, value);\n }\n}\n\n/**\n * Marshal a single value into the isolate at the given path\n */\nfunction marshalValue(\n context: ivm.Context,\n path: string,\n value: unknown\n): void {\n // Check for native Headers\n if (value instanceof Headers) {\n const pairs: [string, string][] = [];\n value.forEach((v, k) => pairs.push([k, v]));\n const pairsJson = JSON.stringify(pairs);\n context.evalSync(`${path} = new Headers(${pairsJson});`);\n return;\n }\n\n // Check for native Request\n if (value instanceof Request) {\n // First marshal the headers\n const headerPairs: [string, string][] = [];\n value.headers.forEach((v, k) => headerPairs.push([k, v]));\n const headersJson = JSON.stringify(headerPairs);\n\n const urlJson = JSON.stringify(value.url);\n const methodJson = JSON.stringify(value.method);\n const modeJson = JSON.stringify(value.mode);\n const credentialsJson = JSON.stringify(value.credentials);\n const cacheJson = JSON.stringify(value.cache);\n const redirectJson = JSON.stringify(value.redirect);\n const referrerJson = JSON.stringify(value.referrer);\n const referrerPolicyJson = JSON.stringify(value.referrerPolicy);\n const integrityJson = JSON.stringify(value.integrity);\n\n context.evalSync(`\n ${path} = new Request(${urlJson}, {\n method: ${methodJson},\n headers: new Headers(${headersJson}),\n mode: ${modeJson},\n credentials: ${credentialsJson},\n cache: ${cacheJson},\n redirect: ${redirectJson},\n referrer: ${referrerJson},\n referrerPolicy: ${referrerPolicyJson},\n integrity: ${integrityJson},\n });\n `);\n return;\n }\n\n // Check for native Response\n if (value instanceof Response) {\n const headerPairs: [string, string][] = [];\n value.headers.forEach((v, k) => headerPairs.push([k, v]));\n const headersJson = JSON.stringify(headerPairs);\n\n const statusJson = JSON.stringify(value.status);\n const statusTextJson = JSON.stringify(value.statusText);\n\n context.evalSync(`\n ${path} = new Response(null, {\n status: ${statusJson},\n statusText: ${statusTextJson},\n headers: new Headers(${headersJson}),\n });\n `);\n return;\n }\n\n // Check for native FormData\n if (value instanceof FormData) {\n context.evalSync(`${path} = new FormData();`);\n\n for (const [key, entryValue] of value.entries()) {\n const keyJson = JSON.stringify(key);\n\n if (typeof entryValue !== \"string\") {\n const file = entryValue as File;\n const nameJson = JSON.stringify(file.name);\n const typeJson = JSON.stringify(file.type);\n const lastModifiedJson = JSON.stringify(file.lastModified);\n context.evalSync(`\n ${path}.append(${keyJson}, new File([], ${nameJson}, { type: ${typeJson}, lastModified: ${lastModifiedJson} }));\n `);\n } else {\n const valueJson = JSON.stringify(entryValue);\n context.evalSync(`${path}.append(${keyJson}, ${valueJson});`);\n }\n }\n return;\n }\n\n // Check for native URL\n if (value instanceof URL) {\n const hrefJson = JSON.stringify(value.href);\n context.evalSync(`${path} = new URL(${hrefJson});`);\n return;\n }\n\n // Check for native File (before Blob, since File extends Blob)\n if (value instanceof File) {\n const nameJson = JSON.stringify(value.name);\n const typeJson = JSON.stringify(value.type);\n const lastModifiedJson = JSON.stringify(value.lastModified);\n context.evalSync(\n `${path} = new File([], ${nameJson}, { type: ${typeJson}, lastModified: ${lastModifiedJson} });`\n );\n return;\n }\n\n // Check for native Blob\n if (value instanceof Blob) {\n const typeJson = JSON.stringify(value.type);\n context.evalSync(`${path} = new Blob([], { type: ${typeJson} });`);\n return;\n }\n\n // Handle arrays recursively\n if (Array.isArray(value)) {\n context.evalSync(`${path} = [];`);\n for (let i = 0; i < value.length; i++) {\n marshalValue(context, `${path}[${i}]`, value[i]);\n }\n return;\n }\n\n // Handle plain objects recursively\n if (value && typeof value === \"object\" && value.constructor === Object) {\n context.evalSync(`${path} = {};`);\n for (const [key, val] of Object.entries(value)) {\n // Use bracket notation for safe property access\n marshalValue(context, `${path}[${JSON.stringify(key)}]`, val);\n }\n return;\n }\n\n // For primitives, set directly via JSON\n const valueJson = JSON.stringify(value);\n context.evalSync(`${path} = ${valueJson};`);\n}\n\n/**\n * Unmarshal a value from JSON, converting special __type__ markers back to native instances\n */\nfunction unmarshalFromJson(value: unknown): unknown {\n if (value === null || value === undefined) {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map((v) => unmarshalFromJson(v));\n }\n\n if (typeof value === \"object\") {\n const obj = value as Record<string, unknown>;\n\n // Check for special type markers\n if (obj.__type__ === \"Headers\") {\n const pairs = obj.pairs as [string, string][];\n const headers = new Headers();\n for (const [k, v] of pairs) {\n headers.append(k, v);\n }\n return headers;\n }\n\n if (obj.__type__ === \"Request\") {\n const headers = new Headers();\n for (const [k, v] of obj.headers as [string, string][]) {\n headers.append(k, v);\n }\n return new Request(obj.url as string, {\n method: obj.method as string,\n headers,\n mode: obj.mode as Request[\"mode\"],\n credentials: obj.credentials as Request[\"credentials\"],\n cache: obj.cache as Request[\"cache\"],\n redirect: obj.redirect as Request[\"redirect\"],\n referrer: obj.referrer as string,\n referrerPolicy: obj.referrerPolicy as Request[\"referrerPolicy\"],\n integrity: obj.integrity as string,\n });\n }\n\n if (obj.__type__ === \"Response\") {\n const headers = new Headers();\n for (const [k, v] of obj.headers as [string, string][]) {\n headers.append(k, v);\n }\n return new Response(null, {\n status: obj.status as number,\n statusText: obj.statusText as string,\n headers,\n });\n }\n\n if (obj.__type__ === \"FormData\") {\n const formData = new FormData();\n for (const [k, v] of obj.entries as [string, unknown][]) {\n if (\n typeof v === \"object\" &&\n v !== null &&\n (v as Record<string, unknown>).__type__ === \"File\"\n ) {\n const fileObj = v as Record<string, unknown>;\n formData.append(\n k,\n new File([], fileObj.name as string, {\n type: fileObj.type as string,\n lastModified: fileObj.lastModified as number,\n })\n );\n } else {\n formData.append(k, v as string);\n }\n }\n return formData;\n }\n\n if (obj.__type__ === \"URL\") {\n return new URL(obj.href as string);\n }\n\n if (obj.__type__ === \"File\") {\n return new File([], obj.name as string, {\n type: obj.type as string,\n lastModified: obj.lastModified as number,\n });\n }\n\n if (obj.__type__ === \"Blob\") {\n return new Blob([], { type: obj.type as string });\n }\n\n // Plain object - recursively unmarshal properties\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) {\n result[k] = unmarshalFromJson(v);\n }\n return result;\n }\n\n return value;\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "EDB1526900BA89BD64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// packages/test-utils/src/runtime-context.ts
|
|
2
|
+
import { MockFileSystem } from "./mock-fs.mjs";
|
|
3
|
+
import { createRuntime } from "@ricsam/isolate-runtime";
|
|
4
|
+
import { clearAllInstanceState } from "@ricsam/isolate-core";
|
|
5
|
+
async function createRuntimeTestContext(options) {
|
|
6
|
+
const opts = options ?? {};
|
|
7
|
+
clearAllInstanceState();
|
|
8
|
+
const logs = [];
|
|
9
|
+
const fetchCalls = [];
|
|
10
|
+
let mockResponse = { status: 200, body: "" };
|
|
11
|
+
let storedResult = undefined;
|
|
12
|
+
const mockFs = new MockFileSystem;
|
|
13
|
+
const runtime = await createRuntime({
|
|
14
|
+
console: {
|
|
15
|
+
onEntry: (entry) => {
|
|
16
|
+
if (entry.type === "output") {
|
|
17
|
+
logs.push({ level: entry.level, stdout: entry.stdout });
|
|
18
|
+
} else if (entry.type === "assert") {
|
|
19
|
+
logs.push({ level: "error", stdout: entry.stdout });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
fetch: async (request) => {
|
|
24
|
+
fetchCalls.push({
|
|
25
|
+
url: request.url,
|
|
26
|
+
method: request.method,
|
|
27
|
+
headers: [...request.headers.entries()]
|
|
28
|
+
});
|
|
29
|
+
return new Response(mockResponse.body ?? "", {
|
|
30
|
+
status: mockResponse.status ?? 200,
|
|
31
|
+
headers: mockResponse.headers
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
fs: opts.fs ? { getDirectory: async () => mockFs } : undefined,
|
|
35
|
+
customFunctions: {
|
|
36
|
+
setResult: {
|
|
37
|
+
fn: (value) => {
|
|
38
|
+
storedResult = value;
|
|
39
|
+
},
|
|
40
|
+
type: "sync"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
eval: runtime.eval.bind(runtime),
|
|
46
|
+
clearTimers: runtime.timers.clearAll.bind(runtime.timers),
|
|
47
|
+
dispatchRequest: runtime.fetch.dispatchRequest.bind(runtime.fetch),
|
|
48
|
+
dispose: runtime.dispose.bind(runtime),
|
|
49
|
+
logs,
|
|
50
|
+
fetchCalls,
|
|
51
|
+
setMockResponse(response) {
|
|
52
|
+
mockResponse = response;
|
|
53
|
+
},
|
|
54
|
+
mockFs,
|
|
55
|
+
getResult() {
|
|
56
|
+
return storedResult;
|
|
57
|
+
},
|
|
58
|
+
clearResult() {
|
|
59
|
+
storedResult = undefined;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
createRuntimeTestContext
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
//# debugId=D0AF38671454769564756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/runtime-context.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { MockFileSystem } from \"./mock-fs.mjs\";\nimport { createRuntime } from \"@ricsam/isolate-runtime\";\nimport { clearAllInstanceState } from \"@ricsam/isolate-core\";\n\nexport interface MockResponse {\n status?: number;\n body?: string;\n headers?: Record<string, string>;\n}\n\nexport interface RuntimeTestContextOptions {\n /** Enable file system APIs with mock file system */\n fs?: boolean;\n}\n\nexport interface RuntimeTestContext {\n /** Execute code in the runtime (ES module mode, supports top-level await) */\n eval(code: string): Promise<void>;\n /** Clear all pending timers */\n clearTimers(): void;\n /** Dispatch an HTTP request to the serve() handler */\n dispatchRequest(request: Request): Promise<Response>;\n /** Dispose all resources */\n dispose(): Promise<void>;\n /** Captured console.log calls */\n logs: Array<{ level: string; stdout: string }>;\n /** Captured fetch calls */\n fetchCalls: Array<{ url: string; method: string; headers: [string, string][] }>;\n /** Set the mock response for the next fetch call */\n setMockResponse(response: MockResponse): void;\n /** Mock file system (only available if fs option is true) */\n mockFs: MockFileSystem;\n /**\n * Get a result from the isolate. Call `await setResult(value)` in your eval code\n * to pass a value back to the host.\n */\n getResult<T = unknown>(): T | undefined;\n /** Clear the stored result */\n clearResult(): void;\n}\n\n/**\n * Create a full runtime test context with all APIs set up.\n * Includes console logging capture, fetch mocking, and optionally file system.\n *\n * @example\n * const ctx = await createRuntimeTestContext({ fs: true });\n *\n * // Set up mock response for fetch\n * ctx.setMockResponse({ status: 200, body: '{\"data\": \"test\"}' });\n *\n * // Run code and pass result back via setResult\n * await ctx.eval(`\n * console.log(\"Starting fetch...\");\n * const response = await fetch(\"https://api.example.com/data\");\n * const data = await response.json();\n * console.log(\"Got data:\", data);\n * setResult(data);\n * `);\n *\n * // Get the result\n * console.log(ctx.getResult()); // { data: \"test\" }\n *\n * // Check logs\n * console.log(ctx.logs); // [{ level: \"log\", stdout: \"Starting fetch...\" }, ...]\n *\n * // Check fetch calls\n * console.log(ctx.fetchCalls); // [{ url: \"https://api.example.com/data\", method: \"GET\", ... }]\n *\n * await ctx.dispose();\n */\nexport async function createRuntimeTestContext(\n options?: RuntimeTestContextOptions\n): Promise<RuntimeTestContext> {\n const opts = options ?? {};\n\n // Clear any previous instance state\n clearAllInstanceState();\n\n // State for capturing logs and fetch calls\n const logs: Array<{ level: string; stdout: string }> = [];\n const fetchCalls: Array<{\n url: string;\n method: string;\n headers: [string, string][];\n }> = [];\n\n let mockResponse: MockResponse = { status: 200, body: \"\" };\n let storedResult: unknown = undefined;\n\n // Create mock file system\n const mockFs = new MockFileSystem();\n\n // Create runtime with configured handlers\n const runtime = await createRuntime({\n console: {\n onEntry: (entry) => {\n if (entry.type === \"output\") {\n logs.push({ level: entry.level, stdout: entry.stdout });\n } else if (entry.type === \"assert\") {\n logs.push({ level: \"error\", stdout: entry.stdout });\n }\n },\n },\n fetch: async (request: Request) => {\n // Capture fetch call\n fetchCalls.push({\n url: request.url,\n method: request.method,\n headers: [...request.headers.entries()],\n });\n\n // Return mock response\n return new Response(mockResponse.body ?? \"\", {\n status: mockResponse.status ?? 200,\n headers: mockResponse.headers,\n });\n },\n fs: opts.fs ? { getDirectory: async () => mockFs } : undefined,\n customFunctions: {\n setResult: {\n fn: (value: unknown) => {\n storedResult = value;\n },\n type: 'sync',\n },\n },\n });\n\n return {\n eval: runtime.eval.bind(runtime),\n clearTimers: runtime.timers.clearAll.bind(runtime.timers),\n dispatchRequest: runtime.fetch.dispatchRequest.bind(runtime.fetch),\n dispose: runtime.dispose.bind(runtime),\n logs,\n fetchCalls,\n setMockResponse(response: MockResponse) {\n mockResponse = response;\n },\n mockFs,\n getResult<T = unknown>(): T | undefined {\n return storedResult as T | undefined;\n },\n clearResult() {\n storedResult = undefined;\n },\n };\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";AAAA;AACA;AACA;AAqEA,eAAsB,wBAAwB,CAC5C,SAC6B;AAAA,EAC7B,MAAM,OAAO,WAAW,CAAC;AAAA,EAGzB,sBAAsB;AAAA,EAGtB,MAAM,OAAiD,CAAC;AAAA,EACxD,MAAM,aAID,CAAC;AAAA,EAEN,IAAI,eAA6B,EAAE,QAAQ,KAAK,MAAM,GAAG;AAAA,EACzD,IAAI,eAAwB;AAAA,EAG5B,MAAM,SAAS,IAAI;AAAA,EAGnB,MAAM,UAAU,MAAM,cAAc;AAAA,IAClC,SAAS;AAAA,MACP,SAAS,CAAC,UAAU;AAAA,QAClB,IAAI,MAAM,SAAS,UAAU;AAAA,UAC3B,KAAK,KAAK,EAAE,OAAO,MAAM,OAAO,QAAQ,MAAM,OAAO,CAAC;AAAA,QACxD,EAAO,SAAI,MAAM,SAAS,UAAU;AAAA,UAClC,KAAK,KAAK,EAAE,OAAO,SAAS,QAAQ,MAAM,OAAO,CAAC;AAAA,QACpD;AAAA;AAAA,IAEJ;AAAA,IACA,OAAO,OAAO,YAAqB;AAAA,MAEjC,WAAW,KAAK;AAAA,QACd,KAAK,QAAQ;AAAA,QACb,QAAQ,QAAQ;AAAA,QAChB,SAAS,CAAC,GAAG,QAAQ,QAAQ,QAAQ,CAAC;AAAA,MACxC,CAAC;AAAA,MAGD,OAAO,IAAI,SAAS,aAAa,QAAQ,IAAI;AAAA,QAC3C,QAAQ,aAAa,UAAU;AAAA,QAC/B,SAAS,aAAa;AAAA,MACxB,CAAC;AAAA;AAAA,IAEH,IAAI,KAAK,KAAK,EAAE,cAAc,YAAY,OAAO,IAAI;AAAA,IACrD,iBAAiB;AAAA,MACf,WAAW;AAAA,QACT,IAAI,CAAC,UAAmB;AAAA,UACtB,eAAe;AAAA;AAAA,QAEjB,MAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF,CAAC;AAAA,EAED,OAAO;AAAA,IACL,MAAM,QAAQ,KAAK,KAAK,OAAO;AAAA,IAC/B,aAAa,QAAQ,OAAO,SAAS,KAAK,QAAQ,MAAM;AAAA,IACxD,iBAAiB,QAAQ,MAAM,gBAAgB,KAAK,QAAQ,KAAK;AAAA,IACjE,SAAS,QAAQ,QAAQ,KAAK,OAAO;AAAA,IACrC;AAAA,IACA;AAAA,IACA,eAAe,CAAC,UAAwB;AAAA,MACtC,eAAe;AAAA;AAAA,IAEjB;AAAA,IACA,SAAsB,GAAkB;AAAA,MACtC,OAAO;AAAA;AAAA,IAET,WAAW,GAAG;AAAA,MACZ,eAAe;AAAA;AAAA,EAEnB;AAAA;",
|
|
8
|
+
"debugId": "D0AF38671454769564756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// packages/test-utils/src/server.ts
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
async function startIntegrationServer(port) {
|
|
4
|
+
const responses = new Map;
|
|
5
|
+
const requests = [];
|
|
6
|
+
let defaultResponse = { status: 404, body: "Not Found" };
|
|
7
|
+
const server = createServer(async (req, res) => {
|
|
8
|
+
const path = req.url ?? "/";
|
|
9
|
+
const method = req.method ?? "GET";
|
|
10
|
+
const chunks = [];
|
|
11
|
+
for await (const chunk of req) {
|
|
12
|
+
chunks.push(chunk);
|
|
13
|
+
}
|
|
14
|
+
const body = chunks.length > 0 ? Buffer.concat(chunks).toString() : undefined;
|
|
15
|
+
const headers = {};
|
|
16
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
17
|
+
if (typeof value === "string") {
|
|
18
|
+
headers[key] = value;
|
|
19
|
+
} else if (Array.isArray(value)) {
|
|
20
|
+
headers[key] = value.join(", ");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
requests.push({ method, path, headers, body });
|
|
24
|
+
const mockResponse = responses.get(path) ?? defaultResponse;
|
|
25
|
+
res.statusCode = mockResponse.status ?? 200;
|
|
26
|
+
if (mockResponse.headers) {
|
|
27
|
+
for (const [key, value] of Object.entries(mockResponse.headers)) {
|
|
28
|
+
res.setHeader(key, value);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
res.end(mockResponse.body ?? "");
|
|
32
|
+
});
|
|
33
|
+
const actualPort = await new Promise((resolve, reject) => {
|
|
34
|
+
server.listen(port ?? 0, () => {
|
|
35
|
+
const address = server.address();
|
|
36
|
+
if (address && typeof address === "object") {
|
|
37
|
+
resolve(address.port);
|
|
38
|
+
} else {
|
|
39
|
+
reject(new Error("Failed to get server address"));
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
server.on("error", reject);
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
url: `http://localhost:${actualPort}`,
|
|
46
|
+
port: actualPort,
|
|
47
|
+
async close() {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
server.close((err) => {
|
|
50
|
+
if (err)
|
|
51
|
+
reject(err);
|
|
52
|
+
else
|
|
53
|
+
resolve();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
setResponse(path, response) {
|
|
58
|
+
responses.set(path, response);
|
|
59
|
+
},
|
|
60
|
+
setDefaultResponse(response) {
|
|
61
|
+
defaultResponse = response;
|
|
62
|
+
},
|
|
63
|
+
getRequests() {
|
|
64
|
+
return [...requests];
|
|
65
|
+
},
|
|
66
|
+
clearRequests() {
|
|
67
|
+
requests.length = 0;
|
|
68
|
+
},
|
|
69
|
+
clearResponses() {
|
|
70
|
+
responses.clear();
|
|
71
|
+
defaultResponse = { status: 404, body: "Not Found" };
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
startIntegrationServer
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
//# debugId=9B0013141DCE45BE64756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/server.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { createServer, type Server, type IncomingMessage, type ServerResponse } from \"node:http\";\n\nexport interface MockServerResponse {\n status?: number;\n body?: string;\n headers?: Record<string, string>;\n}\n\nexport interface RecordedRequest {\n method: string;\n path: string;\n headers: Record<string, string>;\n body?: string;\n}\n\nexport interface IntegrationServer {\n /** The base URL of the server (e.g., \"http://localhost:3000\") */\n url: string;\n /** The port the server is listening on */\n port: number;\n /** Close the server */\n close(): Promise<void>;\n /** Set the response for a specific path */\n setResponse(path: string, response: MockServerResponse): void;\n /** Set a default response for any unmatched path */\n setDefaultResponse(response: MockServerResponse): void;\n /** Get all recorded requests */\n getRequests(): RecordedRequest[];\n /** Clear all recorded requests */\n clearRequests(): void;\n /** Clear all configured responses */\n clearResponses(): void;\n}\n\n/**\n * Start an HTTP server for integration tests.\n * Useful for testing fetch operations against a real server.\n *\n * @example\n * const server = await startIntegrationServer();\n *\n * server.setResponse(\"/api/data\", {\n * status: 200,\n * body: JSON.stringify({ message: \"Hello\" }),\n * headers: { \"Content-Type\": \"application/json\" }\n * });\n *\n * // In your test\n * const response = await fetch(`${server.url}/api/data`);\n * const data = await response.json();\n *\n * // Check what requests were made\n * const requests = server.getRequests();\n * console.log(requests[0].path); // \"/api/data\"\n *\n * await server.close();\n */\nexport async function startIntegrationServer(\n port?: number\n): Promise<IntegrationServer> {\n const responses = new Map<string, MockServerResponse>();\n const requests: RecordedRequest[] = [];\n let defaultResponse: MockServerResponse = { status: 404, body: \"Not Found\" };\n\n const server: Server = createServer(\n async (req: IncomingMessage, res: ServerResponse) => {\n const path = req.url ?? \"/\";\n const method = req.method ?? \"GET\";\n\n // Read request body\n const chunks: Buffer[] = [];\n for await (const chunk of req) {\n chunks.push(chunk as Buffer);\n }\n const body = chunks.length > 0 ? Buffer.concat(chunks).toString() : undefined;\n\n // Record the request\n const headers: Record<string, string> = {};\n for (const [key, value] of Object.entries(req.headers)) {\n if (typeof value === \"string\") {\n headers[key] = value;\n } else if (Array.isArray(value)) {\n headers[key] = value.join(\", \");\n }\n }\n requests.push({ method, path, headers, body });\n\n // Find and send response\n const mockResponse = responses.get(path) ?? defaultResponse;\n\n res.statusCode = mockResponse.status ?? 200;\n\n if (mockResponse.headers) {\n for (const [key, value] of Object.entries(mockResponse.headers)) {\n res.setHeader(key, value);\n }\n }\n\n res.end(mockResponse.body ?? \"\");\n }\n );\n\n // Find an available port\n const actualPort = await new Promise<number>((resolve, reject) => {\n server.listen(port ?? 0, () => {\n const address = server.address();\n if (address && typeof address === \"object\") {\n resolve(address.port);\n } else {\n reject(new Error(\"Failed to get server address\"));\n }\n });\n server.on(\"error\", reject);\n });\n\n return {\n url: `http://localhost:${actualPort}`,\n port: actualPort,\n\n async close() {\n return new Promise((resolve, reject) => {\n server.close((err) => {\n if (err) reject(err);\n else resolve();\n });\n });\n },\n\n setResponse(path: string, response: MockServerResponse) {\n responses.set(path, response);\n },\n\n setDefaultResponse(response: MockServerResponse) {\n defaultResponse = response;\n },\n\n getRequests() {\n return [...requests];\n },\n\n clearRequests() {\n requests.length = 0;\n },\n\n clearResponses() {\n responses.clear();\n defaultResponse = { status: 404, body: \"Not Found\" };\n },\n };\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";AAAA;AAyDA,eAAsB,sBAAsB,CAC1C,MAC4B;AAAA,EAC5B,MAAM,YAAY,IAAI;AAAA,EACtB,MAAM,WAA8B,CAAC;AAAA,EACrC,IAAI,kBAAsC,EAAE,QAAQ,KAAK,MAAM,YAAY;AAAA,EAE3E,MAAM,SAAiB,aACrB,OAAO,KAAsB,QAAwB;AAAA,IACnD,MAAM,OAAO,IAAI,OAAO;AAAA,IACxB,MAAM,SAAS,IAAI,UAAU;AAAA,IAG7B,MAAM,SAAmB,CAAC;AAAA,IAC1B,iBAAiB,SAAS,KAAK;AAAA,MAC7B,OAAO,KAAK,KAAe;AAAA,IAC7B;AAAA,IACA,MAAM,OAAO,OAAO,SAAS,IAAI,OAAO,OAAO,MAAM,EAAE,SAAS,IAAI;AAAA,IAGpE,MAAM,UAAkC,CAAC;AAAA,IACzC,YAAY,KAAK,UAAU,OAAO,QAAQ,IAAI,OAAO,GAAG;AAAA,MACtD,IAAI,OAAO,UAAU,UAAU;AAAA,QAC7B,QAAQ,OAAO;AAAA,MACjB,EAAO,SAAI,MAAM,QAAQ,KAAK,GAAG;AAAA,QAC/B,QAAQ,OAAO,MAAM,KAAK,IAAI;AAAA,MAChC;AAAA,IACF;AAAA,IACA,SAAS,KAAK,EAAE,QAAQ,MAAM,SAAS,KAAK,CAAC;AAAA,IAG7C,MAAM,eAAe,UAAU,IAAI,IAAI,KAAK;AAAA,IAE5C,IAAI,aAAa,aAAa,UAAU;AAAA,IAExC,IAAI,aAAa,SAAS;AAAA,MACxB,YAAY,KAAK,UAAU,OAAO,QAAQ,aAAa,OAAO,GAAG;AAAA,QAC/D,IAAI,UAAU,KAAK,KAAK;AAAA,MAC1B;AAAA,IACF;AAAA,IAEA,IAAI,IAAI,aAAa,QAAQ,EAAE;AAAA,GAEnC;AAAA,EAGA,MAAM,aAAa,MAAM,IAAI,QAAgB,CAAC,SAAS,WAAW;AAAA,IAChE,OAAO,OAAO,QAAQ,GAAG,MAAM;AAAA,MAC7B,MAAM,UAAU,OAAO,QAAQ;AAAA,MAC/B,IAAI,WAAW,OAAO,YAAY,UAAU;AAAA,QAC1C,QAAQ,QAAQ,IAAI;AAAA,MACtB,EAAO;AAAA,QACL,OAAO,IAAI,MAAM,8BAA8B,CAAC;AAAA;AAAA,KAEnD;AAAA,IACD,OAAO,GAAG,SAAS,MAAM;AAAA,GAC1B;AAAA,EAED,OAAO;AAAA,IACL,KAAK,oBAAoB;AAAA,IACzB,MAAM;AAAA,SAEA,MAAK,GAAG;AAAA,MACZ,OAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AAAA,QACtC,OAAO,MAAM,CAAC,QAAQ;AAAA,UACpB,IAAI;AAAA,YAAK,OAAO,GAAG;AAAA,UACd;AAAA,oBAAQ;AAAA,SACd;AAAA,OACF;AAAA;AAAA,IAGH,WAAW,CAAC,MAAc,UAA8B;AAAA,MACtD,UAAU,IAAI,MAAM,QAAQ;AAAA;AAAA,IAG9B,kBAAkB,CAAC,UAA8B;AAAA,MAC/C,kBAAkB;AAAA;AAAA,IAGpB,WAAW,GAAG;AAAA,MACZ,OAAO,CAAC,GAAG,QAAQ;AAAA;AAAA,IAGrB,aAAa,GAAG;AAAA,MACd,SAAS,SAAS;AAAA;AAAA,IAGpB,cAAc,GAAG;AAAA,MACf,UAAU,MAAM;AAAA,MAChB,kBAAkB,EAAE,QAAQ,KAAK,MAAM,YAAY;AAAA;AAAA,EAEvD;AAAA;",
|
|
8
|
+
"debugId": "9B0013141DCE45BE64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TestContext } from "./index.ts";
|
|
2
|
+
export interface FetchTestContext extends TestContext {
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Create a test context with fetch APIs set up (Headers, Request, Response, FormData, fetch)
|
|
6
|
+
*/
|
|
7
|
+
export declare function createFetchTestContext(): Promise<FetchTestContext>;
|