import { describe, it, expect } from 'vitest'; import { format } from './format'; function formatted(src: string): string { const r = format(src); if (!r.ok) throw new Error(`expected format to succeed, got: ${r.error.message}`); return r.text; } describe('format — basic shape', () => { it('normalizes a simple let with operator spacing', () => { const out = formatted('let x=1+2 * 3;'); expect(out).toBe('let x = 1 + 2 * 3;\n'); }); it('renders a fn declaration with body', () => { const out = formatted('fn process(order,user){order.total}'); expect(out).toBe( 'fn process(order, user) {\n' + '\torder.total\n' + '}\n' ); }); it('does not insert a blank between fn decls the user did not separate', () => { // Strict preserve-only policy: no source blank => no emitted blank. const out = formatted('fn a(){1}fn b(){2}'); expect(out).toBe('fn a() {\n\t1\n}\nfn b() {\n\t2\n}\n'); }); it('renders if / else if / else with blocks', () => { const out = formatted('if a{1}else if b{2}else{3}'); expect(out).toBe( 'if a {\n\t1\n} else if b {\n\t2\n} else {\n\t3\n}\n' ); }); it('renders an object-map literal inline when short', () => { const out = formatted('let o=#{a:1,b:2};'); expect(out).toBe('let o = #{ a: 1, b: 2 };\n'); }); it('renders log::info as a namespace call', () => { const out = formatted('log::info( "hi" );'); expect(out).toBe('log::info("hi");\n'); }); it('preserves comments verbatim before statements', () => { const out = formatted('// docstring\nfn process(){1}'); expect(out).toBe( '// docstring\nfn process() {\n\t1\n}\n' ); }); it('keeps block comments verbatim', () => { const out = formatted('/* keep me */ let x = 1;'); expect(out).toContain('/* keep me */'); expect(out).toContain('let x = 1;'); }); it('emits an empty block as `{}` without padding', () => { const out = formatted('fn noop(){}'); expect(out).toBe('fn noop() {}\n'); }); it('preserves string literals verbatim', () => { const out = formatted('let s = "hello\\nworld";'); expect(out).toBe('let s = "hello\\nworld";\n'); }); }); describe('format — reflow', () => { it('reflows a long argument list onto separate lines', () => { const src = 'process(aaaaaaaaaa, bbbbbbbbbb, cccccccccc, dddddddddd, eeeeeeeeee, ffffffffff, gggggggggg, hhhhhhhhhh);'; const out = formatted(src); // Should contain at least one newline inside the parens (multi-line). const callBlock = out.slice(out.indexOf('('), out.lastIndexOf(')') + 1); expect(callBlock).toContain('\n'); expect(callBlock.endsWith(',\n)')).toBe(true); }); it('keeps short argument lists inline', () => { const out = formatted('process(1, 2, 3);'); expect(out).toBe('process(1, 2, 3);\n'); }); }); describe('format — blank-line preservation', () => { it('preserves a single blank line between statements', () => { const src = 'let a = 1;\n\nlet b = 2;'; expect(formatted(src)).toBe('let a = 1;\n\nlet b = 2;\n'); }); it('collapses multiple blank lines to a single one', () => { const src = 'let a = 1;\n\n\n\nlet b = 2;'; expect(formatted(src)).toBe('let a = 1;\n\nlet b = 2;\n'); }); it('preserves blanks inside block bodies', () => { const src = 'fn process() {\n\tlet a = 1;\n\n\tlet b = 2;\n}'; expect(formatted(src)).toBe('fn process() {\n\tlet a = 1;\n\n\tlet b = 2;\n}\n'); }); it('does not invent blanks between adjacent statements', () => { expect(formatted('let a=1;let b=2;')).toBe('let a = 1;\nlet b = 2;\n'); }); }); describe('format — parse failures', () => { it('returns ok=false with a Rhai-flavored message and 1-based line/column', () => { // Pattern from the user complaint: `let;` should surface as // "Expecting name of a variable" at line/column. const r = format('let msg = ctx.request.params.name;\nlet;\n'); expect(r.ok).toBe(false); if (!r.ok) { expect(r.error.message).toBe('Expecting name of a variable'); expect(r.error.line).toBe(2); expect(r.error.column).toBe(4); expect(r.error.offset).toBeGreaterThanOrEqual(0); } }); it('reports script-incomplete on truncated input', () => { // `fn` alone — the parser expects a function name and hits EOF. const r = format('fn'); expect(r.ok).toBe(false); if (!r.ok) expect(r.error.message).toMatch(/script is incomplete/i); }); it('does not partially rewrite when parsing fails', () => { const r = format('let x = 1; this is garbage'); expect(r.ok).toBe(false); }); }); describe('format — idempotent', () => { it('formatting twice yields the same output', () => { const src = ` fn process(order,user) { if order.total > 100 { log::info("big", #{id:order.id}); } else { log::info("small"); } return order; } `; const a = formatted(src); const b = formatted(a); expect(b).toBe(a); }); });