diff --git a/src/__tests__/utils/output.test.ts b/src/__tests__/utils/output.test.ts new file mode 100644 index 0000000..7c55cf9 --- /dev/null +++ b/src/__tests__/utils/output.test.ts @@ -0,0 +1,158 @@ +import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import {serialize, format_from_ext, print} from '../../utils/output'; + +describe('utils/output.serialize csv', ()=>{ + it('serializes array of flat objects as RFC 4180 CSV with header row', ()=>{ + const rows = [ + {url: 'https://a.test/1', title: 'A', price: 1.5}, + {url: 'https://a.test/2', title: 'B', price: 2.0}, + ]; + const out = serialize(rows, 'csv'); + const lines = out.trim().split('\n'); + expect(lines[0]).toBe('url,title,price'); + expect(lines[1]).toBe('https://a.test/1,A,1.5'); + expect(lines[2]).toBe('https://a.test/2,B,2'); + }); + + it('quotes and escapes embedded commas, quotes, and newlines', ()=>{ + const rows = [{name: 'Smith, John', note: 'He said "hi"'}, + {name: 'multi\nline', note: 'ok'}]; + const out = serialize(rows, 'csv'); + const lines = out.trim().split(/\n/); + // header + expect(lines[0]).toBe('name,note'); + // row 1: both fields need quoting; embedded quote doubled + expect(lines[1]).toBe('"Smith, John","He said ""hi"""'); + }); + + it('unions keys across heterogeneous rows', ()=>{ + const rows = [{a: 1, b: 2}, {a: 3, c: 4}]; + const out = serialize(rows, 'csv'); + const lines = out.trim().split('\n'); + expect(lines[0]).toBe('a,b,c'); + expect(lines[1]).toBe('1,2,'); + expect(lines[2]).toBe('3,,4'); + }); + + it('wraps a single object as one CSV row', ()=>{ + const out = serialize({a: 1, b: 'x'}, 'csv'); + expect(out.trim()).toBe('a,b\n1,x'); + }); + + it('serializes nested values via JSON', ()=>{ + const rows = [{id: 1, meta: {tag: 'x'}}]; + const out = serialize(rows, 'csv'); + const lines = out.trim().split('\n'); + expect(lines[1]).toBe('1,"{""tag"":""x""}"'); + }); +}); + +describe('utils/output.serialize markdown', ()=>{ + it('renders an array of objects as a Markdown table', ()=>{ + const rows = [{a: 1, b: 'x'}, {a: 2, b: 'y'}]; + const out = serialize(rows, 'markdown'); + expect(out).toContain('| a | b |'); + expect(out).toContain('| --- | --- |'); + expect(out).toContain('| 1 | x |'); + expect(out).toContain('| 2 | y |'); + }); + + it('escapes pipes and newlines inside cells', ()=>{ + const rows = [{a: 'a|b', b: 'line1\nline2'}]; + const out = serialize(rows, 'markdown'); + expect(out).toContain('| a\\|b | line1 line2 |'); + }); + + it('falls back to a fenced JSON block for non-tabular data', ()=>{ + const out = serialize([1, 2, 3], 'markdown'); + expect(out.startsWith('```json')).toBe(true); + }); +}); + +describe('utils/output.serialize html', ()=>{ + it('renders an array of objects as an HTML table', ()=>{ + const rows = [{a: 1, b: ''}]; + const out = serialize(rows, 'html'); + expect(out).toContain('ab'); + expect(out).toContain('1<x>'); + }); + + it('escapes HTML in non-tabular fallback', ()=>{ + const out = serialize('