orca/tests/e2e/terminal-paste-ownership.sp...

411 lines
16 KiB
TypeScript

import { randomUUID } from 'node:crypto'
import { rmSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import type { ElectronApplication, Page } from '@stablyai/playwright-test'
import { test, expect } from './helpers/orca-app'
import {
focusActiveTerminalInput,
sendToTerminal,
waitForActivePanePtyId,
waitForActiveTerminalManager,
waitForTerminalOutput
} from './helpers/terminal'
import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store'
import {
clearTerminalPtyWriteLog,
installTerminalPtyWriteSpy,
readTerminalPtyWrites
} from './helpers/terminal-pty-write-spy'
function keyboardPasteChords(): string[] {
return process.platform === 'darwin'
? ['Meta+V']
: ['Control+V', 'Control+Shift+V', 'Shift+Insert']
}
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('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('PASTE_CHUNK_${runId}_' + seq + ':' + encoded + '\\n')
})
`
}
function pasteCollectScript(runId: string, sentinel: string, expectedText: string): string {
return `
process.stdin.setEncoding('utf8')
if (process.stdin.isTTY) process.stdin.setRawMode(true)
process.stdin.resume()
let received = ''
const interrupt = String.fromCharCode(3)
const bracketStart = String.fromCharCode(27) + '[200~'
const bracketEnd = String.fromCharCode(27) + '[201~'
const expectedText = ${JSON.stringify(expectedText)}
process.stdout.write('PASTE_READY_${runId}\\n')
process.stdin.on('data', (chunk) => {
if (chunk.includes(interrupt)) {
process.exit(0)
}
received += chunk
if (!received.includes(${JSON.stringify(sentinel)})) {
return
}
const normalized = received.split(bracketStart).join('').split(bracketEnd).join('')
const status = normalized === expectedText
? 'MATCH'
: 'MISMATCH:' + normalized.length + ':' + expectedText.length
process.stdout.write('PASTE_COMPLETE_${runId}:' + status + '\\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
}
async function rightClickActiveTerminalSurface(page: Page): Promise<void> {
const point = await page.evaluate(() => {
const state = window.__store?.getState()
const worktreeId = state?.activeWorktreeId
const tabId =
state?.activeTabType === 'terminal'
? state.activeTabId
: worktreeId
? (state?.activeTabIdByWorktree?.[worktreeId] ?? null)
: null
const manager = tabId ? window.__paneManagers?.get(tabId) : null
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
const surface =
pane?.container.querySelector<HTMLElement>('.xterm-screen') ??
pane?.container.querySelector<HTMLElement>('.xterm') ??
pane?.container
if (!pane || !surface) {
throw new Error('No active terminal surface to right-click')
}
pane.terminal.clearSelection()
const rect = surface.getBoundingClientRect()
if (rect.width <= 0 || rect.height <= 0) {
throw new Error('Active terminal surface is not measurable')
}
return {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
}
})
await page.mouse.click(point.x, point.y, { button: 'right' })
}
async function installClipboardReadTerminalBlurRepro(app: ElectronApplication): Promise<void> {
await app.evaluate(({ BrowserWindow, ipcMain }) => {
type ClipboardReadHandler = (event: unknown, ...args: unknown[]) => Promise<unknown> | unknown
const global = globalThis as unknown as {
__orcaOriginalClipboardReadTextHandler?: ClipboardReadHandler
}
const invokeHandlers = (
ipcMain as unknown as {
_invokeHandlers?: Map<string, ClipboardReadHandler>
}
)._invokeHandlers
const handler = invokeHandlers?.get('clipboard:readText')
if (!invokeHandlers || !handler || global.__orcaOriginalClipboardReadTextHandler) {
return
}
global.__orcaOriginalClipboardReadTextHandler = handler
invokeHandlers.set('clipboard:readText', async (event, ...args) => {
const windows = BrowserWindow.getAllWindows().filter((window) => !window.isDestroyed())
await Promise.all(
windows.map((window) =>
window.webContents
.executeJavaScript(
`
(document.activeElement && document.activeElement.blur && document.activeElement.blur());
document.body.tabIndex = -1;
document.body.focus();
`
)
.catch(() => undefined)
)
)
// Why: reproduce the focus churn window before the async clipboard read resolves.
await new Promise((resolve) => setTimeout(resolve, 0))
return global.__orcaOriginalClipboardReadTextHandler!(event, ...args)
})
})
}
async function restoreClipboardReadTerminalBlurRepro(app: ElectronApplication): Promise<void> {
await app.evaluate(({ ipcMain }) => {
type ClipboardReadHandler = (event: unknown, ...args: unknown[]) => Promise<unknown> | unknown
const global = globalThis as unknown as {
__orcaOriginalClipboardReadTextHandler?: ClipboardReadHandler
}
const invokeHandlers = (
ipcMain as unknown as {
_invokeHandlers?: Map<string, ClipboardReadHandler>
}
)._invokeHandlers
if (invokeHandlers && global.__orcaOriginalClipboardReadTextHandler) {
invokeHandlers.set('clipboard:readText', global.__orcaOriginalClipboardReadTextHandler)
delete global.__orcaOriginalClipboardReadTextHandler
}
})
}
async function openTerminalContextMenu(page: Page): Promise<void> {
const isWindows = await page.evaluate(() => navigator.userAgent.includes('Windows'))
const isMac = await page.evaluate(() => navigator.userAgent.includes('Mac'))
const modifiers: ('Alt' | 'Control' | 'Meta' | 'Shift')[] = isMac || isWindows ? ['Control'] : []
await page
.locator('.xterm:visible')
.first()
.click({
button: isMac ? 'left' : 'right',
position: { x: 40, y: 40 },
modifiers
})
await expect(page.getByRole('menuitem', { name: /Paste/ })).toBeVisible()
}
test.describe('terminal paste ownership', () => {
test('keyboard paste shortcuts send clipboard text to the focused terminal exactly once', async ({
electronApp,
orcaPage,
testRepoPath
}) => {
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
await ensureTerminalVisible(orcaPage)
await waitForActiveTerminalManager(orcaPage, 30_000)
await installTerminalPtyWriteSpy(electronApp)
const ptyId = await waitForActivePanePtyId(orcaPage)
const runId = randomUUID()
const scriptPath = path.join(testRepoPath, `.orca-paste-ownership-${runId}.mjs`)
writeFileSync(scriptPath, pasteEchoScript(runId))
let scriptStarted = false
try {
await sendToTerminal(orcaPage, ptyId, `node ${JSON.stringify(scriptPath)}\r`)
scriptStarted = true
await waitForTerminalOutput(orcaPage, `PASTE_READY_${runId}`, 10_000)
for (const [index, chord] of keyboardPasteChords().entries()) {
const payload = `ORCA_E2E_PASTE_${runId}_${index}`
const encodedPayload = Buffer.from(payload, 'utf8').toString('base64')
await clearTerminalPtyWriteLog(electronApp)
await orcaPage.evaluate((text) => window.api.ui.writeClipboardText(text), payload)
await focusActiveTerminalInput(orcaPage)
await orcaPage.keyboard.press(chord)
await waitForTerminalOutput(orcaPage, encodedPayload, 10_000, 12_000)
const writes = (await readTerminalPtyWrites(electronApp)).join('')
expect(countOccurrences(writes, payload), `${chord} PTY write count`).toBe(1)
}
} finally {
if (scriptStarted) {
await sendToTerminal(orcaPage, ptyId, '\x03').catch(() => undefined)
}
rmSync(scriptPath, { force: true })
}
})
test('keyboard paste survives transient terminal blur during clipboard read', async ({
electronApp,
orcaPage,
testRepoPath
}) => {
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
await ensureTerminalVisible(orcaPage)
await waitForActiveTerminalManager(orcaPage, 30_000)
await installTerminalPtyWriteSpy(electronApp)
const ptyId = await waitForActivePanePtyId(orcaPage)
const runId = randomUUID()
const scriptPath = path.join(testRepoPath, `.orca-paste-blur-${runId}.mjs`)
writeFileSync(scriptPath, pasteEchoScript(runId))
let scriptStarted = false
try {
await sendToTerminal(orcaPage, ptyId, `node ${JSON.stringify(scriptPath)}\r`)
scriptStarted = true
await waitForTerminalOutput(orcaPage, `PASTE_READY_${runId}`, 10_000)
const payload = `ORCA_E2E_TRANSIENT_BLUR_PASTE_${runId}`
const encodedPayload = Buffer.from(payload, 'utf8').toString('base64')
await clearTerminalPtyWriteLog(electronApp)
await orcaPage.evaluate((text) => window.api.ui.writeClipboardText(text), payload)
await focusActiveTerminalInput(orcaPage)
await installClipboardReadTerminalBlurRepro(electronApp)
await orcaPage.keyboard.press(keyboardPasteChords()[0])
await waitForTerminalOutput(orcaPage, encodedPayload, 10_000, 12_000)
const writes = (await readTerminalPtyWrites(electronApp)).join('')
expect(countOccurrences(writes, payload), 'transient blur PTY write count').toBe(1)
} finally {
await restoreClipboardReadTerminalBlurRepro(electronApp).catch(() => undefined)
if (scriptStarted) {
await sendToTerminal(orcaPage, ptyId, '\x03').catch(() => undefined)
}
rmSync(scriptPath, { force: true })
}
})
test('terminal context-menu Paste sends clipboard text exactly once', async ({
electronApp,
orcaPage,
testRepoPath
}) => {
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
await ensureTerminalVisible(orcaPage)
await waitForActiveTerminalManager(orcaPage, 30_000)
await installTerminalPtyWriteSpy(electronApp)
const ptyId = await waitForActivePanePtyId(orcaPage)
const runId = randomUUID()
const scriptPath = path.join(testRepoPath, `.orca-paste-context-menu-${runId}.mjs`)
writeFileSync(scriptPath, pasteEchoScript(runId))
let scriptStarted = false
try {
await sendToTerminal(orcaPage, ptyId, `node ${JSON.stringify(scriptPath)}\r`)
scriptStarted = true
await waitForTerminalOutput(orcaPage, `PASTE_READY_${runId}`, 10_000)
const payload = `ORCA_E2E_CONTEXT_MENU_PASTE_${runId}`
const encodedPayload = Buffer.from(payload, 'utf8').toString('base64')
await clearTerminalPtyWriteLog(electronApp)
await orcaPage.evaluate((text) => window.api.ui.writeClipboardText(text), payload)
await focusActiveTerminalInput(orcaPage)
await openTerminalContextMenu(orcaPage)
await orcaPage.getByRole('menuitem', { name: /Paste/ }).click()
await waitForTerminalOutput(orcaPage, encodedPayload, 10_000, 12_000)
const writes = (await readTerminalPtyWrites(electronApp)).join('')
expect(countOccurrences(writes, payload), 'terminal context-menu PTY write count').toBe(1)
} finally {
if (scriptStarted) {
await sendToTerminal(orcaPage, ptyId, '\x03').catch(() => undefined)
}
rmSync(scriptPath, { force: true })
}
})
test('Windows multiline keyboard paste preserves terminal content with one PTY owner', async ({
electronApp,
orcaPage,
testRepoPath
}) => {
test.skip(process.platform !== 'win32', 'Windows multiline paste behavior is Windows-only')
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
await ensureTerminalVisible(orcaPage)
await waitForActiveTerminalManager(orcaPage, 30_000)
await installTerminalPtyWriteSpy(electronApp)
const ptyId = await waitForActivePanePtyId(orcaPage)
const runId = randomUUID()
const sentinel = `ORCA_E2E_MULTILINE_DONE_${runId}`
const payload = [
`ORCA_E2E_MULTILINE_${runId}`,
'line with spaces and tabs\tend',
'PowerShell metacharacters: ` $ " \' ; | & < > @ { } ( )',
'cmd metacharacters: % ! ^ & | < >',
'Unicode: caf\u00e9 \u4f60\u597d \u0645\u0631\u062d\u0628\u0627 \ud83d\ude00',
`mixed-newline-before\r\nlf-line\ncrlf-line\r\n${sentinel}`
].join('\n')
const scriptPath = path.join(testRepoPath, `.orca-paste-multiline-${runId}.mjs`)
writeFileSync(scriptPath, pasteCollectScript(runId, sentinel, payload))
let scriptStarted = false
try {
await sendToTerminal(orcaPage, ptyId, `node ${JSON.stringify(scriptPath)}\r`)
scriptStarted = true
await waitForTerminalOutput(orcaPage, `PASTE_READY_${runId}`, 10_000)
await clearTerminalPtyWriteLog(electronApp)
await orcaPage.evaluate((text) => window.api.ui.writeClipboardText(text), payload)
await focusActiveTerminalInput(orcaPage)
await orcaPage.keyboard.press('Control+V')
await waitForTerminalOutput(orcaPage, `PASTE_COMPLETE_${runId}:MATCH`, 10_000, 12_000)
const writes = (await readTerminalPtyWrites(electronApp)).join('')
expect(countOccurrences(writes, payload), 'multiline payload PTY write count').toBe(1)
} finally {
if (scriptStarted) {
await sendToTerminal(orcaPage, ptyId, '\x03').catch(() => undefined)
}
rmSync(scriptPath, { force: true })
}
})
test('Windows right-click paste sends clipboard text to the focused terminal exactly once', async ({
electronApp,
orcaPage,
testRepoPath
}) => {
test.skip(process.platform !== 'win32', 'Windows right-click paste is Windows-only')
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
await ensureTerminalVisible(orcaPage)
await orcaPage.evaluate(async () => {
await window.__store?.getState().updateSettings({ terminalRightClickToPaste: true })
})
await waitForActiveTerminalManager(orcaPage, 30_000)
await installTerminalPtyWriteSpy(electronApp)
const ptyId = await waitForActivePanePtyId(orcaPage)
const runId = randomUUID()
const scriptPath = path.join(testRepoPath, `.orca-paste-right-click-${runId}.mjs`)
writeFileSync(scriptPath, pasteEchoScript(runId))
let scriptStarted = false
try {
await sendToTerminal(orcaPage, ptyId, `node ${JSON.stringify(scriptPath)}\r`)
scriptStarted = true
await waitForTerminalOutput(orcaPage, `PASTE_READY_${runId}`, 10_000)
const payload = `ORCA_E2E_RIGHT_CLICK_PASTE_${runId}`
const encodedPayload = Buffer.from(payload, 'utf8').toString('base64')
await clearTerminalPtyWriteLog(electronApp)
await orcaPage.evaluate((text) => window.api.ui.writeClipboardText(text), payload)
await focusActiveTerminalInput(orcaPage)
await rightClickActiveTerminalSurface(orcaPage)
await waitForTerminalOutput(orcaPage, encodedPayload, 10_000, 12_000)
const writes = (await readTerminalPtyWrites(electronApp)).join('')
expect(countOccurrences(writes, payload), 'right-click PTY write count').toBe(1)
} finally {
if (scriptStarted) {
await sendToTerminal(orcaPage, ptyId, '\x03').catch(() => undefined)
}
rmSync(scriptPath, { force: true })
}
})
})