@ricsam/isolate-test-utils 0.1.4 → 0.1.7
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/CHANGELOG.md +36 -0
- package/README.md +75 -0
- package/package.json +1 -1
- package/src/index.test.ts +37 -38
- package/src/native-input-test.ts +5 -4
- package/src/runtime-context.ts +63 -35
- package/tsconfig.json +0 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# @ricsam/isolate-test-utils
|
|
2
2
|
|
|
3
|
+
## 0.1.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- fix bugs
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- @ricsam/isolate-fetch@0.1.7
|
|
10
|
+
- @ricsam/isolate-core@0.1.7
|
|
11
|
+
- @ricsam/isolate-fs@0.1.7
|
|
12
|
+
- @ricsam/isolate-console@0.1.7
|
|
13
|
+
- @ricsam/isolate-runtime@0.1.8
|
|
14
|
+
|
|
15
|
+
## 0.1.6
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- new version
|
|
20
|
+
- Updated dependencies
|
|
21
|
+
- @ricsam/isolate-console@0.1.6
|
|
22
|
+
- @ricsam/isolate-core@0.1.6
|
|
23
|
+
- @ricsam/isolate-fetch@0.1.6
|
|
24
|
+
- @ricsam/isolate-fs@0.1.6
|
|
25
|
+
- @ricsam/isolate-runtime@0.1.7
|
|
26
|
+
|
|
27
|
+
## 0.1.5
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- new API
|
|
32
|
+
- Updated dependencies
|
|
33
|
+
- @ricsam/isolate-runtime@0.1.5
|
|
34
|
+
- @ricsam/isolate-console@0.1.5
|
|
35
|
+
- @ricsam/isolate-core@0.1.5
|
|
36
|
+
- @ricsam/isolate-fetch@0.1.5
|
|
37
|
+
- @ricsam/isolate-fs@0.1.5
|
|
38
|
+
|
|
3
39
|
## 0.1.4
|
|
4
40
|
|
|
5
41
|
### Patch Changes
|
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# @ricsam/isolate-test-utils
|
|
2
|
+
|
|
3
|
+
Testing utilities for isolated-vm V8 sandbox development.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm add @ricsam/isolate-test-utils
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { createRuntimeTestContext } from "@ricsam/isolate-test-utils";
|
|
15
|
+
|
|
16
|
+
// Create a test context with all APIs set up
|
|
17
|
+
const ctx = await createRuntimeTestContext({ fs: true });
|
|
18
|
+
|
|
19
|
+
// Set up mock response for fetch
|
|
20
|
+
ctx.setMockResponse({ status: 200, body: '{"data": "test"}' });
|
|
21
|
+
|
|
22
|
+
// Run code
|
|
23
|
+
await ctx.context.eval(`
|
|
24
|
+
(async () => {
|
|
25
|
+
console.log("Starting fetch...");
|
|
26
|
+
const response = await fetch("https://api.example.com/data");
|
|
27
|
+
const data = await response.json();
|
|
28
|
+
console.log("Got data:", data);
|
|
29
|
+
})()
|
|
30
|
+
`, { promise: true });
|
|
31
|
+
|
|
32
|
+
// Check captured logs
|
|
33
|
+
console.log(ctx.logs);
|
|
34
|
+
// [{ level: "log", args: ["Starting fetch..."] }, ...]
|
|
35
|
+
|
|
36
|
+
// Check captured fetch calls
|
|
37
|
+
console.log(ctx.fetchCalls);
|
|
38
|
+
// [{ url: "https://api.example.com/data", method: "GET", headers: [...] }]
|
|
39
|
+
|
|
40
|
+
// Cleanup
|
|
41
|
+
ctx.dispose();
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## RuntimeTestContext
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
interface RuntimeTestContext {
|
|
48
|
+
isolate: ivm.Isolate;
|
|
49
|
+
context: ivm.Context;
|
|
50
|
+
tick(ms?: number): Promise<void>;
|
|
51
|
+
dispose(): void;
|
|
52
|
+
logs: Array<{ level: string; args: unknown[] }>;
|
|
53
|
+
fetchCalls: Array<{ url: string; method: string; headers: [string, string][] }>;
|
|
54
|
+
setMockResponse(response: MockResponse): void;
|
|
55
|
+
mockFs: MockFileSystem;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface MockResponse {
|
|
59
|
+
status?: number;
|
|
60
|
+
body?: string;
|
|
61
|
+
headers?: Record<string, string>;
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Options
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
interface RuntimeTestContextOptions {
|
|
69
|
+
fs?: boolean; // Enable file system APIs with mock file system
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
MIT
|
package/package.json
CHANGED
package/src/index.test.ts
CHANGED
|
@@ -328,16 +328,17 @@ describe("createFsTestContext", () => {
|
|
|
328
328
|
describe("createRuntimeTestContext", () => {
|
|
329
329
|
let ctx: RuntimeTestContext | undefined;
|
|
330
330
|
|
|
331
|
-
afterEach(() => {
|
|
332
|
-
ctx?.dispose();
|
|
331
|
+
afterEach(async () => {
|
|
332
|
+
await ctx?.dispose();
|
|
333
333
|
ctx = undefined;
|
|
334
334
|
});
|
|
335
335
|
|
|
336
336
|
test("creates full runtime context", async () => {
|
|
337
337
|
ctx = await createRuntimeTestContext();
|
|
338
|
-
assert.ok(ctx.
|
|
339
|
-
assert.ok(ctx.
|
|
340
|
-
assert.ok(typeof ctx.
|
|
338
|
+
assert.ok(typeof ctx.eval === "function");
|
|
339
|
+
assert.ok(typeof ctx.clearTimers === "function");
|
|
340
|
+
assert.ok(typeof ctx.dispatchRequest === "function");
|
|
341
|
+
assert.ok(typeof ctx.dispose === "function");
|
|
341
342
|
assert.ok(Array.isArray(ctx.logs));
|
|
342
343
|
assert.ok(Array.isArray(ctx.fetchCalls));
|
|
343
344
|
});
|
|
@@ -345,14 +346,14 @@ describe("createRuntimeTestContext", () => {
|
|
|
345
346
|
test("captures console logs", async () => {
|
|
346
347
|
ctx = await createRuntimeTestContext();
|
|
347
348
|
|
|
348
|
-
ctx.
|
|
349
|
-
ctx.
|
|
349
|
+
await ctx.eval('console.log("test message")');
|
|
350
|
+
await ctx.eval('console.warn("warning message")');
|
|
350
351
|
|
|
351
352
|
assert.strictEqual(ctx.logs.length, 2);
|
|
352
|
-
assert.strictEqual(ctx.logs[0]
|
|
353
|
-
assert.deepStrictEqual(ctx.logs[0]
|
|
354
|
-
assert.strictEqual(ctx.logs[1]
|
|
355
|
-
assert.deepStrictEqual(ctx.logs[1]
|
|
353
|
+
assert.strictEqual(ctx.logs[0]!.level, "log");
|
|
354
|
+
assert.deepStrictEqual(ctx.logs[0]!.args, ["test message"]);
|
|
355
|
+
assert.strictEqual(ctx.logs[1]!.level, "warn");
|
|
356
|
+
assert.deepStrictEqual(ctx.logs[1]!.args, ["warning message"]);
|
|
356
357
|
});
|
|
357
358
|
|
|
358
359
|
test("captures and mocks fetch calls", async () => {
|
|
@@ -364,42 +365,40 @@ describe("createRuntimeTestContext", () => {
|
|
|
364
365
|
headers: { "Content-Type": "application/json" },
|
|
365
366
|
});
|
|
366
367
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
return JSON.stringify({ status: response.status, data: json });
|
|
373
|
-
})()
|
|
374
|
-
`,
|
|
375
|
-
{ promise: true }
|
|
376
|
-
);
|
|
368
|
+
await ctx.eval(`
|
|
369
|
+
const response = await fetch("https://api.example.com/data");
|
|
370
|
+
const json = await response.json();
|
|
371
|
+
setResult({ status: response.status, data: json });
|
|
372
|
+
`);
|
|
377
373
|
|
|
378
|
-
const
|
|
379
|
-
assert.strictEqual(
|
|
380
|
-
assert.deepStrictEqual(
|
|
374
|
+
const result = ctx.getResult<{ status: number; data: { data: string } }>();
|
|
375
|
+
assert.strictEqual(result?.status, 200);
|
|
376
|
+
assert.deepStrictEqual(result?.data, { data: "test" });
|
|
381
377
|
|
|
382
378
|
assert.strictEqual(ctx.fetchCalls.length, 1);
|
|
383
|
-
assert.strictEqual(ctx.fetchCalls[0]
|
|
384
|
-
assert.strictEqual(ctx.fetchCalls[0]
|
|
379
|
+
assert.strictEqual(ctx.fetchCalls[0]!.url, "https://api.example.com/data");
|
|
380
|
+
assert.strictEqual(ctx.fetchCalls[0]!.method, "GET");
|
|
385
381
|
});
|
|
386
382
|
|
|
387
|
-
test("
|
|
383
|
+
test("timers fire automatically with real time", async () => {
|
|
388
384
|
ctx = await createRuntimeTestContext();
|
|
389
385
|
|
|
390
|
-
ctx.
|
|
386
|
+
await ctx.eval(`
|
|
391
387
|
globalThis.timerFired = false;
|
|
392
|
-
setTimeout(() => { globalThis.timerFired = true; },
|
|
388
|
+
setTimeout(() => { globalThis.timerFired = true; }, 20);
|
|
393
389
|
`);
|
|
394
390
|
|
|
395
|
-
// Timer should not have fired yet
|
|
396
|
-
|
|
391
|
+
// Timer should not have fired yet - check via setResult
|
|
392
|
+
await ctx.eval('setResult(globalThis.timerFired)');
|
|
393
|
+
assert.strictEqual(ctx.getResult<boolean>(), false);
|
|
394
|
+
ctx.clearResult();
|
|
397
395
|
|
|
398
|
-
//
|
|
399
|
-
await
|
|
396
|
+
// Wait for real time to pass
|
|
397
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
400
398
|
|
|
401
399
|
// Timer should have fired
|
|
402
|
-
|
|
400
|
+
await ctx.eval('setResult(globalThis.timerFired)');
|
|
401
|
+
assert.strictEqual(ctx.getResult<boolean>(), true);
|
|
403
402
|
});
|
|
404
403
|
});
|
|
405
404
|
|
|
@@ -447,10 +446,10 @@ describe("startIntegrationServer", () => {
|
|
|
447
446
|
|
|
448
447
|
const requests = server.getRequests();
|
|
449
448
|
assert.strictEqual(requests.length, 1);
|
|
450
|
-
assert.strictEqual(requests[0]
|
|
451
|
-
assert.strictEqual(requests[0]
|
|
452
|
-
assert.strictEqual(requests[0]
|
|
453
|
-
assert.strictEqual(requests[0]
|
|
449
|
+
assert.strictEqual(requests[0]!.method, "POST");
|
|
450
|
+
assert.strictEqual(requests[0]!.path, "/api/endpoint");
|
|
451
|
+
assert.strictEqual(requests[0]!.headers["x-custom"], "value");
|
|
452
|
+
assert.strictEqual(requests[0]!.body, "request body");
|
|
454
453
|
});
|
|
455
454
|
|
|
456
455
|
test("clears requests", async () => {
|
package/src/native-input-test.ts
CHANGED
|
@@ -243,10 +243,11 @@ function marshalValue(
|
|
|
243
243
|
for (const [key, entryValue] of value.entries()) {
|
|
244
244
|
const keyJson = JSON.stringify(key);
|
|
245
245
|
|
|
246
|
-
if (entryValue
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
const
|
|
246
|
+
if (typeof entryValue !== "string") {
|
|
247
|
+
const file = entryValue as File;
|
|
248
|
+
const nameJson = JSON.stringify(file.name);
|
|
249
|
+
const typeJson = JSON.stringify(file.type);
|
|
250
|
+
const lastModifiedJson = JSON.stringify(file.lastModified);
|
|
250
251
|
context.evalSync(`
|
|
251
252
|
${path}.append(${keyJson}, new File([], ${nameJson}, { type: ${typeJson}, lastModified: ${lastModifiedJson} }));
|
|
252
253
|
`);
|
package/src/runtime-context.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type ivm from "isolated-vm";
|
|
2
1
|
import { MockFileSystem } from "./mock-fs.ts";
|
|
3
2
|
|
|
4
3
|
export interface MockResponse {
|
|
@@ -13,11 +12,14 @@ export interface RuntimeTestContextOptions {
|
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
export interface RuntimeTestContext {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
/** Execute code in the runtime (ES module mode, supports top-level await) */
|
|
16
|
+
eval(code: string): Promise<void>;
|
|
17
|
+
/** Clear all pending timers */
|
|
18
|
+
clearTimers(): void;
|
|
19
|
+
/** Dispatch an HTTP request to the serve() handler */
|
|
20
|
+
dispatchRequest(request: Request): Promise<Response>;
|
|
21
|
+
/** Dispose all resources */
|
|
22
|
+
dispose(): Promise<void>;
|
|
21
23
|
/** Captured console.log calls */
|
|
22
24
|
logs: Array<{ level: string; args: unknown[] }>;
|
|
23
25
|
/** Captured fetch calls */
|
|
@@ -26,6 +28,13 @@ export interface RuntimeTestContext {
|
|
|
26
28
|
setMockResponse(response: MockResponse): void;
|
|
27
29
|
/** Mock file system (only available if fs option is true) */
|
|
28
30
|
mockFs: MockFileSystem;
|
|
31
|
+
/**
|
|
32
|
+
* Get a result from the isolate. Call `await setResult(value)` in your eval code
|
|
33
|
+
* to pass a value back to the host.
|
|
34
|
+
*/
|
|
35
|
+
getResult<T = unknown>(): T | undefined;
|
|
36
|
+
/** Clear the stored result */
|
|
37
|
+
clearResult(): void;
|
|
29
38
|
}
|
|
30
39
|
|
|
31
40
|
/**
|
|
@@ -38,15 +47,17 @@ export interface RuntimeTestContext {
|
|
|
38
47
|
* // Set up mock response for fetch
|
|
39
48
|
* ctx.setMockResponse({ status: 200, body: '{"data": "test"}' });
|
|
40
49
|
*
|
|
41
|
-
* // Run code
|
|
42
|
-
* await ctx.
|
|
43
|
-
* (
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
+
* // Run code and pass result back via setResult
|
|
51
|
+
* await ctx.eval(`
|
|
52
|
+
* console.log("Starting fetch...");
|
|
53
|
+
* const response = await fetch("https://api.example.com/data");
|
|
54
|
+
* const data = await response.json();
|
|
55
|
+
* console.log("Got data:", data);
|
|
56
|
+
* setResult(data);
|
|
57
|
+
* `);
|
|
58
|
+
*
|
|
59
|
+
* // Get the result
|
|
60
|
+
* console.log(ctx.getResult()); // { data: "test" }
|
|
50
61
|
*
|
|
51
62
|
* // Check logs
|
|
52
63
|
* console.log(ctx.logs); // [{ level: "log", args: ["Starting fetch..."] }, ...]
|
|
@@ -54,7 +65,7 @@ export interface RuntimeTestContext {
|
|
|
54
65
|
* // Check fetch calls
|
|
55
66
|
* console.log(ctx.fetchCalls); // [{ url: "https://api.example.com/data", method: "GET", ... }]
|
|
56
67
|
*
|
|
57
|
-
* ctx.dispose();
|
|
68
|
+
* await ctx.dispose();
|
|
58
69
|
*/
|
|
59
70
|
export async function createRuntimeTestContext(
|
|
60
71
|
options?: RuntimeTestContextOptions
|
|
@@ -75,6 +86,7 @@ export async function createRuntimeTestContext(
|
|
|
75
86
|
}> = [];
|
|
76
87
|
|
|
77
88
|
let mockResponse: MockResponse = { status: 200, body: "" };
|
|
89
|
+
let storedResult: unknown = undefined;
|
|
78
90
|
|
|
79
91
|
// Create mock file system
|
|
80
92
|
const mockFs = new MockFileSystem();
|
|
@@ -82,33 +94,43 @@ export async function createRuntimeTestContext(
|
|
|
82
94
|
// Create runtime with configured handlers
|
|
83
95
|
const runtime = await createRuntime({
|
|
84
96
|
console: {
|
|
85
|
-
|
|
86
|
-
|
|
97
|
+
onEntry: (entry) => {
|
|
98
|
+
if (entry.type === "output") {
|
|
99
|
+
logs.push({ level: entry.level, args: entry.args });
|
|
100
|
+
} else if (entry.type === "assert") {
|
|
101
|
+
logs.push({ level: "error", args: ["Assertion failed:", ...entry.args] });
|
|
102
|
+
}
|
|
87
103
|
},
|
|
88
104
|
},
|
|
89
|
-
fetch: {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
});
|
|
105
|
+
fetch: async (request: Request) => {
|
|
106
|
+
// Capture fetch call
|
|
107
|
+
fetchCalls.push({
|
|
108
|
+
url: request.url,
|
|
109
|
+
method: request.method,
|
|
110
|
+
headers: [...request.headers.entries()],
|
|
111
|
+
});
|
|
97
112
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
},
|
|
113
|
+
// Return mock response
|
|
114
|
+
return new Response(mockResponse.body ?? "", {
|
|
115
|
+
status: mockResponse.status ?? 200,
|
|
116
|
+
headers: mockResponse.headers,
|
|
117
|
+
});
|
|
104
118
|
},
|
|
105
119
|
fs: opts.fs ? { getDirectory: async () => mockFs } : undefined,
|
|
120
|
+
customFunctions: {
|
|
121
|
+
setResult: {
|
|
122
|
+
fn: (value: unknown) => {
|
|
123
|
+
storedResult = value;
|
|
124
|
+
},
|
|
125
|
+
type: 'sync',
|
|
126
|
+
},
|
|
127
|
+
},
|
|
106
128
|
});
|
|
107
129
|
|
|
108
130
|
return {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
131
|
+
eval: runtime.eval.bind(runtime),
|
|
132
|
+
clearTimers: runtime.timers.clearAll.bind(runtime.timers),
|
|
133
|
+
dispatchRequest: runtime.fetch.dispatchRequest.bind(runtime.fetch),
|
|
112
134
|
dispose: runtime.dispose.bind(runtime),
|
|
113
135
|
logs,
|
|
114
136
|
fetchCalls,
|
|
@@ -116,5 +138,11 @@ export async function createRuntimeTestContext(
|
|
|
116
138
|
mockResponse = response;
|
|
117
139
|
},
|
|
118
140
|
mockFs,
|
|
141
|
+
getResult<T = unknown>(): T | undefined {
|
|
142
|
+
return storedResult as T | undefined;
|
|
143
|
+
},
|
|
144
|
+
clearResult() {
|
|
145
|
+
storedResult = undefined;
|
|
146
|
+
},
|
|
119
147
|
};
|
|
120
148
|
}
|