|
| 1 | +import { sync } from 'cross-spawn'; |
| 2 | +import { vi } from 'vitest'; |
| 3 | + |
| 4 | +import { postprocessOutput } from '../postprocess'; |
| 5 | + |
| 6 | +vi.mock('cross-spawn'); |
| 7 | + |
| 8 | +const mockSync = vi.mocked(sync); |
| 9 | + |
| 10 | +const baseConfig = { |
| 11 | + path: '/output', |
| 12 | + postProcess: [], |
| 13 | +}; |
| 14 | + |
| 15 | +const noopPostProcessors = {}; |
| 16 | + |
| 17 | +describe('postprocessOutput', () => { |
| 18 | + beforeEach(() => { |
| 19 | + vi.clearAllMocks(); |
| 20 | + }); |
| 21 | + |
| 22 | + it('should not call sync when postProcess is empty', () => { |
| 23 | + postprocessOutput(baseConfig, noopPostProcessors, ''); |
| 24 | + expect(mockSync).not.toHaveBeenCalled(); |
| 25 | + }); |
| 26 | + |
| 27 | + it('should call sync with command and resolved args', () => { |
| 28 | + mockSync.mockReturnValue({ error: undefined, status: 0 } as any); |
| 29 | + |
| 30 | + postprocessOutput( |
| 31 | + { ...baseConfig, postProcess: [{ args: ['fmt', '{{path}}'], command: 'dprint' }] }, |
| 32 | + noopPostProcessors, |
| 33 | + '', |
| 34 | + ); |
| 35 | + |
| 36 | + expect(mockSync).toHaveBeenCalledWith('dprint', ['fmt', '/output']); |
| 37 | + }); |
| 38 | + |
| 39 | + it('should replace {{path}} placeholder in args', () => { |
| 40 | + mockSync.mockReturnValue({ error: undefined, status: 0 } as any); |
| 41 | + |
| 42 | + postprocessOutput( |
| 43 | + { path: '/my/output', postProcess: [{ args: ['{{path}}', '--write'], command: 'prettier' }] }, |
| 44 | + noopPostProcessors, |
| 45 | + '', |
| 46 | + ); |
| 47 | + |
| 48 | + expect(mockSync).toHaveBeenCalledWith('prettier', ['/my/output', '--write']); |
| 49 | + }); |
| 50 | + |
| 51 | + it('should throw when the process fails to spawn (e.g., ENOENT)', () => { |
| 52 | + const spawnError = new Error('spawnSync oxfmt ENOENT'); |
| 53 | + mockSync.mockReturnValue({ error: spawnError, status: null } as any); |
| 54 | + |
| 55 | + expect(() => |
| 56 | + postprocessOutput( |
| 57 | + { ...baseConfig, postProcess: [{ args: ['{{path}}'], command: 'oxfmt' }] }, |
| 58 | + noopPostProcessors, |
| 59 | + '', |
| 60 | + ), |
| 61 | + ).toThrow('Post-processor "oxfmt" failed to run: spawnSync oxfmt ENOENT'); |
| 62 | + }); |
| 63 | + |
| 64 | + it('should throw with a custom name when the process fails to spawn', () => { |
| 65 | + const spawnError = new Error('spawnSync my-formatter ENOENT'); |
| 66 | + mockSync.mockReturnValue({ error: spawnError, status: null } as any); |
| 67 | + |
| 68 | + expect(() => |
| 69 | + postprocessOutput( |
| 70 | + { |
| 71 | + ...baseConfig, |
| 72 | + postProcess: [{ args: ['{{path}}'], command: 'my-formatter', name: 'My Formatter' }], |
| 73 | + }, |
| 74 | + noopPostProcessors, |
| 75 | + '', |
| 76 | + ), |
| 77 | + ).toThrow('Post-processor "My Formatter" failed to run: spawnSync my-formatter ENOENT'); |
| 78 | + }); |
| 79 | + |
| 80 | + it('should throw when the process exits with a non-zero status code', () => { |
| 81 | + mockSync.mockReturnValue({ error: undefined, status: 1, stderr: Buffer.from('') } as any); |
| 82 | + |
| 83 | + expect(() => |
| 84 | + postprocessOutput( |
| 85 | + { ...baseConfig, postProcess: [{ args: ['{{path}}'], command: 'prettier' }] }, |
| 86 | + noopPostProcessors, |
| 87 | + '', |
| 88 | + ), |
| 89 | + ).toThrow('Post-processor "prettier" exited with code 1'); |
| 90 | + }); |
| 91 | + |
| 92 | + it('should include stderr output in error message when process fails', () => { |
| 93 | + mockSync.mockReturnValue({ |
| 94 | + error: undefined, |
| 95 | + status: 2, |
| 96 | + stderr: Buffer.from('error: file not found'), |
| 97 | + } as any); |
| 98 | + |
| 99 | + expect(() => |
| 100 | + postprocessOutput( |
| 101 | + { ...baseConfig, postProcess: [{ args: ['{{path}}'], command: 'biome' }] }, |
| 102 | + noopPostProcessors, |
| 103 | + '', |
| 104 | + ), |
| 105 | + ).toThrow('Post-processor "biome" exited with code 2:\nerror: file not found'); |
| 106 | + }); |
| 107 | + |
| 108 | + it('should skip unknown string preset processors', () => { |
| 109 | + postprocessOutput({ ...baseConfig, postProcess: ['unknown-preset'] }, noopPostProcessors, ''); |
| 110 | + expect(mockSync).not.toHaveBeenCalled(); |
| 111 | + }); |
| 112 | + |
| 113 | + it('should resolve and run string preset processors', () => { |
| 114 | + mockSync.mockReturnValue({ error: undefined, status: 0 } as any); |
| 115 | + |
| 116 | + const processors = { |
| 117 | + prettier: { args: ['--write', '{{path}}'], command: 'prettier', name: 'Prettier' }, |
| 118 | + }; |
| 119 | + |
| 120 | + postprocessOutput({ ...baseConfig, postProcess: ['prettier'] }, processors, ''); |
| 121 | + |
| 122 | + expect(mockSync).toHaveBeenCalledWith('prettier', ['--write', '/output']); |
| 123 | + }); |
| 124 | + |
| 125 | + it('should stop processing and throw on first failure', () => { |
| 126 | + const spawnError = new Error('ENOENT'); |
| 127 | + mockSync.mockReturnValue({ error: spawnError, status: null } as any); |
| 128 | + |
| 129 | + expect(() => |
| 130 | + postprocessOutput( |
| 131 | + { |
| 132 | + ...baseConfig, |
| 133 | + postProcess: [ |
| 134 | + { args: ['{{path}}'], command: 'first' }, |
| 135 | + { args: ['{{path}}'], command: 'second' }, |
| 136 | + ], |
| 137 | + }, |
| 138 | + noopPostProcessors, |
| 139 | + '', |
| 140 | + ), |
| 141 | + ).toThrow('Post-processor "first" failed to run: ENOENT'); |
| 142 | + |
| 143 | + expect(mockSync).toHaveBeenCalledTimes(1); |
| 144 | + }); |
| 145 | +}); |
0 commit comments