@gjsify/vm 0.1.0

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.
@@ -0,0 +1,882 @@
1
+ // Ported from refs/node-test/parallel/test-vm-*.js
2
+ // Original: MIT license, Node.js contributors
3
+
4
+ import { describe, it, expect } from '@gjsify/unit';
5
+ import vm, {
6
+ runInThisContext,
7
+ runInNewContext,
8
+ runInContext,
9
+ createContext,
10
+ isContext,
11
+ compileFunction,
12
+ Script,
13
+ } from 'node:vm';
14
+
15
+ export default async () => {
16
+ await describe('vm', async () => {
17
+
18
+ // ==================== exports ====================
19
+ await describe('exports', async () => {
20
+ await it('should export runInThisContext', async () => {
21
+ expect(typeof runInThisContext).toBe('function');
22
+ });
23
+
24
+ await it('should export runInNewContext', async () => {
25
+ expect(typeof runInNewContext).toBe('function');
26
+ });
27
+
28
+ await it('should export runInContext', async () => {
29
+ expect(typeof runInContext).toBe('function');
30
+ });
31
+
32
+ await it('should export createContext', async () => {
33
+ expect(typeof createContext).toBe('function');
34
+ });
35
+
36
+ await it('should export isContext', async () => {
37
+ expect(typeof isContext).toBe('function');
38
+ });
39
+
40
+ await it('should export compileFunction', async () => {
41
+ expect(typeof compileFunction).toBe('function');
42
+ });
43
+
44
+ await it('should export Script class', async () => {
45
+ expect(typeof Script).toBe('function');
46
+ });
47
+
48
+ await it('should have all exports on default', async () => {
49
+ expect(typeof vm.runInThisContext).toBe('function');
50
+ expect(typeof vm.runInNewContext).toBe('function');
51
+ expect(typeof vm.runInContext).toBe('function');
52
+ expect(typeof vm.createContext).toBe('function');
53
+ expect(typeof vm.isContext).toBe('function');
54
+ expect(typeof vm.compileFunction).toBe('function');
55
+ expect(typeof vm.Script).toBe('function');
56
+ });
57
+ });
58
+
59
+ // ==================== runInThisContext ====================
60
+ await describe('runInThisContext', async () => {
61
+ await it('should evaluate arithmetic', async () => {
62
+ expect(runInThisContext('1 + 1')).toBe(2);
63
+ });
64
+
65
+ await it('should evaluate string expressions', async () => {
66
+ expect(runInThisContext('"hello" + " " + "world"')).toBe('hello world');
67
+ });
68
+
69
+ await it('should evaluate complex expressions', async () => {
70
+ expect(runInThisContext('[1,2,3].map(x => x * 2).join(",")')).toBe('2,4,6');
71
+ });
72
+
73
+ await it('should return undefined for statements', async () => {
74
+ const result = runInThisContext('var __vmtest_x = 42');
75
+ expect(result).toBeUndefined();
76
+ });
77
+
78
+ await it('should throw SyntaxError for invalid code', async () => {
79
+ expect(() => runInThisContext('function(')).toThrow();
80
+ });
81
+
82
+ await it('should throw ReferenceError for undefined variables', async () => {
83
+ expect(() => runInThisContext('__nonexistent_var_abc123')).toThrow();
84
+ });
85
+
86
+ await it('should evaluate object literals', async () => {
87
+ const result = runInThisContext('({a: 1, b: 2})');
88
+ expect(result.a).toBe(1);
89
+ expect(result.b).toBe(2);
90
+ });
91
+
92
+ await it('should evaluate closures and IIFEs', async () => {
93
+ const result = runInThisContext('(function() { var x = 10; return x * 2; })()');
94
+ expect(result).toBe(20);
95
+ });
96
+
97
+ await it('should evaluate arrow function IIFEs', async () => {
98
+ const result = runInThisContext('(() => { const arr = [1,2,3]; return arr.reduce((a,b) => a+b, 0); })()');
99
+ expect(result).toBe(6);
100
+ });
101
+
102
+ await it('should evaluate template literals', async () => {
103
+ const result = runInThisContext('`hello ${"world"}`');
104
+ expect(result).toBe('hello world');
105
+ });
106
+
107
+ await it('should evaluate ternary expressions', async () => {
108
+ expect(runInThisContext('true ? "yes" : "no"')).toBe('yes');
109
+ expect(runInThisContext('false ? "yes" : "no"')).toBe('no');
110
+ });
111
+
112
+ await it('should return null', async () => {
113
+ expect(runInThisContext('null')).toBeNull();
114
+ });
115
+
116
+ await it('should return boolean values', async () => {
117
+ expect(runInThisContext('true')).toBe(true);
118
+ expect(runInThisContext('false')).toBe(false);
119
+ });
120
+
121
+ await it('should evaluate typeof expression', async () => {
122
+ expect(runInThisContext('typeof undefined')).toBe('undefined');
123
+ expect(runInThisContext('typeof 42')).toBe('number');
124
+ expect(runInThisContext('typeof "str"')).toBe('string');
125
+ });
126
+
127
+ await it('should evaluate array creation and methods', async () => {
128
+ const result = runInThisContext('Array.from({length: 3}, (_, i) => i)');
129
+ expect(result[0]).toBe(0);
130
+ expect(result[1]).toBe(1);
131
+ expect(result[2]).toBe(2);
132
+ });
133
+
134
+ await it('should evaluate destructuring expressions', async () => {
135
+ const result = runInThisContext('(() => { const [a, ...rest] = [1,2,3]; return rest; })()');
136
+ expect(result[0]).toBe(2);
137
+ expect(result[1]).toBe(3);
138
+ });
139
+
140
+ await it('should handle string with special characters', async () => {
141
+ expect(runInThisContext('"line1\\nline2"')).toBe('line1\nline2');
142
+ });
143
+
144
+ await it('should evaluate regex creation', async () => {
145
+ const result = runInThisContext('/^hello/i.test("Hello World")');
146
+ expect(result).toBe(true);
147
+ });
148
+
149
+ await it('should evaluate Math operations', async () => {
150
+ expect(runInThisContext('Math.max(1, 5, 3)')).toBe(5);
151
+ expect(runInThisContext('Math.min(1, 5, 3)')).toBe(1);
152
+ });
153
+
154
+ await it('should evaluate JSON operations', async () => {
155
+ const result = runInThisContext('JSON.parse(\'{"a":1}\')');
156
+ expect(result.a).toBe(1);
157
+ });
158
+
159
+ await it('should handle empty string code', async () => {
160
+ const result = runInThisContext('');
161
+ expect(result).toBeUndefined();
162
+ });
163
+
164
+ await it('should handle code that is only comments', async () => {
165
+ const result = runInThisContext('// just a comment');
166
+ expect(result).toBeUndefined();
167
+ });
168
+
169
+ await it('should propagate TypeError', async () => {
170
+ expect(() => runInThisContext('null.property')).toThrow();
171
+ });
172
+
173
+ await it('should propagate custom Error', async () => {
174
+ let caught = false;
175
+ try {
176
+ runInThisContext('throw new Error("custom message")');
177
+ } catch (e: unknown) {
178
+ caught = true;
179
+ expect((e as Error).message).toBe('custom message');
180
+ }
181
+ expect(caught).toBe(true);
182
+ });
183
+
184
+ await it('should evaluate string passed expression', async () => {
185
+ expect(runInThisContext('"passed"')).toBe('passed');
186
+ });
187
+
188
+ await it('should evaluate Object.keys', async () => {
189
+ const result = runInThisContext('Object.keys({x:1,y:2,z:3})');
190
+ expect(result.length).toBe(3);
191
+ expect(result[0]).toBe('x');
192
+ });
193
+ });
194
+
195
+ // ==================== runInNewContext ====================
196
+ await describe('runInNewContext', async () => {
197
+ await it('should access sandbox variables', async () => {
198
+ const result = runInNewContext('a + b', { a: 10, b: 20 });
199
+ expect(result).toBe(30);
200
+ });
201
+
202
+ await it('should work with string values', async () => {
203
+ const result = runInNewContext('greeting + " " + name', {
204
+ greeting: 'Hello',
205
+ name: 'World',
206
+ });
207
+ expect(result).toBe('Hello World');
208
+ });
209
+
210
+ await it('should work with empty context', async () => {
211
+ const result = runInNewContext('typeof undefined');
212
+ expect(result).toBe('undefined');
213
+ });
214
+
215
+ await it('should work with no context argument', async () => {
216
+ const result = runInNewContext('1 + 2');
217
+ expect(result).toBe(3);
218
+ });
219
+
220
+ await it('should access array methods via sandbox', async () => {
221
+ const result = runInNewContext('items.join("-")', { items: ['a', 'b', 'c'] });
222
+ expect(result).toBe('a-b-c');
223
+ });
224
+
225
+ await it('should evaluate expressions using sandbox values', async () => {
226
+ const result = runInNewContext('a * b + c', { a: 2, b: 3, c: 4 });
227
+ expect(result).toBe(10);
228
+ });
229
+
230
+ await it('should access nested objects in sandbox', async () => {
231
+ const result = runInNewContext('obj.nested.value', { obj: { nested: { value: 99 } } });
232
+ expect(result).toBe(99);
233
+ });
234
+
235
+ await it('should throw for invalid code in sandbox', async () => {
236
+ expect(() => runInNewContext('function(')).toThrow();
237
+ });
238
+
239
+ await it('should evaluate string concatenation with many params', async () => {
240
+ const result = runInNewContext('p + q + r + s + t', {
241
+ p: 'ab', q: 'cd', r: 'ef', s: 'gh', t: 'ij',
242
+ });
243
+ expect(result).toBe('abcdefghij');
244
+ });
245
+
246
+ await it('should handle boolean sandbox values', async () => {
247
+ expect(runInNewContext('flag ? "yes" : "no"', { flag: true })).toBe('yes');
248
+ expect(runInNewContext('flag ? "yes" : "no"', { flag: false })).toBe('no');
249
+ });
250
+
251
+ await it('should handle null sandbox values', async () => {
252
+ const result = runInNewContext('val === null', { val: null });
253
+ expect(result).toBe(true);
254
+ });
255
+
256
+ await it('should handle function sandbox values', async () => {
257
+ const result = runInNewContext('fn(5)', { fn: (x: number) => x * 2 });
258
+ expect(result).toBe(10);
259
+ });
260
+
261
+ await it('should allow calling sandbox methods', async () => {
262
+ const result = runInNewContext('arr.filter(x => x > 2)', { arr: [1, 2, 3, 4, 5] });
263
+ expect(result.length).toBe(3);
264
+ expect(result[0]).toBe(3);
265
+ });
266
+
267
+ await it('should handle sandbox with Symbol-keyed properties', async () => {
268
+ // Symbol keys are not enumerable by Object.keys, so they won't be injected
269
+ // The code should still work without them
270
+ const sym = Symbol('test');
271
+ const ctx: Record<string | symbol, unknown> = { val: 42 };
272
+ ctx[sym] = 'hidden';
273
+ const result = runInNewContext('val', ctx as Record<string, unknown>);
274
+ expect(result).toBe(42);
275
+ });
276
+
277
+ await it('should handle empty sandbox', async () => {
278
+ const result = runInNewContext('typeof Object', {});
279
+ expect(result).toBe('function');
280
+ });
281
+
282
+ await it('should evaluate JSON stringify in sandbox', async () => {
283
+ const result = runInNewContext('JSON.stringify(data)', { data: { x: 1 } });
284
+ expect(result).toBe('{"x":1}');
285
+ });
286
+
287
+ await it('should throw ReferenceError for missing sandbox variable', async () => {
288
+ expect(() => runInNewContext('missing_var', {})).toThrow();
289
+ });
290
+
291
+ await it('should handle array sandbox value', async () => {
292
+ const result = runInNewContext('arr.length', { arr: [1, 2, 3] });
293
+ expect(result).toBe(3);
294
+ });
295
+
296
+ await it('should propagate errors from sandbox code', async () => {
297
+ let caught = false;
298
+ try {
299
+ runInNewContext('throw new Error("sandbox error")', {});
300
+ } catch (e: unknown) {
301
+ caught = true;
302
+ expect((e as Error).message).toBe('sandbox error');
303
+ }
304
+ expect(caught).toBe(true);
305
+ });
306
+
307
+ await it('should handle regex in sandbox', async () => {
308
+ const result = runInNewContext('pattern.test(str)', {
309
+ pattern: /^hello/i,
310
+ str: 'Hello World',
311
+ });
312
+ expect(result).toBe(true);
313
+ });
314
+
315
+ await it('should return undefined for statements in sandbox', async () => {
316
+ const result = runInNewContext('var x = 42', {});
317
+ expect(result).toBeUndefined();
318
+ });
319
+
320
+ await it('should isolate between calls', async () => {
321
+ runInNewContext('var __isolated_x = 1', {});
322
+ // The variable should not leak to the next call
323
+ expect(() => runInNewContext('__isolated_x', {})).toThrow();
324
+ });
325
+ });
326
+
327
+ // ==================== runInContext ====================
328
+ await describe('runInContext', async () => {
329
+ await it('should run in a created context', async () => {
330
+ const ctx = createContext({ x: 10 });
331
+ const result = runInContext('x + 5', ctx);
332
+ expect(result).toBe(15);
333
+ });
334
+
335
+ await it('should run a string expression in empty context', async () => {
336
+ const ctx = createContext();
337
+ const result = runInContext('"passed"', ctx);
338
+ expect(result).toBe('passed');
339
+ });
340
+
341
+ await it('should access pre-populated context', async () => {
342
+ const ctx = createContext({ foo: 'bar', thing: 'lala' });
343
+ expect(ctx.foo).toBe('bar');
344
+ expect(ctx.thing).toBe('lala');
345
+ });
346
+
347
+ await it('should use context properties', async () => {
348
+ const ctx = createContext({ a: 1, b: 2 });
349
+ const result = runInContext('a + b', ctx);
350
+ expect(result).toBe(3);
351
+ });
352
+
353
+ await it('should evaluate typeof on context values', async () => {
354
+ const ctx = createContext({ num: 42, str: 'hello' });
355
+ expect(runInContext('typeof num', ctx)).toBe('number');
356
+ expect(runInContext('typeof str', ctx)).toBe('string');
357
+ });
358
+
359
+ await it('should handle context with array', async () => {
360
+ const ctx = createContext({ items: [10, 20, 30] });
361
+ const result = runInContext('items.reduce((a, b) => a + b, 0)', ctx);
362
+ expect(result).toBe(60);
363
+ });
364
+
365
+ await it('should throw SyntaxError for invalid code', async () => {
366
+ const ctx = createContext({});
367
+ expect(() => runInContext('function(', ctx)).toThrow();
368
+ });
369
+
370
+ await it('should throw for errors in context code', async () => {
371
+ const ctx = createContext({});
372
+ let caught = false;
373
+ try {
374
+ runInContext('throw new Error("ctx error")', ctx);
375
+ } catch (e: unknown) {
376
+ caught = true;
377
+ expect((e as Error).message).toBe('ctx error');
378
+ }
379
+ expect(caught).toBe(true);
380
+ });
381
+ });
382
+
383
+ // ==================== createContext / isContext ====================
384
+ await describe('createContext', async () => {
385
+ await it('should return an object', async () => {
386
+ const ctx = createContext();
387
+ expect(typeof ctx).toBe('object');
388
+ });
389
+
390
+ await it('should mark as context (isContext returns true)', async () => {
391
+ const ctx = createContext();
392
+ expect(isContext(ctx)).toBe(true);
393
+ });
394
+
395
+ await it('should preserve existing properties', async () => {
396
+ const ctx = createContext({ x: 42 });
397
+ expect(ctx.x).toBe(42);
398
+ expect(isContext(ctx)).toBe(true);
399
+ });
400
+
401
+ await it('should return same object when contextifying', async () => {
402
+ const sandbox: Record<string, unknown> = {};
403
+ const context = createContext(sandbox);
404
+ expect(sandbox === context).toBe(true);
405
+ });
406
+
407
+ await it('should contextify object with many properties', async () => {
408
+ const ctx = createContext({ a: 1, b: 'two', c: true, d: null, e: [1, 2] });
409
+ expect(isContext(ctx)).toBe(true);
410
+ expect(ctx.a).toBe(1);
411
+ expect(ctx.b).toBe('two');
412
+ expect(ctx.c).toBe(true);
413
+ expect(ctx.d).toBeNull();
414
+ });
415
+
416
+ await it('should handle contextifying twice', async () => {
417
+ const sandbox: Record<string, unknown> = { x: 1 };
418
+ const ctx1 = createContext(sandbox);
419
+ const ctx2 = createContext(sandbox);
420
+ expect(isContext(ctx1)).toBe(true);
421
+ expect(isContext(ctx2)).toBe(true);
422
+ expect(ctx1 === ctx2).toBe(true);
423
+ });
424
+
425
+ await it('isContext should return false for plain objects', async () => {
426
+ expect(isContext({})).toBe(false);
427
+ });
428
+
429
+ await it('isContext should throw for null', async () => {
430
+ expect(() => isContext(null as unknown as object)).toThrow();
431
+ });
432
+
433
+ await it('isContext should throw for undefined', async () => {
434
+ expect(() => isContext(undefined as unknown as object)).toThrow();
435
+ });
436
+
437
+ await it('isContext should throw for primitives', async () => {
438
+ expect(() => isContext(42 as unknown as object)).toThrow();
439
+ expect(() => isContext('string' as unknown as object)).toThrow();
440
+ expect(() => isContext(true as unknown as object)).toThrow();
441
+ });
442
+
443
+ await it('isContext should return false for array', async () => {
444
+ expect(isContext([])).toBe(false);
445
+ });
446
+
447
+ await it('should preserve context symbol as non-enumerable', async () => {
448
+ const ctx = createContext({ visible: true });
449
+ const keys = Object.keys(ctx);
450
+ expect(keys.length).toBe(1);
451
+ expect(keys[0]).toBe('visible');
452
+ });
453
+ });
454
+
455
+ // ==================== compileFunction ====================
456
+ await describe('compileFunction', async () => {
457
+ await it('should compile a function with no params', async () => {
458
+ const fn = compileFunction('return 42');
459
+ expect(typeof fn).toBe('function');
460
+ expect(fn()).toBe(42);
461
+ });
462
+
463
+ await it('should compile a function with params', async () => {
464
+ const fn = compileFunction('return a + b', ['a', 'b']);
465
+ expect(fn(3, 4)).toBe(7);
466
+ });
467
+
468
+ await it('should compile a function that returns a string', async () => {
469
+ const fn = compileFunction('return name.toUpperCase()', ['name']);
470
+ expect(fn('hello')).toBe('HELLO');
471
+ });
472
+
473
+ await it('should compile a function with string concatenation params', async () => {
474
+ const fn = compileFunction('return p + q + r + s + t', ['p', 'q', 'r', 's', 't']);
475
+ expect(fn('ab', 'cd', 'ef', 'gh', 'ij')).toBe('abcdefghij');
476
+ });
477
+
478
+ await it('should compile empty body', async () => {
479
+ const fn = compileFunction('');
480
+ expect(fn()).toBeUndefined();
481
+ });
482
+
483
+ await it('should compile with return statement only', async () => {
484
+ const fn = compileFunction('return');
485
+ expect(fn()).toBeUndefined();
486
+ });
487
+
488
+ await it('should throw SyntaxError for invalid code', async () => {
489
+ expect(() => compileFunction('function(')).toThrow();
490
+ });
491
+
492
+ await it('should compile function that uses closures', async () => {
493
+ const fn = compileFunction('var count = 0; count++; return count');
494
+ expect(fn()).toBe(1);
495
+ });
496
+
497
+ await it('should compile function with conditional logic', async () => {
498
+ const fn = compileFunction('return x > 0 ? "positive" : "non-positive"', ['x']);
499
+ expect(fn(5)).toBe('positive');
500
+ expect(fn(-1)).toBe('non-positive');
501
+ expect(fn(0)).toBe('non-positive');
502
+ });
503
+
504
+ await it('should compile function that creates and returns objects', async () => {
505
+ const fn = compileFunction('return { sum: a + b, product: a * b }', ['a', 'b']);
506
+ const result = fn(3, 4);
507
+ expect(result.sum).toBe(7);
508
+ expect(result.product).toBe(12);
509
+ });
510
+
511
+ await it('should compile function that creates arrays', async () => {
512
+ const fn = compileFunction('return [a, b, a + b]', ['a', 'b']);
513
+ const result = fn(1, 2);
514
+ expect(result[0]).toBe(1);
515
+ expect(result[1]).toBe(2);
516
+ expect(result[2]).toBe(3);
517
+ });
518
+
519
+ await it('should compile function with try-catch', async () => {
520
+ const fn = compileFunction('try { return JSON.parse(s); } catch(e) { return null; }', ['s']);
521
+ expect(fn('{"a":1}').a).toBe(1);
522
+ expect(fn('invalid')).toBeNull();
523
+ });
524
+
525
+ await it('should compile function that throws', async () => {
526
+ const fn = compileFunction('throw new Error("compiled error")');
527
+ let caught = false;
528
+ try {
529
+ fn();
530
+ } catch (e: unknown) {
531
+ caught = true;
532
+ expect((e as Error).message).toBe('compiled error');
533
+ }
534
+ expect(caught).toBe(true);
535
+ });
536
+
537
+ await it('should compile function with loop', async () => {
538
+ const fn = compileFunction('var sum = 0; for (var i = 1; i <= n; i++) sum += i; return sum', ['n']);
539
+ expect(fn(10)).toBe(55);
540
+ });
541
+
542
+ await it('should compile function with multiple statements', async () => {
543
+ const fn = compileFunction('var x = a * 2; var y = b * 3; return x + y', ['a', 'b']);
544
+ expect(fn(5, 10)).toBe(40);
545
+ });
546
+ });
547
+
548
+ // ==================== Script ====================
549
+ await describe('Script', async () => {
550
+ await it('should be constructable', async () => {
551
+ const script = new Script('1 + 2');
552
+ expect(script).toBeDefined();
553
+ });
554
+
555
+ await it('should run in this context', async () => {
556
+ const script = new Script('1 + 2');
557
+ expect(script.runInThisContext()).toBe(3);
558
+ });
559
+
560
+ await it('should run a string expression', async () => {
561
+ const script = new Script('"passed"');
562
+ expect(script.runInThisContext()).toBe('passed');
563
+ });
564
+
565
+ await it('should run in new context', async () => {
566
+ const script = new Script('x * y');
567
+ expect(script.runInNewContext({ x: 6, y: 7 })).toBe(42);
568
+ });
569
+
570
+ await it('should run in created context', async () => {
571
+ const ctx = createContext({ value: 100 });
572
+ const script = new Script('value + 1');
573
+ expect(script.runInContext(ctx)).toBe(101);
574
+ });
575
+
576
+ await it('should be reusable', async () => {
577
+ const script = new Script('n + 1');
578
+ expect(script.runInNewContext({ n: 1 })).toBe(2);
579
+ expect(script.runInNewContext({ n: 10 })).toBe(11);
580
+ expect(script.runInNewContext({ n: 100 })).toBe(101);
581
+ });
582
+
583
+ await it('should have createCachedData method', async () => {
584
+ const script = new Script('1');
585
+ const data = script.createCachedData();
586
+ expect(data instanceof Uint8Array).toBe(true);
587
+ });
588
+
589
+ await it('should throw for invalid code at construction or execution', async () => {
590
+ let threw = false;
591
+ try {
592
+ const script = new Script('function(');
593
+ script.runInThisContext();
594
+ } catch {
595
+ threw = true;
596
+ }
597
+ expect(threw).toBe(true);
598
+ });
599
+
600
+ await it('should run same script with different contexts', async () => {
601
+ const script = new Script('x + y');
602
+ expect(script.runInNewContext({ x: 1, y: 2 })).toBe(3);
603
+ expect(script.runInNewContext({ x: 10, y: 20 })).toBe(30);
604
+ expect(script.runInNewContext({ x: 100, y: 200 })).toBe(300);
605
+ });
606
+
607
+ await it('should handle script that returns object', async () => {
608
+ const script = new Script('({key: val})');
609
+ const result = script.runInNewContext({ val: 'test' });
610
+ expect((result as Record<string, unknown>).key).toBe('test');
611
+ });
612
+
613
+ await it('should handle script that uses array methods', async () => {
614
+ const script = new Script('items.map(x => x * 2)');
615
+ const result = script.runInNewContext({ items: [1, 2, 3] }) as number[];
616
+ expect(result[0]).toBe(2);
617
+ expect(result[1]).toBe(4);
618
+ expect(result[2]).toBe(6);
619
+ });
620
+
621
+ await it('should handle script with conditionals', async () => {
622
+ const script = new Script('x > 0 ? "positive" : "non-positive"');
623
+ expect(script.runInNewContext({ x: 5 })).toBe('positive');
624
+ expect(script.runInNewContext({ x: -1 })).toBe('non-positive');
625
+ });
626
+
627
+ await it('should propagate ReferenceError from script', async () => {
628
+ const script = new Script('undeclaredVariable');
629
+ expect(() => script.runInNewContext({})).toThrow();
630
+ });
631
+
632
+ await it('should propagate TypeError from script', async () => {
633
+ const script = new Script('null.property');
634
+ expect(() => script.runInThisContext()).toThrow();
635
+ });
636
+
637
+ await it('should run script that evaluates to boolean', async () => {
638
+ const script = new Script('a === b');
639
+ expect(script.runInNewContext({ a: 1, b: 1 })).toBe(true);
640
+ expect(script.runInNewContext({ a: 1, b: 2 })).toBe(false);
641
+ });
642
+
643
+ await it('should run script with template literal', async () => {
644
+ const script = new Script('`${greeting}, ${name}!`');
645
+ const result = script.runInNewContext({ greeting: 'Hello', name: 'World' });
646
+ expect(result).toBe('Hello, World!');
647
+ });
648
+
649
+ await it('should run script returning null', async () => {
650
+ const script = new Script('null');
651
+ expect(script.runInThisContext()).toBeNull();
652
+ });
653
+
654
+ await it('should run script returning undefined', async () => {
655
+ const script = new Script('undefined');
656
+ expect(script.runInThisContext()).toBeUndefined();
657
+ });
658
+
659
+ await it('should run empty script', async () => {
660
+ const script = new Script('');
661
+ expect(script.runInThisContext()).toBeUndefined();
662
+ });
663
+
664
+ await it('should run script that is only a comment', async () => {
665
+ const script = new Script('// just a comment');
666
+ expect(script.runInThisContext()).toBeUndefined();
667
+ });
668
+
669
+ await it('should run script with IIFE', async () => {
670
+ const script = new Script('(function() { return 42; })()');
671
+ expect(script.runInThisContext()).toBe(42);
672
+ });
673
+
674
+ await it('should run script with spread operator', async () => {
675
+ const script = new Script('Math.max(...nums)');
676
+ expect(script.runInNewContext({ nums: [1, 5, 3, 9, 2] })).toBe(9);
677
+ });
678
+ });
679
+
680
+ // ==================== error cases ====================
681
+ await describe('error cases', async () => {
682
+ await it('should propagate SyntaxError from runInThisContext', async () => {
683
+ let caught = false;
684
+ try {
685
+ runInThisContext('{{{');
686
+ } catch (e: unknown) {
687
+ caught = true;
688
+ expect((e as Error).constructor.name).toBe('SyntaxError');
689
+ }
690
+ expect(caught).toBe(true);
691
+ });
692
+
693
+ await it('should propagate SyntaxError from runInNewContext', async () => {
694
+ let caught = false;
695
+ try {
696
+ runInNewContext('{{{', {});
697
+ } catch (e: unknown) {
698
+ caught = true;
699
+ expect((e as Error).constructor.name).toBe('SyntaxError');
700
+ }
701
+ expect(caught).toBe(true);
702
+ });
703
+
704
+ await it('should propagate SyntaxError from Script', async () => {
705
+ let caught = false;
706
+ try {
707
+ const s = new Script('{{{');
708
+ s.runInThisContext();
709
+ } catch (e: unknown) {
710
+ caught = true;
711
+ expect((e as Error).constructor.name).toBe('SyntaxError');
712
+ }
713
+ expect(caught).toBe(true);
714
+ });
715
+
716
+ await it('should propagate RangeError', async () => {
717
+ expect(() => runInThisContext('new Array(-1)')).toThrow();
718
+ });
719
+
720
+ await it('should propagate custom thrown string', async () => {
721
+ let caught = false;
722
+ try {
723
+ runInThisContext('throw "string error"');
724
+ } catch (e: unknown) {
725
+ caught = true;
726
+ expect(e).toBe('string error');
727
+ }
728
+ expect(caught).toBe(true);
729
+ });
730
+
731
+ await it('should propagate custom thrown number', async () => {
732
+ let caught = false;
733
+ try {
734
+ runInThisContext('throw 42');
735
+ } catch (e: unknown) {
736
+ caught = true;
737
+ expect(e).toBe(42);
738
+ }
739
+ expect(caught).toBe(true);
740
+ });
741
+
742
+ await it('should propagate custom thrown object', async () => {
743
+ let caught = false;
744
+ try {
745
+ runInThisContext('throw {code: "ERR"}');
746
+ } catch (e: unknown) {
747
+ caught = true;
748
+ expect((e as Record<string, unknown>).code).toBe('ERR');
749
+ }
750
+ expect(caught).toBe(true);
751
+ });
752
+
753
+ await it('should throw for undefined property access in new context', async () => {
754
+ expect(() => runInNewContext('noSuchVar.property', {})).toThrow();
755
+ });
756
+
757
+ await it('should throw TypeError for calling non-function in new context', async () => {
758
+ expect(() => runInNewContext('x()', { x: 42 })).toThrow();
759
+ });
760
+
761
+ await it('should propagate error from compileFunction with invalid syntax', async () => {
762
+ let caught = false;
763
+ try {
764
+ compileFunction('return {{{');
765
+ } catch (e: unknown) {
766
+ caught = true;
767
+ expect((e as Error).constructor.name).toBe('SyntaxError');
768
+ }
769
+ expect(caught).toBe(true);
770
+ });
771
+ });
772
+
773
+ // ==================== edge cases ====================
774
+ await describe('edge cases', async () => {
775
+ await it('should handle code returning 0', async () => {
776
+ expect(runInThisContext('0')).toBe(0);
777
+ });
778
+
779
+ await it('should handle code returning empty string', async () => {
780
+ expect(runInThisContext('""')).toBe('');
781
+ });
782
+
783
+ await it('should handle code returning NaN', async () => {
784
+ const result = runInThisContext('NaN');
785
+ expect(typeof result).toBe('number');
786
+ // NaN !== NaN, so we use isNaN
787
+ expect(Number.isNaN(result as number)).toBe(true);
788
+ });
789
+
790
+ await it('should handle code returning Infinity', async () => {
791
+ expect(runInThisContext('Infinity')).toBe(Infinity);
792
+ expect(runInThisContext('-Infinity')).toBe(-Infinity);
793
+ });
794
+
795
+ await it('should handle code returning empty array', async () => {
796
+ const result = runInThisContext('[]') as unknown[];
797
+ expect(Array.isArray(result)).toBe(true);
798
+ expect(result.length).toBe(0);
799
+ });
800
+
801
+ await it('should handle code returning empty object', async () => {
802
+ const result = runInThisContext('({})') as Record<string, unknown>;
803
+ expect(typeof result).toBe('object');
804
+ expect(Object.keys(result).length).toBe(0);
805
+ });
806
+
807
+ await it('should handle multiline code', async () => {
808
+ const code = `
809
+ var a = 1;
810
+ var b = 2;
811
+ a + b;
812
+ `;
813
+ expect(runInThisContext(code)).toBe(3);
814
+ });
815
+
816
+ await it('should handle code with semicolons', async () => {
817
+ expect(runInThisContext('1; 2; 3')).toBe(3);
818
+ });
819
+
820
+ await it('should handle code with comma operator', async () => {
821
+ expect(runInThisContext('(1, 2, 3)')).toBe(3);
822
+ });
823
+
824
+ await it('should handle very long expressions', async () => {
825
+ const code = Array.from({ length: 100 }, (_, i) => String(i)).join(' + ');
826
+ const expected = (100 * 99) / 2; // Sum 0..99
827
+ expect(runInThisContext(code)).toBe(expected);
828
+ });
829
+
830
+ await it('should handle sandbox with numeric keys', async () => {
831
+ // Object.keys will make them strings, but they should still work
832
+ const result = runInNewContext('x', { x: 'works' });
833
+ expect(result).toBe('works');
834
+ });
835
+
836
+ await it('should handle sandbox with special property names', async () => {
837
+ const result = runInNewContext('$val + _val', { $val: 10, _val: 20 });
838
+ expect(result).toBe(30);
839
+ });
840
+
841
+ await it('should handle script with large sandbox', async () => {
842
+ const sandbox: Record<string, number> = {};
843
+ for (let i = 0; i < 50; i++) {
844
+ sandbox[`v${i}`] = i;
845
+ }
846
+ const result = runInNewContext('v0 + v49', sandbox);
847
+ expect(result).toBe(49);
848
+ });
849
+
850
+ await it('should handle createContext with no arguments', async () => {
851
+ const ctx = createContext();
852
+ expect(isContext(ctx)).toBe(true);
853
+ expect(typeof ctx).toBe('object');
854
+ });
855
+
856
+ await it('should handle script with Date', async () => {
857
+ const result = runInThisContext('typeof new Date()');
858
+ expect(result).toBe('object');
859
+ });
860
+
861
+ await it('should handle script with Map', async () => {
862
+ const result = runInThisContext('(() => { const m = new Map(); m.set("a", 1); return m.get("a"); })()');
863
+ expect(result).toBe(1);
864
+ });
865
+
866
+ await it('should handle script with Set', async () => {
867
+ const result = runInThisContext('(() => { const s = new Set([1,2,3,2,1]); return s.size; })()');
868
+ expect(result).toBe(3);
869
+ });
870
+
871
+ await it('should handle script with Promise.resolve', async () => {
872
+ const result = runInThisContext('Promise.resolve(42)');
873
+ expect(result instanceof Promise).toBe(true);
874
+ });
875
+
876
+ await it('should handle script with Symbol', async () => {
877
+ const result = runInThisContext('typeof Symbol("test")');
878
+ expect(result).toBe('symbol');
879
+ });
880
+ });
881
+ });
882
+ };