@ricsam/isolate-test-utils 0.1.9 → 0.1.11

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.
Files changed (46) hide show
  1. package/README.md +2 -2
  2. package/dist/cjs/fetch-context.cjs +66 -0
  3. package/dist/cjs/fetch-context.cjs.map +10 -0
  4. package/dist/cjs/fs-context.cjs +73 -0
  5. package/dist/cjs/fs-context.cjs.map +10 -0
  6. package/dist/cjs/index.cjs +121 -0
  7. package/dist/cjs/index.cjs.map +10 -0
  8. package/{src/mock-fs.ts → dist/cjs/mock-fs.cjs} +63 -97
  9. package/dist/cjs/mock-fs.cjs.map +10 -0
  10. package/dist/cjs/native-input-test.cjs +347 -0
  11. package/dist/cjs/native-input-test.cjs.map +10 -0
  12. package/dist/cjs/package.json +5 -0
  13. package/dist/cjs/runtime-context.cjs +97 -0
  14. package/dist/cjs/runtime-context.cjs.map +10 -0
  15. package/dist/cjs/server.cjs +109 -0
  16. package/dist/cjs/server.cjs.map +10 -0
  17. package/dist/mjs/fetch-context.mjs +23 -0
  18. package/dist/mjs/fetch-context.mjs.map +10 -0
  19. package/dist/mjs/fs-context.mjs +30 -0
  20. package/dist/mjs/fs-context.mjs.map +10 -0
  21. package/dist/mjs/index.mjs +78 -0
  22. package/dist/mjs/index.mjs.map +10 -0
  23. package/dist/mjs/mock-fs.mjs +181 -0
  24. package/dist/mjs/mock-fs.mjs.map +10 -0
  25. package/{src/native-input-test.ts → dist/mjs/native-input-test.mjs} +60 -169
  26. package/dist/mjs/native-input-test.mjs.map +10 -0
  27. package/dist/mjs/package.json +5 -0
  28. package/dist/mjs/runtime-context.mjs +67 -0
  29. package/dist/mjs/runtime-context.mjs.map +10 -0
  30. package/dist/mjs/server.mjs +79 -0
  31. package/dist/mjs/server.mjs.map +10 -0
  32. package/dist/types/fetch-context.d.ts +7 -0
  33. package/dist/types/fs-context.d.ts +30 -0
  34. package/dist/types/index.d.ts +88 -0
  35. package/dist/types/mock-fs.d.ts +59 -0
  36. package/dist/types/native-input-test.d.ts +29 -0
  37. package/dist/types/runtime-context.d.ts +73 -0
  38. package/dist/types/server.d.ts +53 -0
  39. package/package.json +45 -18
  40. package/CHANGELOG.md +0 -109
  41. package/src/fetch-context.ts +0 -33
  42. package/src/fs-context.ts +0 -65
  43. package/src/index.test.ts +0 -472
  44. package/src/index.ts +0 -177
  45. package/src/runtime-context.ts +0 -148
  46. package/src/server.ts +0 -150
package/src/index.test.ts DELETED
@@ -1,472 +0,0 @@
1
- import { test, describe, afterEach } from "node:test";
2
- import assert from "node:assert";
3
- import {
4
- createTestContext,
5
- createCoreTestContext,
6
- evalCode,
7
- evalCodeAsync,
8
- evalCodeJson,
9
- evalCodeJsonAsync,
10
- injectGlobals,
11
- MockFileSystem,
12
- createFsTestContext,
13
- createRuntimeTestContext,
14
- startIntegrationServer,
15
- type TestContext,
16
- type FsTestContext,
17
- type RuntimeTestContext,
18
- type IntegrationServer,
19
- } from "./index.ts";
20
-
21
- // ============================================================================
22
- // createTestContext tests
23
- // ============================================================================
24
-
25
- describe("createTestContext", () => {
26
- let ctx: TestContext | undefined;
27
-
28
- afterEach(() => {
29
- ctx?.dispose();
30
- ctx = undefined;
31
- });
32
-
33
- test("creates a basic context", async () => {
34
- ctx = await createTestContext();
35
- assert.ok(ctx.isolate);
36
- assert.ok(ctx.context);
37
- assert.ok(typeof ctx.dispose === "function");
38
- });
39
-
40
- test("can evaluate code in context", async () => {
41
- ctx = await createTestContext();
42
- const result = ctx.context.evalSync("1 + 1");
43
- assert.strictEqual(result, 2);
44
- });
45
- });
46
-
47
- describe("createCoreTestContext", () => {
48
- let ctx: TestContext | undefined;
49
-
50
- afterEach(() => {
51
- ctx?.dispose();
52
- ctx = undefined;
53
- });
54
-
55
- test("creates a context with core APIs", async () => {
56
- ctx = await createCoreTestContext();
57
- assert.ok(ctx.isolate);
58
- assert.ok(ctx.context);
59
- });
60
-
61
- test("has Blob available", async () => {
62
- ctx = await createCoreTestContext();
63
- const result = ctx.context.evalSync("typeof Blob");
64
- assert.strictEqual(result, "function");
65
- });
66
-
67
- test("has URL available", async () => {
68
- ctx = await createCoreTestContext();
69
- const result = ctx.context.evalSync(
70
- "new URL('https://example.com').hostname"
71
- );
72
- assert.strictEqual(result, "example.com");
73
- });
74
- });
75
-
76
- // ============================================================================
77
- // evalCode tests
78
- // ============================================================================
79
-
80
- describe("evalCode", () => {
81
- let ctx: TestContext | undefined;
82
-
83
- afterEach(() => {
84
- ctx?.dispose();
85
- ctx = undefined;
86
- });
87
-
88
- test("evaluates code synchronously", async () => {
89
- ctx = await createTestContext();
90
- const result = evalCode<number>(ctx.context, "2 + 3");
91
- assert.strictEqual(result, 5);
92
- });
93
-
94
- test("returns string", async () => {
95
- ctx = await createTestContext();
96
- const result = evalCode<string>(ctx.context, '"hello"');
97
- assert.strictEqual(result, "hello");
98
- });
99
- });
100
-
101
- describe("evalCodeAsync", () => {
102
- let ctx: TestContext | undefined;
103
-
104
- afterEach(() => {
105
- ctx?.dispose();
106
- ctx = undefined;
107
- });
108
-
109
- test("evaluates async code", async () => {
110
- ctx = await createTestContext();
111
- const result = await evalCodeAsync<number>(
112
- ctx.context,
113
- "Promise.resolve(42)"
114
- );
115
- assert.strictEqual(result, 42);
116
- });
117
-
118
- test("handles async IIFE", async () => {
119
- ctx = await createTestContext();
120
- const result = await evalCodeAsync<string>(
121
- ctx.context,
122
- `(async () => { return "async result"; })()`
123
- );
124
- assert.strictEqual(result, "async result");
125
- });
126
- });
127
-
128
- describe("evalCodeJson", () => {
129
- let ctx: TestContext | undefined;
130
-
131
- afterEach(() => {
132
- ctx?.dispose();
133
- ctx = undefined;
134
- });
135
-
136
- test("parses JSON result", async () => {
137
- ctx = await createTestContext();
138
- const result = evalCodeJson<{ name: string }>(
139
- ctx.context,
140
- 'JSON.stringify({ name: "test" })'
141
- );
142
- assert.deepStrictEqual(result, { name: "test" });
143
- });
144
- });
145
-
146
- describe("evalCodeJsonAsync", () => {
147
- let ctx: TestContext | undefined;
148
-
149
- afterEach(() => {
150
- ctx?.dispose();
151
- ctx = undefined;
152
- });
153
-
154
- test("parses async JSON result", async () => {
155
- ctx = await createTestContext();
156
- const result = await evalCodeJsonAsync<{ value: number }>(
157
- ctx.context,
158
- `(async () => JSON.stringify({ value: 123 }))()`
159
- );
160
- assert.deepStrictEqual(result, { value: 123 });
161
- });
162
- });
163
-
164
- describe("injectGlobals", () => {
165
- let ctx: TestContext | undefined;
166
-
167
- afterEach(() => {
168
- ctx?.dispose();
169
- ctx = undefined;
170
- });
171
-
172
- test("injects primitive values", async () => {
173
- ctx = await createTestContext();
174
- await injectGlobals(ctx.context, {
175
- testString: "hello",
176
- testNumber: 42,
177
- testBool: true,
178
- });
179
- assert.strictEqual(evalCode<string>(ctx.context, "testString"), "hello");
180
- assert.strictEqual(evalCode<number>(ctx.context, "testNumber"), 42);
181
- assert.strictEqual(evalCode<boolean>(ctx.context, "testBool"), true);
182
- });
183
-
184
- test("injects objects", async () => {
185
- ctx = await createTestContext();
186
- await injectGlobals(ctx.context, {
187
- testConfig: { debug: true, level: 3 },
188
- });
189
- const result = evalCodeJson<{ debug: boolean; level: number }>(
190
- ctx.context,
191
- "JSON.stringify(testConfig)"
192
- );
193
- assert.deepStrictEqual(result, { debug: true, level: 3 });
194
- });
195
- });
196
-
197
- // ============================================================================
198
- // MockFileSystem tests
199
- // ============================================================================
200
-
201
- describe("MockFileSystem", () => {
202
- test("creates empty file system with root directory", () => {
203
- const fs = new MockFileSystem();
204
- assert.strictEqual(fs.directories.has("/"), true);
205
- assert.strictEqual(fs.files.size, 0);
206
- });
207
-
208
- test("setFile creates file with content", async () => {
209
- const fs = new MockFileSystem();
210
- fs.setFile("/test.txt", "Hello, World!");
211
-
212
- const file = await fs.readFile("/test.txt");
213
- assert.strictEqual(new TextDecoder().decode(file.data), "Hello, World!");
214
- });
215
-
216
- test("getFile retrieves file content", () => {
217
- const fs = new MockFileSystem();
218
- fs.setFile("/test.txt", "content");
219
-
220
- const data = fs.getFile("/test.txt");
221
- assert.ok(data);
222
- assert.strictEqual(new TextDecoder().decode(data), "content");
223
- });
224
-
225
- test("getFileAsString retrieves file as string", () => {
226
- const fs = new MockFileSystem();
227
- fs.setFile("/test.txt", "string content");
228
-
229
- const content = fs.getFileAsString("/test.txt");
230
- assert.strictEqual(content, "string content");
231
- });
232
-
233
- test("createDirectory creates nested directories", () => {
234
- const fs = new MockFileSystem();
235
- fs.createDirectory("/a/b/c");
236
-
237
- assert.strictEqual(fs.directories.has("/a"), true);
238
- assert.strictEqual(fs.directories.has("/a/b"), true);
239
- assert.strictEqual(fs.directories.has("/a/b/c"), true);
240
- });
241
-
242
- test("reset clears all files and directories", async () => {
243
- const fs = new MockFileSystem();
244
- fs.setFile("/test.txt", "content");
245
- fs.createDirectory("/dir");
246
-
247
- fs.reset();
248
-
249
- assert.strictEqual(fs.files.size, 0);
250
- assert.strictEqual(fs.directories.size, 1); // Only root
251
- assert.strictEqual(fs.directories.has("/"), true);
252
- });
253
-
254
- test("getFileHandle throws NotFoundError for missing file", async () => {
255
- const fs = new MockFileSystem();
256
- await assert.rejects(
257
- fs.getFileHandle("/missing.txt"),
258
- /NotFoundError/
259
- );
260
- });
261
-
262
- test("getFileHandle creates file with create option", async () => {
263
- const fs = new MockFileSystem();
264
- await fs.getFileHandle("/new.txt", { create: true });
265
- assert.strictEqual(fs.files.has("/new.txt"), true);
266
- });
267
-
268
- test("readDirectory lists files and directories", async () => {
269
- const fs = new MockFileSystem();
270
- fs.setFile("/file1.txt", "content");
271
- fs.setFile("/file2.txt", "content");
272
- fs.createDirectory("/subdir");
273
-
274
- const entries = await fs.readDirectory("/");
275
- assert.strictEqual(entries.length, 3);
276
-
277
- const names = entries.map((e) => e.name).sort();
278
- assert.deepStrictEqual(names, ["file1.txt", "file2.txt", "subdir"]);
279
- });
280
- });
281
-
282
- // ============================================================================
283
- // createFsTestContext tests
284
- // ============================================================================
285
-
286
- describe("createFsTestContext", () => {
287
- let ctx: FsTestContext | undefined;
288
-
289
- afterEach(() => {
290
- ctx?.dispose();
291
- ctx = undefined;
292
- });
293
-
294
- test("creates context with file system APIs", async () => {
295
- ctx = await createFsTestContext();
296
- assert.ok(ctx.isolate);
297
- assert.ok(ctx.context);
298
- assert.ok(ctx.mockFs);
299
- });
300
-
301
- test("mockFs is connected to context", async () => {
302
- ctx = await createFsTestContext();
303
-
304
- // Create a file in mockFs
305
- ctx.mockFs.setFile("/test.txt", "Hello from test!");
306
-
307
- // Read it from the isolate
308
- const result = await ctx.context.eval(
309
- `
310
- (async () => {
311
- const root = await getDirectory('/');
312
- const fileHandle = await root.getFileHandle("test.txt");
313
- const file = await fileHandle.getFile();
314
- return await file.text();
315
- })()
316
- `,
317
- { promise: true }
318
- );
319
-
320
- assert.strictEqual(result, "Hello from test!");
321
- });
322
- });
323
-
324
- // ============================================================================
325
- // createRuntimeTestContext tests
326
- // ============================================================================
327
-
328
- describe("createRuntimeTestContext", () => {
329
- let ctx: RuntimeTestContext | undefined;
330
-
331
- afterEach(async () => {
332
- await ctx?.dispose();
333
- ctx = undefined;
334
- });
335
-
336
- test("creates full runtime context", async () => {
337
- ctx = await createRuntimeTestContext();
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");
342
- assert.ok(Array.isArray(ctx.logs));
343
- assert.ok(Array.isArray(ctx.fetchCalls));
344
- });
345
-
346
- test("captures console logs", async () => {
347
- ctx = await createRuntimeTestContext();
348
-
349
- await ctx.eval('console.log("test message")');
350
- await ctx.eval('console.warn("warning message")');
351
-
352
- assert.strictEqual(ctx.logs.length, 2);
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"]);
357
- });
358
-
359
- test("captures and mocks fetch calls", async () => {
360
- ctx = await createRuntimeTestContext();
361
-
362
- ctx.setMockResponse({
363
- status: 200,
364
- body: '{"data": "test"}',
365
- headers: { "Content-Type": "application/json" },
366
- });
367
-
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
- `);
373
-
374
- const result = ctx.getResult<{ status: number; data: { data: string } }>();
375
- assert.strictEqual(result?.status, 200);
376
- assert.deepStrictEqual(result?.data, { data: "test" });
377
-
378
- assert.strictEqual(ctx.fetchCalls.length, 1);
379
- assert.strictEqual(ctx.fetchCalls[0]!.url, "https://api.example.com/data");
380
- assert.strictEqual(ctx.fetchCalls[0]!.method, "GET");
381
- });
382
-
383
- test("timers fire automatically with real time", async () => {
384
- ctx = await createRuntimeTestContext();
385
-
386
- await ctx.eval(`
387
- globalThis.timerFired = false;
388
- setTimeout(() => { globalThis.timerFired = true; }, 20);
389
- `);
390
-
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();
395
-
396
- // Wait for real time to pass
397
- await new Promise((r) => setTimeout(r, 50));
398
-
399
- // Timer should have fired
400
- await ctx.eval('setResult(globalThis.timerFired)');
401
- assert.strictEqual(ctx.getResult<boolean>(), true);
402
- });
403
- });
404
-
405
- // ============================================================================
406
- // startIntegrationServer tests
407
- // ============================================================================
408
-
409
- describe("startIntegrationServer", () => {
410
- let server: IntegrationServer | undefined;
411
-
412
- afterEach(async () => {
413
- await server?.close();
414
- server = undefined;
415
- });
416
-
417
- test("starts server on available port", async () => {
418
- server = await startIntegrationServer();
419
- assert.ok(server.port > 0);
420
- assert.ok(server.url.startsWith("http://localhost:"));
421
- });
422
-
423
- test("responds with configured response", async () => {
424
- server = await startIntegrationServer();
425
- server.setResponse("/api/test", {
426
- status: 200,
427
- body: '{"message": "hello"}',
428
- headers: { "Content-Type": "application/json" },
429
- });
430
-
431
- const response = await fetch(`${server.url}/api/test`);
432
- assert.strictEqual(response.status, 200);
433
- const data = await response.json();
434
- assert.deepStrictEqual(data, { message: "hello" });
435
- });
436
-
437
- test("records requests", async () => {
438
- server = await startIntegrationServer();
439
- server.setDefaultResponse({ status: 200, body: "OK" });
440
-
441
- await fetch(`${server.url}/api/endpoint`, {
442
- method: "POST",
443
- headers: { "X-Custom": "value" },
444
- body: "request body",
445
- });
446
-
447
- const requests = server.getRequests();
448
- assert.strictEqual(requests.length, 1);
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");
453
- });
454
-
455
- test("clears requests", async () => {
456
- server = await startIntegrationServer();
457
- server.setDefaultResponse({ status: 200 });
458
-
459
- await fetch(`${server.url}/test`);
460
- assert.strictEqual(server.getRequests().length, 1);
461
-
462
- server.clearRequests();
463
- assert.strictEqual(server.getRequests().length, 0);
464
- });
465
-
466
- test("returns 404 for unmatched paths", async () => {
467
- server = await startIntegrationServer();
468
-
469
- const response = await fetch(`${server.url}/unknown`);
470
- assert.strictEqual(response.status, 404);
471
- });
472
- });
package/src/index.ts DELETED
@@ -1,177 +0,0 @@
1
- import type ivm from "isolated-vm";
2
-
3
- // ============================================================================
4
- // Types
5
- // ============================================================================
6
-
7
- export interface TestContext {
8
- isolate: ivm.Isolate;
9
- context: ivm.Context;
10
- dispose(): void;
11
- }
12
-
13
- export interface TestResult<T> {
14
- result: T;
15
- logs: Array<{ level: string; args: unknown[] }>;
16
- }
17
-
18
- // ============================================================================
19
- // Context Creation
20
- // ============================================================================
21
-
22
- /**
23
- * Create a basic test context for isolated-vm tests.
24
- * This creates a bare context without any APIs set up.
25
- */
26
- export async function createTestContext(): Promise<TestContext> {
27
- const ivm = await import("isolated-vm");
28
- const isolate = new ivm.default.Isolate();
29
- const context = await isolate.createContext();
30
-
31
- return {
32
- isolate,
33
- context,
34
- dispose() {
35
- context.release();
36
- isolate.dispose();
37
- },
38
- };
39
- }
40
-
41
- /**
42
- * Create a test context with core APIs set up (Blob, File, URL, streams, etc.)
43
- */
44
- export async function createCoreTestContext(): Promise<TestContext> {
45
- const ivm = await import("isolated-vm");
46
- const { setupCore } = await import("@ricsam/isolate-core");
47
-
48
- const isolate = new ivm.default.Isolate();
49
- const context = await isolate.createContext();
50
- const coreHandle = await setupCore(context);
51
-
52
- return {
53
- isolate,
54
- context,
55
- dispose() {
56
- coreHandle.dispose();
57
- context.release();
58
- isolate.dispose();
59
- },
60
- };
61
- }
62
-
63
- // ============================================================================
64
- // Code Evaluation Helpers
65
- // ============================================================================
66
-
67
- /**
68
- * Synchronously evaluate code and return typed result.
69
- * Use this for simple expressions that don't involve promises.
70
- *
71
- * @example
72
- * const result = evalCode<number>(ctx.context, "1 + 1");
73
- * // result === 2
74
- */
75
- export function evalCode<T = unknown>(context: ivm.Context, code: string): T {
76
- return context.evalSync(code) as T;
77
- }
78
-
79
- /**
80
- * Asynchronously evaluate code that may return promises.
81
- * Automatically wraps code to handle promise resolution.
82
- *
83
- * @example
84
- * const result = await evalCodeAsync<string>(ctx.context, `
85
- * (async () => {
86
- * return "hello";
87
- * })()
88
- * `);
89
- */
90
- export async function evalCodeAsync<T = unknown>(
91
- context: ivm.Context,
92
- code: string
93
- ): Promise<T> {
94
- return (await context.eval(code, { promise: true })) as T;
95
- }
96
-
97
- /**
98
- * Evaluate code and return the result as JSON (for complex objects).
99
- * Useful when you need to extract structured data from the isolate.
100
- *
101
- * @example
102
- * const data = evalCodeJson<{ name: string }>(ctx.context, `
103
- * JSON.stringify({ name: "test" })
104
- * `);
105
- */
106
- export function evalCodeJson<T = unknown>(context: ivm.Context, code: string): T {
107
- const jsonString = context.evalSync(code) as string;
108
- return JSON.parse(jsonString) as T;
109
- }
110
-
111
- /**
112
- * Evaluate async code and return the result as JSON (for complex objects).
113
- *
114
- * @example
115
- * const data = await evalCodeJsonAsync<{ status: number }>(ctx.context, `
116
- * (async () => {
117
- * const response = await fetch("...");
118
- * return JSON.stringify({ status: response.status });
119
- * })()
120
- * `);
121
- */
122
- export async function evalCodeJsonAsync<T = unknown>(
123
- context: ivm.Context,
124
- code: string
125
- ): Promise<T> {
126
- const jsonString = (await context.eval(code, { promise: true })) as string;
127
- return JSON.parse(jsonString) as T;
128
- }
129
-
130
- /**
131
- * Inject values into the isolate's global scope before running code.
132
- *
133
- * @example
134
- * await injectGlobals(ctx.context, {
135
- * testInput: "hello",
136
- * testConfig: { debug: true }
137
- * });
138
- * const result = evalCode<string>(ctx.context, "testInput");
139
- */
140
- export async function injectGlobals(
141
- context: ivm.Context,
142
- values: Record<string, unknown>
143
- ): Promise<void> {
144
- const global = context.global;
145
-
146
- for (const [key, value] of Object.entries(values)) {
147
- if (typeof value === "function") {
148
- const ivm = await import("isolated-vm");
149
- global.setSync(key, new ivm.default.Callback(value as (...args: unknown[]) => unknown));
150
- } else if (typeof value === "object" && value !== null) {
151
- // For objects, serialize as JSON and inject
152
- context.evalSync(`globalThis.${key} = ${JSON.stringify(value)}`);
153
- } else {
154
- // For primitives, set directly
155
- global.setSync(key, value);
156
- }
157
- }
158
- }
159
-
160
- // ============================================================================
161
- // Exports from other modules
162
- // ============================================================================
163
-
164
- export { MockFileSystem } from "./mock-fs.ts";
165
- export { createFsTestContext } from "./fs-context.ts";
166
- export type { FsTestContext } from "./fs-context.ts";
167
- export { createRuntimeTestContext } from "./runtime-context.ts";
168
- export type { RuntimeTestContext } from "./runtime-context.ts";
169
- export { startIntegrationServer } from "./server.ts";
170
- export type { IntegrationServer } from "./server.ts";
171
- export { runTestCode } from "./native-input-test.ts";
172
- export type { TestRunner, TestRuntime } from "./native-input-test.ts";
173
- export { createFetchTestContext } from "./fetch-context.ts";
174
- export type { FetchTestContext } from "./fetch-context.ts";
175
-
176
- // Re-export useful types
177
- export type { FileSystemHandler } from "@ricsam/isolate-fs";