@@ -175,6 +175,9 @@ describe('runScriptInQuickJs', () => {
175175 '__task' ,
176176 'insomnia' ,
177177 '$' ,
178+ 'chai' ,
179+ '__testResults' ,
180+ '__testPromises' ,
178181 ] ) ;
179182 const unexpectedGlobals = ( data . sandboxGlobalNames as string [ ] ) . filter (
180183 name => ! baseline . has ( name ) && ! ALLOWED_EXTRA_GLOBALS . has ( name ) ,
@@ -429,6 +432,132 @@ describe('runScriptInQuickJs sendRequest bridge', () => {
429432 } ) ;
430433} ) ;
431434
435+ describe ( 'runScriptInQuickJs insomnia.test()/pm.test()' , ( ) => {
436+ it ( 'records pass, fail-with-message, and throw in one requestTestResults array, in order' , async ( ) => {
437+ const context = baseContext ( ) ;
438+
439+ const result = await runScriptInQuickJs ( {
440+ script : `
441+ await insomnia.test('passes', () => {
442+ insomnia.expect(200).to.eql(200);
443+ });
444+ await insomnia.test('fails with a message', () => {
445+ insomnia.expect(199).to.eql(200);
446+ });
447+ await insomnia.test('throws', () => {
448+ throw new Error('boom');
449+ });
450+ ` ,
451+ context,
452+ } ) ;
453+
454+ expect ( result . requestTestResults ) . toHaveLength ( 3 ) ;
455+ expect ( result . requestTestResults ?. [ 0 ] ) . toMatchObject ( { testCase : 'passes' , status : 'passed' } ) ;
456+ expect ( result . requestTestResults ?. [ 1 ] ) . toMatchObject ( {
457+ testCase : 'fails with a message' ,
458+ status : 'failed' ,
459+ errorMessage : expect . stringContaining ( 'expected 199 to deeply equal 200' ) ,
460+ } ) ;
461+ expect ( result . requestTestResults ?. [ 2 ] ) . toMatchObject ( {
462+ testCase : 'throws' ,
463+ status : 'failed' ,
464+ errorMessage : expect . stringContaining ( 'boom' ) ,
465+ } ) ;
466+ } ) ;
467+
468+ it ( '$ is a Postman-compat alias, and pm-style chai chains work: type/length/include/oneOf/below/keys/property' , async ( ) => {
469+ const context = baseContext ( ) ;
470+
471+ const result = await runScriptInQuickJs ( {
472+ script : `
473+ $.test('happy tests', () => {
474+ $.expect(200).to.eql(200);
475+ $.expect('uname').to.be.a('string');
476+ $.expect('a').to.have.lengthOf(1);
477+ $.expect('xxx_customer_id_yyy').to.include('customer_id');
478+ $.expect(201).to.be.oneOf([201, 202]);
479+ $.expect(199).to.be.below(200);
480+ $.expect({ a: 1, b: 2 }).to.have.all.keys('a', 'b');
481+ $.expect({ a: 1, b: 2 }).to.have.any.keys('a', 'b');
482+ $.expect({ a: 1, b: 2 }).to.not.have.any.keys('c', 'd');
483+ $.expect({ a: 1 }).to.have.property('a');
484+ $.expect({ a: 1, b: 2 }).to.be.a('object').that.has.all.keys('a', 'b');
485+ });
486+ ` ,
487+ context,
488+ } ) ;
489+
490+ expect ( result . requestTestResults ) . toEqual ( [
491+ expect . objectContaining ( { testCase : 'happy tests' , status : 'passed' } ) ,
492+ ] ) ;
493+ } ) ;
494+
495+ it ( 'waits for a test the script body did not await before considering the run done' , async ( ) => {
496+ const context = baseContext ( ) ;
497+
498+ const result = await runScriptInQuickJs ( {
499+ script : `
500+ insomnia.test('not awaited', async () => {
501+ // QuickJS has no timers bridged in; a few microtask hops stand in for "still pending
502+ // when the script body returns" without depending on one.
503+ await Promise.resolve().then(() => Promise.resolve()).then(() => Promise.resolve());
504+ insomnia.expect(1).to.eql(1);
505+ });
506+ insomnia.environment.set('reachedEnd', true);
507+ ` ,
508+ context,
509+ } ) ;
510+
511+ expect ( ( result . environment . data as Record < string , unknown > ) . reachedEnd ) . toBe ( true ) ;
512+ expect ( result . requestTestResults ) . toEqual ( [ expect . objectContaining ( { testCase : 'not awaited' , status : 'passed' } ) ] ) ;
513+ } ) ;
514+
515+ it ( 'records a skipped test without running its body' , async ( ) => {
516+ const context = baseContext ( ) ;
517+
518+ const result = await runScriptInQuickJs ( {
519+ script : `insomnia.test.skip('not run', () => { throw new Error('should never execute'); });` ,
520+ context,
521+ } ) ;
522+
523+ expect ( result . requestTestResults ) . toEqual ( [
524+ expect . objectContaining ( { testCase : 'not run' , status : 'skipped' } ) ,
525+ ] ) ;
526+ } ) ;
527+
528+ it ( 'is caught by the script timeout, not a silent hang, when a test never resolves' , async ( ) => {
529+ const context = baseContext ( ) ;
530+ context . settings = { timeout : 30 } as any ;
531+
532+ await expect (
533+ runScriptInQuickJs ( {
534+ script : `insomnia.test('never resolves', () => new Promise(() => {}));` ,
535+ context,
536+ } ) ,
537+ ) . rejects . toThrow ( / E x e c u t i n g s c r i p t t i m e o u t : 3 0 / ) ;
538+
539+ const laterResult = await runScriptInQuickJs ( {
540+ script : 'insomnia.environment.set("ranCleanly", true);' ,
541+ context : baseContext ( ) ,
542+ } ) ;
543+ expect ( ( laterResult . environment . data as Record < string , unknown > ) . ranCleanly ) . toBe ( true ) ;
544+ } ) ;
545+
546+ it ( 'never populates requestTestResults when the script throws before registering any test' , async ( ) => {
547+ const context = baseContext ( ) ;
548+
549+ await expect (
550+ runScriptInQuickJs ( {
551+ script : `
552+ throw new Error('top-level failure');
553+ insomnia.test('unreachable', () => {});
554+ ` ,
555+ context,
556+ } ) ,
557+ ) . rejects . toThrow ( 'top-level failure' ) ;
558+ } ) ;
559+ } ) ;
560+
432561/**
433562 * `vm.newPromise()` allocates three JSValues — the promise plus its `resolve`/`reject` functions —
434563 * and quickjs-emscripten frees the latter two only from inside `resolve()`/`reject()`. Disposing the
@@ -469,24 +598,24 @@ describe('runScriptInQuickJs sendRequest bridge teardown', () => {
469598 return fetchMock ;
470599 } ;
471600
472- it ( 'tears down cleanly when the script never awaits its sendRequest ' , async ( ) => {
601+ it ( 'delivers the callback of a sendRequest the script never awaited ' , async ( ) => {
473602 stubSlowFetch ( 120 ) ;
474603 const context = baseContext ( ) ;
475604
476- // Postman-style fire-and-forget: the script returns while the request is still in flight.
605+ // The Postman-style callback form returns undefined, so this script reaches the end of its body
606+ // with the request still in flight. The run drains outstanding bridge calls before tearing down,
607+ // so the callback still fires instead of being silently dropped.
477608 const result = await runScriptInQuickJs ( {
478609 script : `
479- insomnia.sendRequest('https://example.com', () => {});
610+ insomnia.sendRequest('https://example.com', (error, response) => {
611+ insomnia.environment.set('callbackBody', response.body);
612+ });
480613 insomnia.environment.set('finished', true);
481614 ` ,
482615 context,
483616 } ) ;
484617
485- expect ( ( result . environment . data as Record < string , unknown > ) . finished ) . toBe ( true ) ;
486-
487- // The response lands after teardown; it must be a silent no-op rather than an abort or an
488- // unhandled QuickJSUseAfterFree rejection out of the settle callback.
489- await new Promise ( resolve => setTimeout ( resolve , 250 ) ) ;
618+ expect ( result . environment . data ) . toMatchObject ( { finished : true , callbackBody : 'late' } ) ;
490619
491620 // A later run still works. Note this is NOT what detects the abort — a fresh context on the same
492621 // WASM module succeeds even after one, so only the assertions above are load-bearing here.
@@ -497,6 +626,26 @@ describe('runScriptInQuickJs sendRequest bridge teardown', () => {
497626 expect ( ( laterResult . environment . data as Record < string , unknown > ) . ranCleanly ) . toBe ( true ) ;
498627 } ) ;
499628
629+ it ( 'cancels and reports a sendRequest still outstanding when the drain deadline passes' , async ( ) => {
630+ stubSlowFetch ( 5000 ) ;
631+ const context = baseContext ( ) ;
632+ context . settings = { timeout : 150 } as any ;
633+
634+ const result = await runScriptInQuickJs ( {
635+ script : `
636+ insomnia.sendRequest('https://example.com', (error) => {
637+ console.log('sendRequest failed: ' + error);
638+ });
639+ insomnia.environment.set('finished', true);
640+ ` ,
641+ context,
642+ } ) ;
643+
644+ expect ( ( result . environment . data as Record < string , unknown > ) . finished ) . toBe ( true ) ;
645+ // The script learns the request was cancelled rather than the response vanishing silently.
646+ expect ( result . logs . some ( row => row . includes ( 'did not finish before the script did' ) ) ) . toBe ( true ) ;
647+ } ) ;
648+
500649 it ( 'tears down cleanly when the deadline fires while a sendRequest is in flight' , async ( ) => {
501650 stubSlowFetch ( 2000 ) ;
502651 const context = baseContext ( ) ;
0 commit comments