198 lines
7.0 KiB
TypeScript
198 lines
7.0 KiB
TypeScript
import { randomUUID } from 'node:crypto'
|
|
import { rmSync, writeFileSync } from 'node:fs'
|
|
import path from 'node:path'
|
|
import { test, expect } from './helpers/orca-app'
|
|
import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store'
|
|
import {
|
|
focusActiveTerminalInput,
|
|
focusLastTerminalPane,
|
|
sendToTerminal,
|
|
splitActiveTerminalPane,
|
|
waitForActiveTerminalManager,
|
|
waitForPaneCount,
|
|
waitForPaneIdentitySnapshot,
|
|
waitForTerminalOutput
|
|
} from './helpers/terminal'
|
|
import {
|
|
clearTerminalPtyWriteLog,
|
|
installTerminalPtyWriteSpy,
|
|
readTerminalPtyWriteEntries
|
|
} from './helpers/terminal-pty-write-spy'
|
|
|
|
function keyboardPasteChord(): string {
|
|
return process.platform === 'darwin' ? 'Meta+V' : 'Control+V'
|
|
}
|
|
|
|
function pasteEchoScript(runId: string): string {
|
|
return `
|
|
process.stdin.setEncoding('utf8')
|
|
if (process.stdin.isTTY) process.stdin.setRawMode(true)
|
|
process.stdin.resume()
|
|
let seq = 0
|
|
const interrupt = String.fromCharCode(3)
|
|
process.stdout.write('SPLIT_PASTE_READY_${runId}\\n')
|
|
process.stdin.on('data', (chunk) => {
|
|
if (chunk.includes(interrupt)) {
|
|
process.exit(0)
|
|
}
|
|
seq += 1
|
|
const encoded = Buffer.from(chunk, 'utf8').toString('base64')
|
|
process.stdout.write('SPLIT_PASTE_CHUNK_${runId}_' + seq + ':' + encoded + '\\n')
|
|
})
|
|
`
|
|
}
|
|
|
|
function countOccurrences(value: string, needle: string): number {
|
|
let count = 0
|
|
let index = value.indexOf(needle)
|
|
while (index !== -1) {
|
|
count += 1
|
|
index = value.indexOf(needle, index + needle.length)
|
|
}
|
|
return count
|
|
}
|
|
|
|
test.describe('split terminal pane paste ownership', () => {
|
|
test('keyboard paste writes only to the active split pane PTY', async ({
|
|
electronApp,
|
|
orcaPage,
|
|
testRepoPath
|
|
}) => {
|
|
await waitForSessionReady(orcaPage)
|
|
await waitForActiveWorktree(orcaPage)
|
|
await ensureTerminalVisible(orcaPage)
|
|
await waitForActiveTerminalManager(orcaPage, 30_000)
|
|
await installTerminalPtyWriteSpy(electronApp)
|
|
|
|
await splitActiveTerminalPane(orcaPage, 'vertical')
|
|
await waitForPaneCount(orcaPage, 2)
|
|
await focusLastTerminalPane(orcaPage)
|
|
|
|
const snapshot = await waitForPaneIdentitySnapshot(orcaPage, 2)
|
|
const activePane = snapshot.panes.find((pane) => pane.leafId === snapshot.activeLeafId)
|
|
const inactivePane = snapshot.panes.find((pane) => pane.leafId !== snapshot.activeLeafId)
|
|
if (!activePane?.ptyId || !inactivePane?.ptyId) {
|
|
throw new Error('Split terminal panes did not expose active and inactive PTY ids')
|
|
}
|
|
|
|
const runId = randomUUID()
|
|
const scriptPath = path.join(testRepoPath, `.orca-split-paste-${runId}.mjs`)
|
|
writeFileSync(scriptPath, pasteEchoScript(runId))
|
|
let scriptStarted = false
|
|
|
|
try {
|
|
await sendToTerminal(orcaPage, activePane.ptyId, `node ${JSON.stringify(scriptPath)}\r`)
|
|
scriptStarted = true
|
|
await waitForTerminalOutput(orcaPage, `SPLIT_PASTE_READY_${runId}`, 10_000)
|
|
|
|
const payload = `ORCA_E2E_SPLIT_PASTE_${runId}`
|
|
const encodedPayload = Buffer.from(payload, 'utf8').toString('base64')
|
|
await orcaPage.evaluate((text) => window.api.ui.writeClipboardText(text), payload)
|
|
await clearTerminalPtyWriteLog(electronApp)
|
|
await focusActiveTerminalInput(orcaPage)
|
|
|
|
await orcaPage.keyboard.press(keyboardPasteChord())
|
|
await waitForTerminalOutput(orcaPage, encodedPayload, 10_000, 12_000)
|
|
|
|
const writes = await readTerminalPtyWriteEntries(electronApp)
|
|
const activeWrites = writes
|
|
.filter((entry) => entry.id === activePane.ptyId)
|
|
.map((entry) => entry.data)
|
|
.join('')
|
|
const inactiveWrites = writes
|
|
.filter((entry) => entry.id === inactivePane.ptyId)
|
|
.map((entry) => entry.data)
|
|
.join('')
|
|
expect(countOccurrences(activeWrites, payload)).toBe(1)
|
|
expect(inactiveWrites).not.toContain(payload)
|
|
} finally {
|
|
if (scriptStarted) {
|
|
await sendToTerminal(orcaPage, activePane.ptyId, '\x03').catch(() => undefined)
|
|
}
|
|
rmSync(scriptPath, { force: true })
|
|
}
|
|
})
|
|
|
|
test('internal file drop writes only to the pane under the drop target', async ({
|
|
electronApp,
|
|
orcaPage,
|
|
testRepoPath
|
|
}) => {
|
|
await waitForSessionReady(orcaPage)
|
|
await waitForActiveWorktree(orcaPage)
|
|
await ensureTerminalVisible(orcaPage)
|
|
await waitForActiveTerminalManager(orcaPage, 30_000)
|
|
await installTerminalPtyWriteSpy(electronApp)
|
|
|
|
await splitActiveTerminalPane(orcaPage, 'vertical')
|
|
await waitForPaneCount(orcaPage, 2)
|
|
await focusLastTerminalPane(orcaPage)
|
|
|
|
const snapshot = await waitForPaneIdentitySnapshot(orcaPage, 2)
|
|
const activePane = snapshot.panes.find((pane) => pane.leafId === snapshot.activeLeafId)
|
|
const dropPane = snapshot.panes.find((pane) => pane.leafId !== snapshot.activeLeafId)
|
|
if (!activePane?.ptyId || !dropPane?.ptyId) {
|
|
throw new Error('Split terminal panes did not expose active and drop-target PTY ids')
|
|
}
|
|
|
|
const runId = randomUUID()
|
|
const dropPath = path.join(testRepoPath, `drop-target-${runId}.txt`)
|
|
const dropMarker = path.basename(dropPath)
|
|
|
|
await clearTerminalPtyWriteLog(electronApp)
|
|
await orcaPage.evaluate(
|
|
({ leafId, pathValue }) => {
|
|
const state = window.__store?.getState()
|
|
const tabId =
|
|
state?.activeTabType === 'terminal'
|
|
? state.activeTabId
|
|
: state?.activeWorktreeId
|
|
? (state.activeTabIdByWorktree?.[state.activeWorktreeId] ?? null)
|
|
: null
|
|
const manager = tabId ? window.__paneManagers?.get(tabId) : null
|
|
const pane = manager?.getPanes?.().find((candidate) => candidate.leafId === leafId)
|
|
if (!pane) {
|
|
throw new Error('Drop target pane not found')
|
|
}
|
|
const dataTransfer = new DataTransfer()
|
|
dataTransfer.setData('text/x-orca-file-path', pathValue)
|
|
const target = pane.container.querySelector('.xterm-screen, textarea') ?? pane.container
|
|
for (const eventType of ['dragenter', 'dragover', 'drop']) {
|
|
target.dispatchEvent(
|
|
new DragEvent(eventType, {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
dataTransfer
|
|
})
|
|
)
|
|
}
|
|
},
|
|
{ leafId: dropPane.leafId, pathValue: dropPath }
|
|
)
|
|
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const writes = await readTerminalPtyWriteEntries(electronApp)
|
|
return writes.some(
|
|
(entry) => entry.id === dropPane.ptyId && entry.data.includes(dropMarker)
|
|
)
|
|
},
|
|
{ timeout: 10_000, message: 'Internal file drop did not write to the drop-target PTY' }
|
|
)
|
|
.toBe(true)
|
|
|
|
const writes = await readTerminalPtyWriteEntries(electronApp)
|
|
const activeWrites = writes
|
|
.filter((entry) => entry.id === activePane.ptyId)
|
|
.map((entry) => entry.data)
|
|
.join('')
|
|
const dropWrites = writes
|
|
.filter((entry) => entry.id === dropPane.ptyId)
|
|
.map((entry) => entry.data)
|
|
.join('')
|
|
expect(dropWrites).toContain(dropMarker)
|
|
expect(activeWrites).not.toContain(dropMarker)
|
|
})
|
|
})
|