Finder-launched terminals now inherit a login PATH so tools like ollama resolve, destructive SFTP/editor actions require confirmation, and terminal search plus window dragging work again. SSH forwards and RDP sessions keep listeners and survive reconnect without FastPath crashes. Co-authored-by: Cursor <cursoragent@cursor.com>
178 lines
5.2 KiB
TypeScript
178 lines
5.2 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
clearSftpDragEntry,
|
|
hasSftpDragEntry,
|
|
isCrossPaneSftpDrag,
|
|
peekSftpDragEntry,
|
|
peekSftpDragEntries,
|
|
primeSftpDragEntries,
|
|
readSftpDragEntry,
|
|
readSftpDragEntries,
|
|
resolveSftpDragEntries,
|
|
scheduleClearSftpDragEntry,
|
|
SFTP_ENTRY_TEXT_PREFIX,
|
|
writeSftpDragEntry,
|
|
writeSftpDragEntries,
|
|
} from '../../../src/components/sftp/dragDrop'
|
|
|
|
function mockDataTransfer(initialTypes: string[] = []): DataTransfer {
|
|
const store = new Map<string, string>()
|
|
const types = [...initialTypes]
|
|
return {
|
|
effectAllowed: 'none',
|
|
dropEffect: 'none',
|
|
files: [] as unknown as FileList,
|
|
items: [] as unknown as DataTransferItemList,
|
|
types,
|
|
setData(format: string, data: string) {
|
|
store.set(format, data)
|
|
if (!types.includes(format)) types.push(format)
|
|
},
|
|
getData(format: string) {
|
|
return store.get(format) || ''
|
|
},
|
|
clearData() {
|
|
store.clear()
|
|
types.length = 0
|
|
},
|
|
setDragImage() {},
|
|
} as DataTransfer
|
|
}
|
|
|
|
describe('sftp dragDrop', () => {
|
|
afterEach(() => {
|
|
clearSftpDragEntry()
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
it('mirrors entry for dragover when getData is empty', () => {
|
|
const dt = mockDataTransfer()
|
|
writeSftpDragEntry(dt, {
|
|
source: 'local',
|
|
name: 'a.txt',
|
|
path: '/tmp/a.txt',
|
|
type: 'file',
|
|
size: 12,
|
|
})
|
|
|
|
// Simulate WebView dragover: types may be present but getData returns empty
|
|
const over = mockDataTransfer([...(dt.types as string[])])
|
|
expect(hasSftpDragEntry(over)).toBe(true)
|
|
expect(peekSftpDragEntry()?.name).toBe('a.txt')
|
|
expect(readSftpDragEntry(over)?.source).toBe('local')
|
|
|
|
clearSftpDragEntry()
|
|
expect(peekSftpDragEntry()).toBeNull()
|
|
})
|
|
|
|
it('writes text/plain prefix for WebKit-compatible drop payloads', () => {
|
|
const dt = mockDataTransfer()
|
|
writeSftpDragEntry(dt, {
|
|
source: 'remote',
|
|
name: 'b.txt',
|
|
path: '/home/u/b.txt',
|
|
type: 'file',
|
|
size: 3,
|
|
})
|
|
|
|
const plain = dt.getData('text/plain')
|
|
expect(plain.startsWith(SFTP_ENTRY_TEXT_PREFIX)).toBe(true)
|
|
|
|
clearSftpDragEntry()
|
|
// Drop path without mirror: parse text/plain
|
|
expect(readSftpDragEntry(dt)?.path).toBe('/home/u/b.txt')
|
|
})
|
|
|
|
it('only accepts cross-pane drops', () => {
|
|
const dt = mockDataTransfer()
|
|
writeSftpDragEntry(dt, {
|
|
source: 'local',
|
|
name: 'a.txt',
|
|
path: '/tmp/a.txt',
|
|
type: 'file',
|
|
size: 1,
|
|
})
|
|
|
|
expect(isCrossPaneSftpDrag(dt, false)).toBe(true) // remote pane
|
|
expect(isCrossPaneSftpDrag(dt, true)).toBe(false) // local pane
|
|
})
|
|
|
|
it('uses the pre-drag mirror when WebKit has not exposed DataTransfer data', () => {
|
|
primeSftpDragEntries([
|
|
{ source: 'remote', name: 'server.log', path: '/var/log/server.log', type: 'file', size: 9 },
|
|
])
|
|
|
|
const hiddenWebKitTransfer = mockDataTransfer()
|
|
expect(isCrossPaneSftpDrag(hiddenWebKitTransfer, true)).toBe(true)
|
|
expect(resolveSftpDragEntries(hiddenWebKitTransfer).entries[0]?.name).toBe('server.log')
|
|
})
|
|
|
|
it('keeps mirror readable until scheduled clear runs (drop/dragend race)', () => {
|
|
vi.useFakeTimers()
|
|
const dt = mockDataTransfer()
|
|
writeSftpDragEntry(dt, {
|
|
source: 'remote',
|
|
name: 'b.txt',
|
|
path: '/home/u/b.txt',
|
|
type: 'file',
|
|
size: 3,
|
|
})
|
|
|
|
scheduleClearSftpDragEntry(50)
|
|
expect(peekSftpDragEntry()?.name).toBe('b.txt')
|
|
expect(readSftpDragEntry(mockDataTransfer())?.path).toBe('/home/u/b.txt')
|
|
|
|
vi.advanceTimersByTime(50)
|
|
expect(peekSftpDragEntry()).toBeNull()
|
|
})
|
|
|
|
it('ignores stale clear timers after a new drag starts', () => {
|
|
vi.useFakeTimers()
|
|
const dt = mockDataTransfer()
|
|
writeSftpDragEntry(dt, {
|
|
source: 'local',
|
|
name: 'old.txt',
|
|
path: '/tmp/old.txt',
|
|
type: 'file',
|
|
size: 1,
|
|
})
|
|
scheduleClearSftpDragEntry(100)
|
|
|
|
writeSftpDragEntry(dt, {
|
|
source: 'remote',
|
|
name: 'new.txt',
|
|
path: '/r/new.txt',
|
|
type: 'file',
|
|
size: 2,
|
|
})
|
|
|
|
vi.advanceTimersByTime(100)
|
|
expect(peekSftpDragEntry()?.name).toBe('new.txt')
|
|
})
|
|
|
|
it('keeps multi-selection batch in the drag mirror', () => {
|
|
const dt = mockDataTransfer()
|
|
writeSftpDragEntries(dt, [
|
|
{ source: 'local', name: 'a.txt', path: '/tmp/a.txt', type: 'file', size: 1 },
|
|
{ source: 'local', name: 'b.txt', path: '/tmp/b.txt', type: 'file', size: 2 },
|
|
])
|
|
expect(peekSftpDragEntries().map((e) => e.name)).toEqual(['a.txt', 'b.txt'])
|
|
clearSftpDragEntry()
|
|
expect(readSftpDragEntries(dt).map((e) => e.name)).toEqual(['a.txt', 'b.txt'])
|
|
})
|
|
|
|
it('resolves drop payload via mirror or text/plain', () => {
|
|
const dt = mockDataTransfer()
|
|
writeSftpDragEntries(dt, [
|
|
{ source: 'local', name: 'a.txt', path: '/tmp/a.txt', type: 'file', size: 1 },
|
|
{ source: 'local', name: 'b.txt', path: '/tmp/b.txt', type: 'file', size: 2 },
|
|
])
|
|
expect(resolveSftpDragEntries(dt).via).toBe('mirror')
|
|
expect(resolveSftpDragEntries(dt).entries).toHaveLength(2)
|
|
|
|
clearSftpDragEntry()
|
|
expect(resolveSftpDragEntries(dt).via).toBe('text/plain')
|
|
expect(resolveSftpDragEntries(dt).entries.map((e) => e.name)).toEqual(['a.txt', 'b.txt'])
|
|
})
|
|
})
|