|
| 1 | +import { describe, it, expect } from 'vitest'; |
| 2 | +import { processChunksToLines } from './processOutput'; |
| 3 | +import type { OutputChunk } from './actions'; |
| 4 | + |
| 5 | +/** Helper to build an OutputChunk. */ |
| 6 | +function chunk(text: string, stream: 'stdout' | 'stderr' = 'stdout'): OutputChunk { |
| 7 | + return { chunk: text, stream, timestamp: 0 }; |
| 8 | +} |
| 9 | + |
| 10 | +/** Helper to extract just the text from result lines. */ |
| 11 | +function texts(chunks: OutputChunk[]): string[] { |
| 12 | + return processChunksToLines(chunks).map((l) => l.text); |
| 13 | +} |
| 14 | + |
| 15 | +describe('processChunksToLines', () => { |
| 16 | + // --------------------------------------------------------------------------- |
| 17 | + // Basic newline handling |
| 18 | + // --------------------------------------------------------------------------- |
| 19 | + |
| 20 | + it('splits on \\n', () => { |
| 21 | + expect(texts([chunk('hello\nworld\n')])).toEqual(['hello', 'world']); |
| 22 | + }); |
| 23 | + |
| 24 | + it('keeps a trailing line that has no terminating newline', () => { |
| 25 | + expect(texts([chunk('hello\nworld')])).toEqual(['hello', 'world']); |
| 26 | + }); |
| 27 | + |
| 28 | + it('handles empty input', () => { |
| 29 | + expect(texts([])).toEqual([]); |
| 30 | + }); |
| 31 | + |
| 32 | + it('handles a single chunk with no newlines', () => { |
| 33 | + expect(texts([chunk('hello')])).toEqual(['hello']); |
| 34 | + }); |
| 35 | + |
| 36 | + // --------------------------------------------------------------------------- |
| 37 | + // \\r\\n (CRLF) handling |
| 38 | + // --------------------------------------------------------------------------- |
| 39 | + |
| 40 | + it('treats \\r\\n as a single newline', () => { |
| 41 | + expect(texts([chunk('hello\r\nworld\r\n')])).toEqual(['hello', 'world']); |
| 42 | + }); |
| 43 | + |
| 44 | + it('handles \\r\\n split across two chunks', () => { |
| 45 | + expect(texts([chunk('hello\r'), chunk('\nworld')])).toEqual(['hello', 'world']); |
| 46 | + }); |
| 47 | + |
| 48 | + // --------------------------------------------------------------------------- |
| 49 | + // Bare \\r (carriage return) — progress bar behavior |
| 50 | + // --------------------------------------------------------------------------- |
| 51 | + |
| 52 | + it('bare \\r overwrites the current line (single chunk)', () => { |
| 53 | + expect(texts([chunk('progress 50%\rprogress 100%\n')])).toEqual(['progress 100%']); |
| 54 | + }); |
| 55 | + |
| 56 | + it('bare \\r overwrites across multiple updates in one chunk', () => { |
| 57 | + expect(texts([chunk('10%\r20%\r30%\r40%\n')])).toEqual(['40%']); |
| 58 | + }); |
| 59 | + |
| 60 | + it('bare \\r overwrites across separate chunks', () => { |
| 61 | + expect(texts([chunk('downloading 50%\r'), chunk('downloading 100%\n')])).toEqual([ |
| 62 | + 'downloading 100%', |
| 63 | + ]); |
| 64 | + }); |
| 65 | + |
| 66 | + it('bare \\r at end of chunk followed by non-\\n text in next chunk overwrites', () => { |
| 67 | + expect(texts([chunk('old text\r'), chunk('new text')])).toEqual(['new text']); |
| 68 | + }); |
| 69 | + |
| 70 | + it('multiple progress updates across separate chunks collapse correctly', () => { |
| 71 | + expect(texts([chunk('10%\r'), chunk('20%\r'), chunk('30%\r'), chunk('done\n')])).toEqual([ |
| 72 | + 'done', |
| 73 | + ]); |
| 74 | + }); |
| 75 | + |
| 76 | + it('progress bar followed by normal output', () => { |
| 77 | + expect(texts([chunk('building...\rprogress 50%\rprogress 100%\nSuccess!\n')])).toEqual([ |
| 78 | + 'progress 100%', |
| 79 | + 'Success!', |
| 80 | + ]); |
| 81 | + }); |
| 82 | + |
| 83 | + // --------------------------------------------------------------------------- |
| 84 | + // Mixed scenarios |
| 85 | + // --------------------------------------------------------------------------- |
| 86 | + |
| 87 | + it('handles normal lines before and after progress bars', () => { |
| 88 | + expect( |
| 89 | + texts([chunk('Starting build\n'), chunk('0%\r50%\r100%\n'), chunk('Build complete\n')]) |
| 90 | + ).toEqual(['Starting build', '100%', 'Build complete']); |
| 91 | + }); |
| 92 | + |
| 93 | + it('handles interleaved stdout and stderr', () => { |
| 94 | + const result = processChunksToLines([ |
| 95 | + chunk('out line\n', 'stdout'), |
| 96 | + chunk('err line\n', 'stderr'), |
| 97 | + ]); |
| 98 | + expect(result).toEqual([ |
| 99 | + { text: 'out line', stream: 'stdout' }, |
| 100 | + { text: 'err line', stream: 'stderr' }, |
| 101 | + ]); |
| 102 | + }); |
| 103 | + |
| 104 | + it('bare \\r at the very end of all chunks produces no trailing line', () => { |
| 105 | + // A progress bar that never finishes with \n — should show nothing |
| 106 | + // because the \r resets the line and there's no further content. |
| 107 | + expect(texts([chunk('in progress\r')])).toEqual([]); |
| 108 | + }); |
| 109 | + |
| 110 | + it('empty lines are preserved', () => { |
| 111 | + expect(texts([chunk('a\n\nb\n')])).toEqual(['a', '', 'b']); |
| 112 | + }); |
| 113 | + |
| 114 | + it('handles chunk boundaries mid-text', () => { |
| 115 | + expect(texts([chunk('hel'), chunk('lo\nwor'), chunk('ld\n')])).toEqual(['hello', 'world']); |
| 116 | + }); |
| 117 | +}); |
0 commit comments