diff options
Diffstat (limited to 'packages/core/src/utils/editor.test.ts')
| -rw-r--r-- | packages/core/src/utils/editor.test.ts | 424 |
1 files changed, 245 insertions, 179 deletions
diff --git a/packages/core/src/utils/editor.test.ts b/packages/core/src/utils/editor.test.ts index 8bc7a49c..84cca8b8 100644 --- a/packages/core/src/utils/editor.test.ts +++ b/packages/core/src/utils/editor.test.ts @@ -19,6 +19,7 @@ import { openDiff, allowEditorTypeInSandbox, isEditorAvailable, + type EditorType, } from './editor.js'; import { execSync, spawn } from 'child_process'; @@ -27,225 +28,290 @@ vi.mock('child_process', () => ({ spawn: vi.fn(), })); -describe('checkHasEditorType', () => { +const originalPlatform = process.platform; + +describe('editor utils', () => { beforeEach(() => { vi.clearAllMocks(); - }); - - it('should return true for vscode if "code" command exists', () => { - (execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/code')); - expect(checkHasEditorType('vscode')).toBe(true); - const expectedCommand = - process.platform === 'win32' ? 'where.exe code.cmd' : 'command -v code'; - expect(execSync).toHaveBeenCalledWith(expectedCommand, { - stdio: 'ignore', + delete process.env.SANDBOX; + Object.defineProperty(process, 'platform', { + value: originalPlatform, + writable: true, }); }); - it('should return false for vscode if "code" command does not exist', () => { - (execSync as Mock).mockImplementation(() => { - throw new Error(); + afterEach(() => { + vi.restoreAllMocks(); + delete process.env.SANDBOX; + Object.defineProperty(process, 'platform', { + value: originalPlatform, + writable: true, }); - expect(checkHasEditorType('vscode')).toBe(false); }); - it('should return true for windsurf if "windsurf" command exists', () => { - (execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/windsurf')); - expect(checkHasEditorType('windsurf')).toBe(true); - expect(execSync).toHaveBeenCalledWith('command -v windsurf', { - stdio: 'ignore', - }); - }); + describe('checkHasEditorType', () => { + const testCases: Array<{ + editor: EditorType; + command: string; + win32Command: string; + }> = [ + { editor: 'vscode', command: 'code', win32Command: 'code.cmd' }, + { editor: 'windsurf', command: 'windsurf', win32Command: 'windsurf' }, + { editor: 'cursor', command: 'cursor', win32Command: 'cursor' }, + { editor: 'vim', command: 'vim', win32Command: 'vim' }, + ]; - it('should return false for windsurf if "windsurf" command does not exist', () => { - (execSync as Mock).mockImplementation(() => { - throw new Error(); - }); - expect(checkHasEditorType('windsurf')).toBe(false); - }); + for (const { editor, command, win32Command } of testCases) { + describe(`${editor}`, () => { + it(`should return true if "${command}" command exists on non-windows`, () => { + Object.defineProperty(process, 'platform', { value: 'linux' }); + (execSync as Mock).mockReturnValue( + Buffer.from(`/usr/bin/${command}`), + ); + expect(checkHasEditorType(editor)).toBe(true); + expect(execSync).toHaveBeenCalledWith(`command -v ${command}`, { + stdio: 'ignore', + }); + }); - it('should return true for cursor if "cursor" command exists', () => { - (execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/cursor')); - expect(checkHasEditorType('cursor')).toBe(true); - expect(execSync).toHaveBeenCalledWith('command -v cursor', { - stdio: 'ignore', - }); - }); + it(`should return false if "${command}" command does not exist on non-windows`, () => { + Object.defineProperty(process, 'platform', { value: 'linux' }); + (execSync as Mock).mockImplementation(() => { + throw new Error(); + }); + expect(checkHasEditorType(editor)).toBe(false); + }); - it('should return false for cursor if "cursor" command does not exist', () => { - (execSync as Mock).mockImplementation(() => { - throw new Error(); - }); - expect(checkHasEditorType('cursor')).toBe(false); - }); + it(`should return true if "${win32Command}" command exists on windows`, () => { + Object.defineProperty(process, 'platform', { value: 'win32' }); + (execSync as Mock).mockReturnValue( + Buffer.from(`C:\\Program Files\\...\\${win32Command}`), + ); + expect(checkHasEditorType(editor)).toBe(true); + expect(execSync).toHaveBeenCalledWith(`where.exe ${win32Command}`, { + stdio: 'ignore', + }); + }); - it('should return true for vim if "vim" command exists', () => { - (execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/vim')); - expect(checkHasEditorType('vim')).toBe(true); - const expectedCommand = - process.platform === 'win32' ? 'where.exe vim' : 'command -v vim'; - expect(execSync).toHaveBeenCalledWith(expectedCommand, { - stdio: 'ignore', - }); + it(`should return false if "${win32Command}" command does not exist on windows`, () => { + Object.defineProperty(process, 'platform', { value: 'win32' }); + (execSync as Mock).mockImplementation(() => { + throw new Error(); + }); + expect(checkHasEditorType(editor)).toBe(false); + }); + }); + } }); - it('should return false for vim if "vim" command does not exist', () => { - (execSync as Mock).mockImplementation(() => { - throw new Error(); + describe('getDiffCommand', () => { + it('should return the correct command for vscode', () => { + const command = getDiffCommand('old.txt', 'new.txt', 'vscode'); + expect(command).toEqual({ + command: 'code', + args: ['--wait', '--diff', 'old.txt', 'new.txt'], + }); }); - expect(checkHasEditorType('vim')).toBe(false); - }); -}); -describe('getDiffCommand', () => { - it('should return the correct command for vscode', () => { - const command = getDiffCommand('old.txt', 'new.txt', 'vscode'); - expect(command).toEqual({ - command: 'code', - args: ['--wait', '--diff', 'old.txt', 'new.txt'], + it('should return the correct command for windsurf', () => { + const command = getDiffCommand('old.txt', 'new.txt', 'windsurf'); + expect(command).toEqual({ + command: 'windsurf', + args: ['--wait', '--diff', 'old.txt', 'new.txt'], + }); }); - }); - - it('should return the correct command for vim', () => { - const command = getDiffCommand('old.txt', 'new.txt', 'vim'); - expect(command?.command).toBe('vim'); - expect(command?.args).toContain('old.txt'); - expect(command?.args).toContain('new.txt'); - }); - - it('should return null for an unsupported editor', () => { - // @ts-expect-error Testing unsupported editor - const command = getDiffCommand('old.txt', 'new.txt', 'foobar'); - expect(command).toBeNull(); - }); -}); -describe('openDiff', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); + it('should return the correct command for cursor', () => { + const command = getDiffCommand('old.txt', 'new.txt', 'cursor'); + expect(command).toEqual({ + command: 'cursor', + args: ['--wait', '--diff', 'old.txt', 'new.txt'], + }); + }); - it('should call spawn for vscode', async () => { - const mockSpawn = { - on: vi.fn((event, cb) => { - if (event === 'close') { - cb(0); - } - }), - }; - (spawn as Mock).mockReturnValue(mockSpawn); - await openDiff('old.txt', 'new.txt', 'vscode'); - expect(spawn).toHaveBeenCalledWith( - 'code', - ['--wait', '--diff', 'old.txt', 'new.txt'], - { stdio: 'inherit' }, - ); - expect(mockSpawn.on).toHaveBeenCalledWith('close', expect.any(Function)); - expect(mockSpawn.on).toHaveBeenCalledWith('error', expect.any(Function)); - }); + it('should return the correct command for vim', () => { + const command = getDiffCommand('old.txt', 'new.txt', 'vim'); + expect(command).toEqual({ + command: 'vim', + args: [ + '-d', + '-i', + 'NONE', + '-c', + 'wincmd h | set readonly | wincmd l', + '-c', + 'highlight DiffAdd cterm=bold ctermbg=22 guibg=#005f00 | highlight DiffChange cterm=bold ctermbg=24 guibg=#005f87 | highlight DiffText ctermbg=21 guibg=#0000af | highlight DiffDelete ctermbg=52 guibg=#5f0000', + '-c', + 'set showtabline=2 | set tabline=[Instructions]\\ :wqa(save\\ &\\ quit)\\ \\|\\ i/esc(toggle\\ edit\\ mode)', + '-c', + 'wincmd h | setlocal statusline=OLD\\ FILE', + '-c', + 'wincmd l | setlocal statusline=%#StatusBold#NEW\\ FILE\\ :wqa(save\\ &\\ quit)\\ \\|\\ i/esc(toggle\\ edit\\ mode)', + '-c', + 'autocmd WinClosed * wqa', + 'old.txt', + 'new.txt', + ], + }); + }); - it('should call execSync for vim', async () => { - await openDiff('old.txt', 'new.txt', 'vim'); - expect(execSync).toHaveBeenCalled(); - const command = (execSync as Mock).mock.calls[0][0]; - expect(command).toContain('vim'); - expect(command).toContain('old.txt'); - expect(command).toContain('new.txt'); + it('should return null for an unsupported editor', () => { + // @ts-expect-error Testing unsupported editor + const command = getDiffCommand('old.txt', 'new.txt', 'foobar'); + expect(command).toBeNull(); + }); }); - it('should handle spawn error for vscode', async () => { - const mockSpawn = { - on: vi.fn((event, cb) => { - if (event === 'error') { - cb(new Error('spawn error')); - } - }), - }; - (spawn as Mock).mockReturnValue(mockSpawn); - await expect(openDiff('old.txt', 'new.txt', 'vscode')).rejects.toThrow( - 'spawn error', - ); - }); -}); + describe('openDiff', () => { + it('should call spawn for vscode', async () => { + const mockSpawn = { + on: vi.fn((event, cb) => { + if (event === 'close') { + cb(0); + } + }), + }; + (spawn as Mock).mockReturnValue(mockSpawn); + await openDiff('old.txt', 'new.txt', 'vscode'); + expect(spawn).toHaveBeenCalledWith( + 'code', + ['--wait', '--diff', 'old.txt', 'new.txt'], + { stdio: 'inherit' }, + ); + expect(mockSpawn.on).toHaveBeenCalledWith('close', expect.any(Function)); + expect(mockSpawn.on).toHaveBeenCalledWith('error', expect.any(Function)); + }); -describe('allowEditorTypeInSandbox', () => { - afterEach(() => { - delete process.env.SANDBOX; - }); + it('should reject if spawn for vscode fails', async () => { + const mockError = new Error('spawn error'); + const mockSpawn = { + on: vi.fn((event, cb) => { + if (event === 'error') { + cb(mockError); + } + }), + }; + (spawn as Mock).mockReturnValue(mockSpawn); + await expect(openDiff('old.txt', 'new.txt', 'vscode')).rejects.toThrow( + 'spawn error', + ); + }); - it('should allow vim in sandbox mode', () => { - process.env.SANDBOX = 'sandbox'; - expect(allowEditorTypeInSandbox('vim')).toBe(true); - }); + it('should reject if vscode exits with non-zero code', async () => { + const mockSpawn = { + on: vi.fn((event, cb) => { + if (event === 'close') { + cb(1); + } + }), + }; + (spawn as Mock).mockReturnValue(mockSpawn); + await expect(openDiff('old.txt', 'new.txt', 'vscode')).rejects.toThrow( + 'VS Code exited with code 1', + ); + }); - it('should allow vim when not in sandbox mode', () => { - delete process.env.SANDBOX; - expect(allowEditorTypeInSandbox('vim')).toBe(true); - }); + const execSyncEditors: EditorType[] = ['vim', 'windsurf', 'cursor']; + for (const editor of execSyncEditors) { + it(`should call execSync for ${editor} on non-windows`, async () => { + Object.defineProperty(process, 'platform', { value: 'linux' }); + await openDiff('old.txt', 'new.txt', editor); + expect(execSync).toHaveBeenCalledTimes(1); + const diffCommand = getDiffCommand('old.txt', 'new.txt', editor)!; + const expectedCommand = `${ + diffCommand.command + } ${diffCommand.args.map((arg) => `"${arg}"`).join(' ')}`; + expect(execSync).toHaveBeenCalledWith(expectedCommand, { + stdio: 'inherit', + encoding: 'utf8', + }); + }); - it('should not allow vscode in sandbox mode', () => { - process.env.SANDBOX = 'sandbox'; - expect(allowEditorTypeInSandbox('vscode')).toBe(false); - }); + it(`should call execSync for ${editor} on windows`, async () => { + Object.defineProperty(process, 'platform', { value: 'win32' }); + await openDiff('old.txt', 'new.txt', editor); + expect(execSync).toHaveBeenCalledTimes(1); + const diffCommand = getDiffCommand('old.txt', 'new.txt', editor)!; + const expectedCommand = `${diffCommand.command} ${diffCommand.args.join( + ' ', + )}`; + expect(execSync).toHaveBeenCalledWith(expectedCommand, { + stdio: 'inherit', + encoding: 'utf8', + }); + }); + } - it('should allow vscode when not in sandbox mode', () => { - delete process.env.SANDBOX; - expect(allowEditorTypeInSandbox('vscode')).toBe(true); + it('should log an error if diff command is not available', async () => { + const consoleErrorSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + // @ts-expect-error Testing unsupported editor + await openDiff('old.txt', 'new.txt', 'foobar'); + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'No diff tool available. Install vim or vscode.', + ); + }); }); - it('should not allow windsurf in sandbox mode', () => { - process.env.SANDBOX = 'sandbox'; - expect(allowEditorTypeInSandbox('windsurf')).toBe(false); - }); + describe('allowEditorTypeInSandbox', () => { + it('should allow vim in sandbox mode', () => { + process.env.SANDBOX = 'sandbox'; + expect(allowEditorTypeInSandbox('vim')).toBe(true); + }); - it('should allow windsurf when not in sandbox mode', () => { - delete process.env.SANDBOX; - expect(allowEditorTypeInSandbox('windsurf')).toBe(true); - }); + it('should allow vim when not in sandbox mode', () => { + expect(allowEditorTypeInSandbox('vim')).toBe(true); + }); - it('should not allow cursor in sandbox mode', () => { - process.env.SANDBOX = 'sandbox'; - expect(allowEditorTypeInSandbox('cursor')).toBe(false); - }); + const guiEditors: EditorType[] = ['vscode', 'windsurf', 'cursor']; + for (const editor of guiEditors) { + it(`should not allow ${editor} in sandbox mode`, () => { + process.env.SANDBOX = 'sandbox'; + expect(allowEditorTypeInSandbox(editor)).toBe(false); + }); - it('should allow cursor when not in sandbox mode', () => { - delete process.env.SANDBOX; - expect(allowEditorTypeInSandbox('cursor')).toBe(true); + it(`should allow ${editor} when not in sandbox mode`, () => { + expect(allowEditorTypeInSandbox(editor)).toBe(true); + }); + } }); -}); -describe('isEditorAvailable', () => { - afterEach(() => { - delete process.env.SANDBOX; - }); + describe('isEditorAvailable', () => { + it('should return false for undefined editor', () => { + expect(isEditorAvailable(undefined)).toBe(false); + }); - it('should return false for undefined editor', () => { - expect(isEditorAvailable(undefined)).toBe(false); - }); + it('should return false for empty string editor', () => { + expect(isEditorAvailable('')).toBe(false); + }); - it('should return false for empty string editor', () => { - expect(isEditorAvailable('')).toBe(false); - }); + it('should return false for invalid editor type', () => { + expect(isEditorAvailable('invalid-editor')).toBe(false); + }); - it('should return false for invalid editor type', () => { - expect(isEditorAvailable('invalid-editor')).toBe(false); - }); + it('should return true for vscode when installed and not in sandbox mode', () => { + (execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/code')); + expect(isEditorAvailable('vscode')).toBe(true); + }); - it('should return true for vscode when installed and not in sandbox mode', () => { - (execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/code')); - expect(isEditorAvailable('vscode')).toBe(true); - }); + it('should return false for vscode when not installed and not in sandbox mode', () => { + (execSync as Mock).mockImplementation(() => { + throw new Error(); + }); + expect(isEditorAvailable('vscode')).toBe(false); + }); - it('should return false for vscode when not installed and not in sandbox mode', () => { - (execSync as Mock).mockImplementation(() => { - throw new Error(); + it('should return false for vscode when installed and in sandbox mode', () => { + (execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/code')); + process.env.SANDBOX = 'sandbox'; + expect(isEditorAvailable('vscode')).toBe(false); }); - expect(isEditorAvailable('vscode')).toBe(false); - }); - it('should return false for vscode when installed and in sandbox mode', () => { - (execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/code')); - process.env.SANDBOX = 'sandbox'; - expect(isEditorAvailable('vscode')).toBe(false); + it('should return true for vim when installed and in sandbox mode', () => { + (execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/vim')); + process.env.SANDBOX = 'sandbox'; + expect(isEditorAvailable('vim')).toBe(true); + }); }); }); |
