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() 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']) }) })