test: parametrize dragDropPaths cases with test.each

Groups the 21 scenarios into four table-driven describes
(empty-result, single-path, multi-path, backslash-escaped) so that
adding a new case is a one-line row instead of a new `test()` block.
Fixture directories are now created synchronously at describe-load
time so their paths are available to the test.each tables, which are
built before any hook runs.
This commit is contained in:
Paulo Reis
2026-04-05 08:24:02 -03:00
parent f25ea81f86
commit c2e32b6f37

View File

@@ -1,145 +1,100 @@
import { afterAll, beforeAll, describe, expect, test } from 'bun:test' import { afterAll, describe, expect, test } from 'bun:test'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs' import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'
import { tmpdir } from 'os' import { tmpdir } from 'os'
import { join } from 'path' import { join } from 'path'
import { extractDraggedFilePaths } from './dragDropPaths.js' import { extractDraggedFilePaths } from './dragDropPaths.js'
describe('extractDraggedFilePaths', () => { describe('extractDraggedFilePaths', () => {
// Use paths that actually exist on any system // Paths that exist on any system.
const thisFile = import.meta.path const thisFile = import.meta.path
const packageJson = `${process.cwd()}/package.json` const packageJson = `${process.cwd()}/package.json`
// Temp dir with a file whose name contains a space, for Finder-drag // Fixtures created synchronously at describe-load time (not in
// backslash-escape tests, and a scoped-package-style subdir so we can // `beforeAll`) so their paths are available to `test.each` tables,
// exercise paths that embed `@` (e.g. `node_modules/@types/...`). // which are built before any hook runs.
let tmpDir: string const tmpDir = mkdtempSync(join(tmpdir(), 'dragdrop-test-'))
let spacedFile: string const spacedFile = join(tmpDir, 'my file.txt')
let atSignFile: string
beforeAll(() => {
tmpDir = mkdtempSync(join(tmpdir(), 'dragdrop-test-'))
spacedFile = join(tmpDir, 'my file.txt')
writeFileSync(spacedFile, 'test') writeFileSync(spacedFile, 'test')
const scopedDir = join(tmpDir, '@types') const scopedDir = join(tmpDir, '@types')
mkdirSync(scopedDir) mkdirSync(scopedDir)
atSignFile = join(scopedDir, 'index.d.ts') const atSignFile = join(scopedDir, 'index.d.ts')
writeFileSync(atSignFile, 'test') writeFileSync(atSignFile, 'test')
})
afterAll(() => { afterAll(() => {
rmSync(tmpDir, { recursive: true, force: true }) rmSync(tmpDir, { recursive: true, force: true })
}) })
test('detects a single absolute file path', () => { describe('returns an empty array', () => {
const result = extractDraggedFilePaths(thisFile) const emptyCases: Array<[string, string]> = [
expect(result).toEqual([thisFile]) ['a non-absolute path', 'relative/path/file.ts'],
['a plain image path', '/Users/foo/image.png'],
['an uppercase image extension', '/Users/foo/SHOT.PNG'],
['a double-quoted image path', '"/Users/foo/shot.png"'],
['a single-quoted image path', "'/Users/foo/shot.jpg'"],
['regular prose text', 'hello world this is text'],
['a nonexistent absolute path', '/definitely/nonexistent/file.ts'],
['a single-quoted nonexistent path', "'/definitely/nonexistent.ts'"],
['an empty string', ''],
['whitespace only', ' \n '],
// Mixed-segment cases: all-or-nothing policy means a single bad
// entry disqualifies the whole paste.
['a mix where one path does not exist', `${thisFile}\n/nonexistent/file.ts`],
['a mix where one segment is an image', `${thisFile}\n/Users/foo/shot.png`],
]
test.each(emptyCases)('for %s', (_label, input) => {
expect(extractDraggedFilePaths(input)).toEqual([])
})
}) })
test('detects newline-separated file paths', () => { describe('resolves a single path', () => {
const result = extractDraggedFilePaths(`${thisFile}\n${packageJson}`) const singleCases: Array<[string, string, string]> = [
expect(result).toEqual([thisFile, packageJson]) ['a plain absolute path', thisFile, thisFile],
['a double-quoted path', `"${thisFile}"`, thisFile],
['a single-quoted path', `'${thisFile}'`, thisFile],
['a path with leading/trailing whitespace', ` ${thisFile} `, thisFile],
// Realistic: dragging something under `node_modules/@types/...`.
// `@` inside the path must not collide with the mention prefix
// that the caller prepends downstream.
['a path containing an `@` segment', atSignFile, atSignFile],
]
test.each(singleCases)('from %s', (_label, input, expected) => {
expect(extractDraggedFilePaths(input)).toEqual([expected])
})
}) })
test('detects space-separated absolute paths (Finder drag)', () => { describe('resolves multiple paths', () => {
const result = extractDraggedFilePaths(`${thisFile} ${packageJson}`) const multiCases: Array<[string, string, string[]]> = [
expect(result).toEqual([thisFile, packageJson]) [
}) 'newline-separated',
`${thisFile}\n${packageJson}`,
test('returns empty for non-absolute paths', () => { [thisFile, packageJson],
expect(extractDraggedFilePaths('relative/path/file.ts')).toEqual([]) ],
}) [
'space-separated (Finder drag)',
test('returns empty for image file paths', () => { `${thisFile} ${packageJson}`,
expect(extractDraggedFilePaths('/Users/foo/image.png')).toEqual([]) [thisFile, packageJson],
}) ],
]
test('returns empty for regular text', () => { test.each(multiCases)('when input is %s', (_label, input, expected) => {
expect(extractDraggedFilePaths('hello world this is text')).toEqual([]) expect(extractDraggedFilePaths(input)).toEqual(expected)
}) })
test('returns empty when file does not exist', () => {
expect(
extractDraggedFilePaths('/definitely/nonexistent/file.ts'),
).toEqual([])
})
test('returns empty for empty string', () => {
expect(extractDraggedFilePaths('')).toEqual([])
})
test('returns empty for whitespace only', () => {
expect(extractDraggedFilePaths(' \n ')).toEqual([])
})
test('returns empty if any path does not exist', () => {
expect(
extractDraggedFilePaths(`${thisFile}\n/nonexistent/file.ts`),
).toEqual([])
})
test('strips outer double quotes from paths', () => {
const result = extractDraggedFilePaths(`"${thisFile}"`)
expect(result).toEqual([thisFile])
})
test('strips outer single quotes from paths', () => {
const result = extractDraggedFilePaths(`'${thisFile}'`)
expect(result).toEqual([thisFile])
})
test('returns empty for a double-quoted image path', () => {
// Regression guard: image detection must see through outer quotes so
// quoted image drops still route to the image paste handler.
expect(extractDraggedFilePaths('"/Users/foo/shot.png"')).toEqual([])
})
test('returns empty for a single-quoted image path', () => {
expect(extractDraggedFilePaths("'/Users/foo/shot.jpg'")).toEqual([])
})
test('returns empty for an uppercase image extension', () => {
expect(extractDraggedFilePaths('/Users/foo/SHOT.PNG')).toEqual([])
}) })
// Backslash-escaped paths are a Finder/macOS + Linux convention — on
// Windows the shell-escape step is skipped, so these cases do not apply.
if (process.platform !== 'win32') { if (process.platform !== 'win32') {
test('returns empty for a backslash-escaped image path', () => { describe('handles backslash-escaped paths', () => {
// Finder drags escape spaces with backslashes; the image check must test('returns empty for an escaped image path', () => {
// apply after escape stripping. // The image check must apply after escape stripping so Finder
expect( // image drags still route to the image paste handler.
extractDraggedFilePaths('/Users/foo/my\\ shot.png'), expect(extractDraggedFilePaths('/Users/foo/my\\ shot.png')).toEqual([])
).toEqual([])
}) })
test('resolves a backslash-escaped path to a real file on disk', () => { test('resolves an escaped real file with a space in its name', () => {
// `spacedFile` is an existing file with a space in its name; the // Raw form matches what a terminal delivers on Finder drag.
// raw form matches what a terminal delivers on Finder drag.
const escaped = spacedFile.replace(/ /g, '\\ ') const escaped = spacedFile.replace(/ /g, '\\ ')
expect(extractDraggedFilePaths(escaped)).toEqual([spacedFile]) expect(extractDraggedFilePaths(escaped)).toEqual([spacedFile])
}) })
})
} }
test('returns empty when mixed segments include an image file', () => {
// All-or-nothing: one image in the group disqualifies the whole paste
// so it can be handled by the image paste handler instead.
expect(
extractDraggedFilePaths(`${thisFile}\n/Users/foo/shot.png`),
).toEqual([])
})
test('returns empty for a single-quoted nonexistent path', () => {
// Quoted but nonexistent — exists check still runs after unquoting.
expect(extractDraggedFilePaths("'/definitely/nonexistent.ts'")).toEqual(
[],
)
})
test('trims surrounding whitespace from the whole paste', () => {
expect(extractDraggedFilePaths(` ${thisFile} `)).toEqual([thisFile])
})
test('resolves a path that embeds an `@` segment', () => {
// Realistic case: dragging something under `node_modules/@types/...`.
// The `@` inside the path must not be confused with the mention prefix
// that the caller prepends downstream — `extractDraggedFilePaths`
// returns raw paths and leaves mention formatting to PromptInput.
expect(extractDraggedFilePaths(atSignFile)).toEqual([atSignFile])
})
}) })