Files
orca/src/shared/native-file-drop.test.ts
T

278 lines
8.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
NATIVE_FILE_DROP_MAX_PATH_BYTES,
NATIVE_FILE_DROP_MAX_PATHS,
NATIVE_FILE_DROP_TARGET,
ORCA_INTERNAL_FILE_DRAG_TYPE,
createNativeFileDropPayload,
createRejectedNativeFileDropPayload,
hasNativeFileDragTypes,
isNativeFileDropPayload,
validateNativeFileDropPaths,
resolveNativeFileDropPath
} from './native-file-drop'
describe('hasNativeFileDragTypes', () => {
it('accepts native OS file drags', () => {
expect(hasNativeFileDragTypes(['Files'])).toBe(true)
})
it('rejects internal Orca file moves and URL/text drags', () => {
expect(hasNativeFileDragTypes(['Files', ORCA_INTERNAL_FILE_DRAG_TYPE])).toBe(false)
expect(hasNativeFileDragTypes(['text/uri-list'])).toBe(false)
expect(hasNativeFileDragTypes(['text/plain'])).toBe(false)
})
})
describe('resolveNativeFileDropPath', () => {
it('routes drops on the left sidebar to the add-project surface', () => {
expect(
resolveNativeFileDropPath([{ nativeFileDropTarget: NATIVE_FILE_DROP_TARGET.projectSidebar }])
).toEqual({ target: NATIVE_FILE_DROP_TARGET.projectSidebar })
})
it('preserves terminal tab and pane routing for native file drops', () => {
expect(
resolveNativeFileDropPath([
{
terminalPaneLeafId: 'leaf-1'
},
{
nativeFileDropTarget: NATIVE_FILE_DROP_TARGET.terminal,
terminalTabId: 'tab-1'
}
])
).toEqual({
target: NATIVE_FILE_DROP_TARGET.terminal,
tabId: 'tab-1',
paneLeafId: 'leaf-1'
})
})
it('uses the nearest file-explorer destination and fails closed without one', () => {
expect(
resolveNativeFileDropPath([
{ nativeFileDropDir: '/repo/src' },
{
nativeFileDropTarget: NATIVE_FILE_DROP_TARGET.fileExplorer,
nativeFileDropDir: '/repo'
}
])
).toEqual({
target: NATIVE_FILE_DROP_TARGET.fileExplorer,
destinationDir: '/repo/src'
})
expect(
resolveNativeFileDropPath([{ nativeFileDropTarget: NATIVE_FILE_DROP_TARGET.fileExplorer }])
).toEqual({ target: 'rejected' })
})
})
describe('validateNativeFileDropPaths', () => {
it('rejects native drops by file count before path byte accounting is needed', () => {
const paths = Array.from({ length: NATIVE_FILE_DROP_MAX_PATHS + 1 }, (_value, index) =>
['/tmp/file-', String(index)].join('')
)
expect(validateNativeFileDropPaths(paths)).toEqual({
byteLength: 0,
pathCount: NATIVE_FILE_DROP_MAX_PATHS + 1,
reason: 'too-many-paths',
status: 'rejected'
})
})
it('rejects native drops whose path list is too large without exposing paths', () => {
const validation = validateNativeFileDropPaths(['C:\\Users\\alice\\secret-token.txt'], {
maxPathBytes: 4
})
expect(validation).toEqual({
byteLength: 5,
pathCount: 1,
reason: 'paths-too-large',
status: 'rejected'
})
if (validation.status === 'rejected') {
const payload = createRejectedNativeFileDropPayload(validation)
expect(JSON.stringify(payload)).not.toContain('secret')
expect(JSON.stringify(payload)).not.toContain('alice')
}
})
it('accepts path payloads within the configured limits', () => {
expect(validateNativeFileDropPaths(['/tmp/a', '/tmp/b'])).toEqual({
byteLength: 12,
pathCount: 2,
status: 'accepted'
})
})
it('rejects multibyte native path lists with bounded byte accounting', () => {
expect(validateNativeFileDropPaths(['😀'.repeat(3)], { maxPathBytes: 5 })).toEqual({
byteLength: 8,
pathCount: 1,
reason: 'paths-too-large',
status: 'rejected'
})
})
})
describe('createNativeFileDropPayload', () => {
it('preserves terminal tab and pane routing in accepted payloads', () => {
expect(
createNativeFileDropPayload(
{ target: NATIVE_FILE_DROP_TARGET.terminal, tabId: 'tab-1', paneLeafId: 'leaf-1' },
['/tmp/a']
)
).toEqual({
paneLeafId: 'leaf-1',
paths: ['/tmp/a'],
tabId: 'tab-1',
target: NATIVE_FILE_DROP_TARGET.terminal
})
})
it('preserves file explorer destination routing in accepted payloads', () => {
expect(
createNativeFileDropPayload(
{ target: NATIVE_FILE_DROP_TARGET.fileExplorer, destinationDir: '/repo/src' },
['/tmp/a']
)
).toEqual({
destinationDir: '/repo/src',
paths: ['/tmp/a'],
target: NATIVE_FILE_DROP_TARGET.fileExplorer
})
})
it('falls back to editor for unmarked drops and fails closed for rejected targets', () => {
expect(createNativeFileDropPayload(null, ['/tmp/a'])).toEqual({
paths: ['/tmp/a'],
target: NATIVE_FILE_DROP_TARGET.editor
})
expect(createNativeFileDropPayload({ target: 'rejected' }, ['/tmp/a'])).toBeNull()
})
it('returns metadata-only rejected payloads for oversized path lists', () => {
const payload = createNativeFileDropPayload(null, [
'C:\\Users\\alice\\',
'a'.repeat(NATIVE_FILE_DROP_MAX_PATH_BYTES)
])
expect(payload).toEqual({
byteLength: NATIVE_FILE_DROP_MAX_PATH_BYTES + 1,
pathCount: 2,
reason: 'paths-too-large',
target: 'rejected'
})
expect(JSON.stringify(payload)).not.toContain('alice')
})
})
describe('isNativeFileDropPayload', () => {
it('accepts bounded native file-drop payload shapes', () => {
expect(
isNativeFileDropPayload({
paths: ['/tmp/a'],
target: NATIVE_FILE_DROP_TARGET.editor
})
).toBe(true)
expect(
isNativeFileDropPayload({
destinationDir: '/repo/src',
paths: ['/tmp/a'],
target: NATIVE_FILE_DROP_TARGET.fileExplorer
})
).toBe(true)
expect(
isNativeFileDropPayload({
paneLeafId: 'leaf-1',
paths: ['/tmp/a'],
tabId: 'tab-1',
target: NATIVE_FILE_DROP_TARGET.terminal
})
).toBe(true)
expect(
isNativeFileDropPayload({
byteLength: 0,
pathCount: NATIVE_FILE_DROP_MAX_PATHS + 1,
reason: 'too-many-paths',
target: 'rejected'
})
).toBe(true)
})
it('rejects malformed or unbounded native file-drop payloads', () => {
expect(isNativeFileDropPayload(null)).toBe(false)
expect(isNativeFileDropPayload({ paths: ['/tmp/a'], target: 'browser' })).toBe(false)
expect(
isNativeFileDropPayload({
paths: ['/tmp/a'],
target: NATIVE_FILE_DROP_TARGET.fileExplorer
})
).toBe(false)
expect(
isNativeFileDropPayload({
paths: ['/tmp/a'],
tabId: 42,
target: NATIVE_FILE_DROP_TARGET.terminal
})
).toBe(false)
expect(
isNativeFileDropPayload({
paths: Array.from({ length: NATIVE_FILE_DROP_MAX_PATHS + 1 }, () => '/tmp/a'),
target: NATIVE_FILE_DROP_TARGET.editor
})
).toBe(false)
expect(
isNativeFileDropPayload({
paths: ['a'.repeat(NATIVE_FILE_DROP_MAX_PATH_BYTES + 1)],
target: NATIVE_FILE_DROP_TARGET.editor
})
).toBe(false)
expect(
isNativeFileDropPayload({
byteLength: 0,
pathCount: 1,
reason: 'contains-secret-path',
target: 'rejected'
})
).toBe(false)
})
it('enforces native file-drop count and byte limits at their boundaries', () => {
expect(
isNativeFileDropPayload({
paths: Array.from(
{ length: NATIVE_FILE_DROP_MAX_PATHS },
(_value, index) => `/tmp/${index}`
),
target: NATIVE_FILE_DROP_TARGET.editor
})
).toBe(true)
expect(
isNativeFileDropPayload({
paths: Array.from(
{ length: NATIVE_FILE_DROP_MAX_PATHS + 1 },
(_value, index) => `/tmp/${index}`
),
target: NATIVE_FILE_DROP_TARGET.editor
})
).toBe(false)
expect(
isNativeFileDropPayload({
paths: ['a'.repeat(NATIVE_FILE_DROP_MAX_PATH_BYTES)],
target: NATIVE_FILE_DROP_TARGET.editor
})
).toBe(true)
expect(
isNativeFileDropPayload({
paths: ['a'.repeat(NATIVE_FILE_DROP_MAX_PATH_BYTES + 1)],
target: NATIVE_FILE_DROP_TARGET.editor
})
).toBe(false)
})
})