Files
orca/tests/e2e/golden-terminal-file-link.spec.ts
T
Jinjing b8d6b21dfa test(e2e): add golden E2E tests for workspace session management (#14304)
* test(e2e): add golden E2E tests for workspace session management

- Restore exact file and terminal state after quit/relaunch
- Verify terminal file link activation and external edit detection
- Test worktree creation and switching with isolated terminals
- Isolate test repo paths between concurrent CI runs with UUIDs

* Add platform-aware marker echo command utility

- Create splitMarkerEchoCommand() to generate shell commands that
  safely echo test markers across Windows and Unix platforms
- Split markers into prefix/suffix fragments so output assertions
  prove execution, not just shell echo-back
- Consolidate SORTABLE_TAB export and improve tab bar locator logic
- Refactor terminal link helpers to extract client point calculation
2026-08-13 10:26:20 -07:00

189 lines
7.2 KiB
TypeScript

import { readFileSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import type { Page } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
import { openFileExplorer } from './helpers/file-explorer'
import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store'
import {
getTerminalContent,
sendToTerminal,
waitForActivePanePtyId,
waitForActiveTerminalManager
} from './helpers/terminal'
import { nodeTerminalCommand } from './terminal-node-command'
import { waitForPtyShellEcho } from './terminal-pty-readiness'
type LinkProbe = { col: number; row: number; tabId: string }
type LinkClientPoint = { x: number; y: number }
const LINK_SCAN_CHAR_LIMIT = 12_000
async function locateLink(page: Page, needle: string): Promise<LinkProbe | null> {
return page.evaluate((needle) => {
const state = window.__store?.getState()
const worktreeId = state?.activeWorktreeId
const tabId =
state?.activeTabType === 'terminal'
? state.activeTabId
: worktreeId
? state?.activeTabIdByWorktree?.[worktreeId]
: null
const manager = tabId ? window.__paneManagers?.get(tabId) : null
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
if (!tabId || !pane) {
return null
}
const buffer = pane.terminal.buffer.active
for (let row = 0; row < pane.terminal.rows; row += 1) {
const line = buffer.getLine(buffer.viewportY + row)
const col = line?.translateToString(true).indexOf(needle) ?? -1
if (col >= 0) {
return { col: col + Math.floor(needle.length / 2), row, tabId }
}
}
return null
}, needle)
}
async function linkClientPoint(page: Page, probe: LinkProbe): Promise<LinkClientPoint> {
return page.evaluate(({ col, row, tabId }) => {
const manager = window.__paneManagers?.get(tabId)
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
const screen = pane?.terminal.element?.querySelector<HTMLElement>('.xterm-screen')
if (!pane || !screen) {
throw new Error('terminal link surface unavailable')
}
const rect = screen.getBoundingClientRect()
const cell = pane.terminal.dimensions?.css.cell
if (!cell?.width || !cell.height) {
throw new Error('terminal cell dimensions unavailable')
}
return {
x: rect.left + (col + 0.5) * cell.width,
y: rect.top + (row + 0.5) * cell.height
}
}, probe)
}
async function hoverLink(page: Page, probe: LinkProbe): Promise<string | null> {
const point = await linkClientPoint(page, probe)
return page.evaluate(
({ tabId, x, y }) => {
const manager = window.__paneManagers?.get(tabId)
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
const screen = pane?.terminal.element?.querySelector<HTMLElement>('.xterm-screen')
if (!pane || !screen) {
throw new Error('terminal link surface unavailable')
}
screen.dispatchEvent(
new MouseEvent('mousemove', { bubbles: true, cancelable: true, clientX: x, clientY: y })
)
const terminal = pane.terminal as unknown as {
_core?: { linkifier?: { currentLink?: { link?: { text?: string } } } }
}
return terminal._core?.linkifier?.currentLink?.link?.text ?? null
},
{ tabId: probe.tabId, ...point }
)
}
async function clickLink(page: Page, probe: LinkProbe): Promise<void> {
const target = await linkClientPoint(page, probe)
await page.mouse.move(target.x, target.y)
await page.mouse.click(target.x, target.y)
}
test('opens a terminal file link and observes an external edit @golden', async ({ orcaPage }) => {
test.setTimeout(180_000)
await waitForSessionReady(orcaPage)
const worktreeId = await waitForActiveWorktree(orcaPage)
await ensureTerminalVisible(orcaPage)
await waitForActiveTerminalManager(orcaPage, 30_000)
const ptyId = await waitForActivePanePtyId(orcaPage)
await waitForPtyShellEcho(orcaPage, ptyId, 15_000)
const worktreePath = await orcaPage.evaluate((id) => {
return (
Object.values(window.__store?.getState().worktreesByRepo ?? {})
.flat()
.find((worktree) => worktree.id === id)?.path ?? ''
)
}, worktreeId)
const filePath = path.join(worktreePath, 'package.json')
const original = readFileSync(filePath, 'utf8')
const clickablePath = process.platform === 'win32' ? filePath.replaceAll('\\', '/') : filePath
const changedMarker = `golden-external-edit-${Date.now()}`
try {
await openFileExplorer(orcaPage)
const explorerRow = orcaPage
.locator('[data-file-explorer-row]')
.filter({ hasText: 'package.json' })
.first()
await expect(explorerRow).toBeVisible({ timeout: 15_000 })
const pathsToPrint = process.platform === 'win32' ? [filePath, clickablePath] : [filePath]
for (const printedPath of pathsToPrint) {
const command = nodeTerminalCommand(['-e', `console.log(${JSON.stringify(printedPath)})`])
await sendToTerminal(orcaPage, ptyId, `${command}\r`)
}
await expect
.poll(() => getTerminalContent(orcaPage, LINK_SCAN_CHAR_LIMIT), { timeout: 15_000 })
.toContain(clickablePath)
let probe: LinkProbe | null = null
await expect
.poll(
async () => {
probe = await locateLink(orcaPage, clickablePath)
return probe ? hoverLink(orcaPage, probe) : null
},
{ timeout: 10_000, message: 'absolute file path did not become clickable' }
)
.toContain('package.json')
if (!probe) {
throw new Error('terminal file link disappeared before activation')
}
await clickLink(orcaPage, probe)
const actionPopover = orcaPage.locator('[data-terminal-link-action-popover]')
await expect(actionPopover).toBeVisible()
// Why: the popover echoes the link text verbatim, which on Windows is the forward-slash form.
await expect(actionPopover.locator('[data-terminal-link-destination]')).toContainText(
clickablePath
)
await actionPopover.getByRole('button', { name: /Open file/i }).click()
const editorHeader = orcaPage.locator('.editor-header-path').first()
await expect(editorHeader).toContainText('package.json', { timeout: 20_000 })
await expect(explorerRow).toHaveAttribute('data-selected', 'true', { timeout: 10_000 })
await expect
.poll(
() =>
orcaPage.evaluate(
(expectedPath) => window.__monacoEditorE2E?.filePath === expectedPath,
filePath
),
{ timeout: 20_000, message: 'Monaco opened a different file identity' }
)
.toBe(true)
writeFileSync(filePath, `${original.trimEnd()}\n\n${changedMarker}\n`)
await expect
.poll(
async () => {
const snapshot = await orcaPage.evaluate(() => window.__monacoEditorE2E?.snapshot())
const reloadVisible = await orcaPage
.getByRole('button', { name: 'Reload from Disk' })
.isVisible()
.catch(() => false)
return Boolean(snapshot?.valueTail.includes(changedMarker) || reloadVisible)
},
{ timeout: 20_000, message: 'editor stayed silently stale after the external edit' }
)
.toBe(true)
} finally {
writeFileSync(filePath, original)
}
})