Skip to content

Commit 797c000

Browse files
committed
feat(quickjs): insomnia.test()/pm.test() lifecycle + real chai parity
Vendors chai@4.5.0 through the existing M3 npm-vendoring pipeline (sandbox-vendored-libs-list.ts -> chai.generated.ts) and binds its .expect as insomnia.expect, so pm.test() assertion messages match the hidden-window path byte-for-byte instead of a hand-rolled reimplementation. insomnia.test()/insomnia.test.skip() run entirely inside the VM, building requestTestResults in the same shape run-script.ts produces; a pm.test() the script doesn't await is still waited on before the run ends. Also generalizes the sendRequest bridge's teardown fix into a BridgeCalls registry that drains outstanding calls after the task settles (bounded by the script deadline) -- landing PR2's deferred item so a fire-and-forget insomnia.sendRequest(url, callback) actually gets its callback called instead of being silently dropped, with anything still open at the deadline cancelled and rejected with a real, observable error.
1 parent 7ddf333 commit 797c000

6 files changed

Lines changed: 462 additions & 65 deletions

File tree

packages/insomnia/scripts/sandbox-vendored-libs-list.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,8 @@ import { type VendoredLib } from './sandbox-vendored-lib';
99
export const VENDORED_LIBS: VendoredLib[] = [
1010
{ name: 'uuid', entry: "module.exports = require('uuid');" },
1111
{ name: 'ajv', entry: "module.exports = require('ajv').default || require('ajv');" },
12+
// Also consumed outside the template-tag sandbox: quickjs-script-engine.ts binds this bundle's
13+
// `.expect` as `insomnia.expect`, so pm.test()/insomnia.test() assertions match the hidden-window
14+
// path byte-for-byte (same library, same error messages) instead of a hand-rolled reimplementation.
15+
{ name: 'chai', entry: "module.exports = require('chai');" },
1216
];

packages/insomnia/src/scripting/quickjs-script-engine.test.ts

Lines changed: 157 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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(/Executing script timeout: 30/);
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

Comments
 (0)