Files
gstack/browse/test/browse-eval-wrapping.test.ts
Garry TanandClaude Fable 5 ce3f3848a7 fix(browse): chained IIFE + second statement no longer misclassified as one expression
isSingleParenOrIifeExpression accepted any tail after the initial group's
close as long as trailing chars looked chain-ish, so
`(async()=>{await 1})().then(x=>x); console.log('done')` classified as a
single expression and the expression wrapper emitted a SyntaxError. The
tail is now consumed as a strict member/call/index/optional-chain walk to
END of input via a shared string/escape-aware findBalancedClose scanner;
anything else (';', operators) demotes to the block wrapper. Negative +
positive tests added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 16:07:22 +00:00

87 lines
3.8 KiB
TypeScript

import { describe, it, expect } from 'bun:test';
import {
hasAwait,
isSingleParenOrIifeExpression,
needsBlockWrapper,
wrapForEvaluate,
} from '../src/read-commands';
// Regression: a chained IIFE followed by a SECOND statement was classified as
// a single expression, and the expression wrapper emitted a SyntaxError. The
// tail after the initial group must be a continuous member/call/index chain
// to END of input.
describe('chained IIFE followed by a second statement', () => {
it('is NOT a single expression and wraps to syntactically valid code', () => {
const code = "(async()=>{await 1})().then(x=>x); console.log('done')";
expect(isSingleParenOrIifeExpression(code)).toBe(false);
const wrapped = wrapForEvaluate(code);
expect(() => new Function('return ' + wrapped)).not.toThrow();
});
it('still accepts a pure chained IIFE (with optional chaining and index access)', () => {
expect(isSingleParenOrIifeExpression('(async()=>{await 1})().then(x=>x)')).toBe(true);
expect(isSingleParenOrIifeExpression('(getObj())?.items[0].run()')).toBe(true);
});
it('rejects an operator tail', () => {
expect(isSingleParenOrIifeExpression('(a)() + 1')).toBe(false);
});
});
describe('browse js / eval wrapping (#2727)', () => {
it('detects presence of await keyword', () => {
expect(hasAwait('await Promise.resolve(1)')).toBe(true);
expect(hasAwait('(async()=>{ await 1; return 2; })()')).toBe(true);
expect(hasAwait('Promise.resolve(1)')).toBe(false);
expect(hasAwait('// await inside comment\nreturn 123')).toBe(false);
});
it('detects single paren and IIFE expressions containing statements', () => {
expect(isSingleParenOrIifeExpression("(async()=>{ return 'x'; })()")).toBe(true);
expect(isSingleParenOrIifeExpression("(async()=>{await 1; return 'y';})()")).toBe(true);
expect(
isSingleParenOrIifeExpression(
"(async()=>{await new Promise(r=>setTimeout(r,50)); return 'done';})()"
)
).toBe(true);
expect(isSingleParenOrIifeExpression("((async () => { await 1; return 'z'; })())")).toBe(true);
expect(isSingleParenOrIifeExpression("await (async () => { return 'ok'; })()")).toBe(true);
expect(
isSingleParenOrIifeExpression(`(async () => {
const a = await Promise.resolve(10);
return a * 2;
})()`)
).toBe(true);
expect(isSingleParenOrIifeExpression('const a = 1; return a;')).toBe(false);
expect(isSingleParenOrIifeExpression("await Promise.resolve(1); return 'ok';")).toBe(false);
});
it('correctly decides block vs expression wrapper', () => {
expect(needsBlockWrapper("(async()=>{await 1; return 'y';})()")).toBe(false);
expect(needsBlockWrapper('await Promise.resolve(7)')).toBe(false);
expect(needsBlockWrapper('await Promise.resolve(7);')).toBe(false);
expect(needsBlockWrapper("await Promise.resolve(1); return 'ok';")).toBe(true);
expect(needsBlockWrapper('const x = await 1;\nreturn x + 1;')).toBe(true);
});
it('wraps code for evaluate properly preserving return values', () => {
// Non-await expressions remain unwrapped
expect(wrapForEvaluate("Promise.resolve('x')")).toBe("Promise.resolve('x')");
expect(wrapForEvaluate("(async()=>{ return 'x'; })()")).toBe("(async()=>{ return 'x'; })()");
// Simple await expression gets expression wrapper
expect(wrapForEvaluate('await Promise.resolve(7)')).toBe('(async()=>(await Promise.resolve(7)))()');
// Async IIFE with internal statements gets expression wrapper preserving return value
expect(wrapForEvaluate("(async()=>{await 1; return 'y';})()")).toBe(
"(async()=>((async()=>{await 1; return 'y';})()))()"
);
// Multi-statement sequence gets block wrapper
expect(wrapForEvaluate("await Promise.resolve(1); return 'ok';")).toBe(
"(async()=>{\nawait Promise.resolve(1); return 'ok';\n})()"
);
});
});