native-path-opener.spec.ts 3.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. type ExecFileCallback = (
  2. error: (Error & { code?: string | number }) | null,
  3. stdout: string,
  4. stderr: string,
  5. ) => void
  6. type ExecFileMock = (
  7. command: string,
  8. args: readonly string[],
  9. options: { encoding: string; signal: AbortSignal; windowsHide: boolean },
  10. callback: ExecFileCallback,
  11. ) => void
  12. const { execFileMock } = vi.hoisted(() => ({ execFileMock: vi.fn<ExecFileMock>() }))
  13. vi.mock('node:child_process', () => ({ execFile: execFileMock }))
  14. import { describe, expect, it, vi } from 'vitest'
  15. import { openNativePath, type PathOpenerRunner } from '../src/native-path-opener.ts'
  16. const signal = () => new AbortController().signal
  17. describe('native path opener', () => {
  18. it('opens with macOS open(1)', async () => {
  19. const run = vi.fn<PathOpenerRunner>(async () => ({ stdout: '', stderr: '' }))
  20. await openNativePath('/Users/test/file.txt', signal(), { platform: 'darwin', run })
  21. expect(run).toHaveBeenCalledWith('open', ['/Users/test/file.txt'], expect.any(AbortSignal))
  22. })
  23. it('opens with Windows Invoke-Item and escapes single quotes', async () => {
  24. const run = vi.fn<PathOpenerRunner>(async () => ({ stdout: '', stderr: '' }))
  25. await openNativePath("C:\\work\\o'reilly.txt", signal(), { platform: 'win32', run })
  26. expect(run).toHaveBeenCalledWith(
  27. 'powershell.exe',
  28. ['-NoProfile', '-Command', "Invoke-Item -LiteralPath 'C:\\work\\o''reilly.txt'"],
  29. expect.any(AbortSignal),
  30. )
  31. })
  32. it('opens with Linux xdg-open', async () => {
  33. const run = vi.fn<PathOpenerRunner>(async () => ({ stdout: '', stderr: '' }))
  34. await openNativePath('/tmp/a.txt', signal(), { platform: 'linux', run })
  35. expect(run).toHaveBeenCalledWith('xdg-open', ['/tmp/a.txt'], expect.any(AbortSignal))
  36. })
  37. it('rejects unsupported platforms', async () => {
  38. await expect(openNativePath('/x', signal(), { platform: 'freebsd' as NodeJS.Platform }))
  39. .rejects.toThrow('unsupported on freebsd')
  40. })
  41. it('uses the current process platform when no platform override is supplied', async () => {
  42. const run = vi.fn<PathOpenerRunner>(async () => ({ stdout: '', stderr: '' }))
  43. await openNativePath('/tmp/platform-default.txt', signal(), { run })
  44. const expected = process.platform === 'win32'
  45. ? 'powershell.exe'
  46. : process.platform === 'linux'
  47. ? 'xdg-open'
  48. : 'open'
  49. expect(run.mock.calls[0]?.[0]).toBe(expected)
  50. })
  51. it('runs the default command adapter without a shell and preserves command failures', async () => {
  52. execFileMock.mockImplementationOnce((_command, _args, _options, callback) => {
  53. callback(null, '', '')
  54. })
  55. await openNativePath('/tmp/default.txt', signal(), { platform: 'darwin' })
  56. const [command, args, options] = execFileMock.mock.calls[0]!
  57. expect(command).toBe('open')
  58. expect(args).toEqual(['/tmp/default.txt'])
  59. expect(options.encoding).toBe('utf8')
  60. expect(options.windowsHide).toBe(true)
  61. expect(options.signal).toBeInstanceOf(AbortSignal)
  62. const commandError = Object.assign(new Error('open failed'), { code: 1 })
  63. execFileMock.mockImplementationOnce((_command, _args, _options, callback) => {
  64. callback(commandError, 'partial output', 'failure details')
  65. })
  66. await expect(openNativePath('/tmp/missing.txt', signal(), { platform: 'darwin' })).rejects.toMatchObject({
  67. message: 'open failed', cause: commandError, code: 1,
  68. stdout: 'partial output', stderr: 'failure details',
  69. })
  70. })
  71. })