Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tests/e2e/diff-word-wrap.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ async function toggleWordWrap(page: Page, surface: DiffSurface): Promise<void> {
await page.getByRole('button', { name: /^Wrap (On|Off)$/ }).click()
return
}
await page.getByRole('button', { name: 'More actions', exact: true }).click()
await page.getByRole('button', { name: 'File actions', exact: true }).click()
await page.getByRole('menuitemcheckbox', { name: 'Word Wrap', exact: true }).click()
}

Expand Down
11 changes: 10 additions & 1 deletion tests/e2e/github-created-issue-start-prefill.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,19 @@ test.beforeAll(({ testRepoPath }) => {
})

test('starting a just-created GitHub issue launches Claude with its URL prefilled', async ({
orcaPage
orcaPage,
electronApp
}) => {
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
// Why: the gh preflight merges the login-shell PATH ahead of launchEnv, so the fake gh can lose to a real one.
await electronApp.evaluate(
({ ipcMain }, created) => {
ipcMain.removeHandler('gh:createIssue')
ipcMain.handle('gh:createIssue', () => ({ ok: true, ...created }))
},
{ number: ISSUE_NUMBER, url: ISSUE_URL }
)

await orcaPage.evaluate(async () => {
const store = window.__store
Expand Down
2 changes: 2 additions & 0 deletions tests/e2e/github-project-board.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ test('board drops cross preload, settle optimistically, and roll back failures',
}
])
await done.getByRole('listitem').dragTo(done)
// Scrolling the board after mouse-down moves the card off the press point, so no drag starts.
await empty.scrollIntoViewIfNeeded()
await done.getByRole('listitem').dragTo(empty)
await expect(empty.getByRole('button', { name: title })).toBeVisible()
await expect(orcaPage.getByText('Board verification: offline', { exact: true })).toBeVisible()
Expand Down
15 changes: 10 additions & 5 deletions tests/e2e/landing-preflight-runtime-routing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,15 @@ test.afterAll(() => {
rmSync(missingGitPath, { recursive: true, force: true })
})

async function setProcessPath(app: ElectronApplication, value: string): Promise<void> {
await app.evaluate((_electron, nextPath) => {
process.env.PATH = nextPath
}, value)
async function hideLocalGit(app: ElectronApplication, value: string): Promise<void> {
// Why SHELL too: a forced local preflight merges the login shell's PATH back in first.
await app.evaluate(
(_electron, { nextPath, missingShell }) => {
process.env.PATH = nextPath
process.env.SHELL = missingShell
},
{ nextPath: value, missingShell: path.join(value, 'missing-shell') }
)
}

async function directGitPreflight(page: Page): Promise<boolean> {
Expand Down Expand Up @@ -84,7 +89,7 @@ async function runRuntimePreflightJourney(

const offerA = await createRuntimeDesktopPairingOffer(hubAPage)
client = await launchPairedElectronClient(offerA, testInfo, 'Preflight runtime A')
await setProcessPath(client.app, missingGitPath)
await hideLocalGit(client.app, missingGitPath)
expect(await client.app.evaluate(() => process.env.PATH)).toBe(missingGitPath)
if (headed) {
await client.app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0]?.show())
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/large-markdown-preview.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ for (const width of [1920, 1280]) {
})
await expect(preview).toHaveClass(/markdown-dark/)
await orcaPage.screenshot({ path: path.join(proofDirectory, 'toc-dark.png') })
await orcaPage.getByRole('button', { name: 'More actions', exact: true }).click()
await orcaPage.getByRole('button', { name: 'File actions', exact: true }).click()
await expect(
orcaPage.getByRole('menuitem', { name: 'Export as PDF', exact: true })
).toHaveAttribute('aria-disabled', 'true')
Expand Down
8 changes: 2 additions & 6 deletions tests/e2e/native-chat-ask-user-question-card.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,9 @@ test.describe('Desktop chat AskUserQuestion card (#11761)', () => {
await expect(orcaPage.getByRole('button', { name: /Spaces/ })).toBeVisible()
await orcaPage.screenshot({ path: path.join(screenshotDir, '01-question-card.png') })

// The submit button reads "Skip" until an option is chosen; picking one is
// what proves the card is answerable rather than merely rendered.
// A single-select pick on the last question submits on its own after a short
// beat (#26288); that is what proves the card is answerable rather than merely rendered.
await orcaPage.getByRole('button', { name: /Spaces/ }).click()
await expect(orcaPage.getByRole('button', { name: 'Submit' })).toBeVisible()
await orcaPage.screenshot({ path: path.join(screenshotDir, '02-option-selected.png') })

await orcaPage.getByRole('button', { name: 'Submit' }).click()
// The card owns the composer slot, so its disappearance is the visible
// signal that the answer was accepted and chat input came back. The
// resolution receipt keeps the question in the transcript row, so only
Expand Down
13 changes: 12 additions & 1 deletion tests/e2e/native-chat-history-prepend-anchor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,18 @@ test.describe('Native chat transcript anchoring', () => {
)
)
.toBeGreaterThan(1_000)
const readingAt = await scroll.evaluate((element) => element.scrollTop)
// Rows the wheel mounted above the reader settle their measured heights frames later.
const readingAt = await scroll.evaluate(async (element) => {
let previousGeometry = ''
let stableFrames = 0
for (let frame = 0; frame < 120 && stableFrames < 5; frame += 1) {
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
const geometry = `${element.scrollHeight}:${element.scrollTop}`
stableFrames = geometry === previousGeometry ? stableFrames + 1 : 0
previousGeometry = geometry
}
return element.scrollTop
})
await expect(jump).toBeVisible()

await activateNewTerminalTab(orcaPage, descriptor.worktreeId)
Expand Down
133 changes: 11 additions & 122 deletions tests/e2e/terminal-attention.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,28 +106,6 @@ async function getUnreadTerminalTabIds(page: Page): Promise<string[]> {
})
}

async function getUnreadTerminalPaneKeys(page: Page): Promise<string[]> {
return page.evaluate(() => {
const store = window.__store
if (!store) {
return []
}
return Object.keys(store.getState().unreadTerminalPanes)
})
}

async function getActivePaneKey(page: Page, tabId: string): Promise<string> {
return page.evaluate((targetTabId) => {
const manager = window.__paneManagers?.get(targetTabId)
const pane = manager?.getActivePane?.()
const leafId = pane?.leafId ?? null
if (!leafId) {
throw new Error(`No active pane leaf for terminal tab ${targetTabId}`)
}
return `${targetTabId}:${leafId}`
}, tabId)
}

async function focusActiveXterm(page: Page, tabId: string): Promise<void> {
await page.evaluate((targetTabId) => {
const manager = window.__paneManagers?.get(targetTabId)
Expand Down Expand Up @@ -223,11 +201,9 @@ test.describe('Terminal attention', () => {
await expect(secondTabBell).toBeHidden()
})

// Why (show-until-interact): ghostty's model fires the bell even on the
// currently-focused tab — the user only dismisses it by actually engaging
// with the pane. This test proves the BEL on a focused tab is visible
// until a pointerdown on the terminal container clears it.
test('a BEL on the focused tab raises, then clears on click', async ({ orcaPage }) => {
// Why (on screen means read, #28637): a BEL on the tab the present user is
// looking at is already read, so it must leave no unread marker or bell.
test('a BEL on the focused tab leaves no unread marker', async ({ orcaPage }) => {
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
await ensureTerminalVisible(orcaPage)
Expand All @@ -240,114 +216,27 @@ test.describe('Terminal attention', () => {
const activePtyId = await waitForActivePanePtyId(orcaPage)
await installRendererTitleLog(orcaPage)

// Why: presence comes only from trusted input; a bare Shift keydown sends nothing to the shell.
await focusActiveXterm(orcaPage, activeTabId)
await orcaPage.keyboard.press('Shift')

await emitBellAndWaitForTitleFlush(
orcaPage,
activePtyId,
`focused-tab-bell-marker-${Date.now()}`
)

// The focused tab is now unread — the bell persists until the user
// actually interacts with the pane.
expect((await getUnreadTerminalTabIds(orcaPage)).includes(activeTabId)).toBe(true)
const activeTabBell = orcaPage
.locator(
`[data-testid="sortable-tab"][data-tab-id="${activeTabId}"] [data-testid="tab-activity-bell"]`
)
.first()
await expect(activeTabBell).toBeVisible()

// A pointerdown inside the terminal container counts as interaction
// (matches the pointerdown handler added in TerminalPane.tsx). Drive it
// via the DOM so we exercise the real listener path rather than bypassing
// to the store action.
await orcaPage.evaluate((tabId) => {
const managers = window.__paneManagers
const manager = managers?.get(tabId)
const pane = manager?.getActivePane()
const container = pane?.container
if (!container) {
throw new Error('No active pane container to click')
}
container.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, cancelable: true }))
}, activeTabId)

await expect
.poll(async () => (await getUnreadTerminalTabIds(orcaPage)).includes(activeTabId), {
timeout: 5_000,
message: 'Unread state did not clear after interacting with the pane'
message: 'A BEL on the focused tab left it unread'
})
.toBe(false)
await expect(activeTabBell).toBeHidden()
})

// Why (plain Escape regression): Escape also emits real terminal input, but
// the interrupt-intent branch returns early. It must still dismiss focused
// terminal attention just like other user key input.
test('a BEL on the focused tab raises, then clears on plain Escape', async ({ orcaPage }) => {
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
await ensureTerminalVisible(orcaPage)
await waitForActiveTerminalManager(orcaPage, 30_000)

const activeTabId = await getActiveTabId(orcaPage)
if (!activeTabId) {
throw new Error('Expected an active terminal tab')
}
const activePaneKey = await getActivePaneKey(orcaPage, activeTabId)
const activePtyId = await waitForActivePanePtyId(orcaPage)
await installRendererTitleLog(orcaPage)

await emitBellAndWaitForTitleFlush(
orcaPage,
activePtyId,
`focused-tab-escape-marker-${Date.now()}`
)

await expect
.poll(async () => (await getUnreadTerminalTabIds(orcaPage)).includes(activeTabId), {
timeout: 10_000,
message: 'Focused tab did not become unread after BEL'
})
.toBe(true)

// Focused BEL owns the tab indicator; seed pane attention separately so the
// Escape path proves it clears both store surfaces that pty-connection owns.
await orcaPage.evaluate((paneKey) => {
// Why: consumers read the marker value, not key presence (#20525), so seed the real reason.
window.__store?.setState((state) => ({
unreadTerminalPanes: { ...state.unreadTerminalPanes, [paneKey]: 'terminal-bell' }
}))
}, activePaneKey)
await expect
.poll(async () => (await getUnreadTerminalPaneKeys(orcaPage)).includes(activePaneKey), {
timeout: 5_000,
message: 'Seeded focused pane attention did not land'
})
.toBe(true)

const activeTabBell = orcaPage
.locator(
await expect(
orcaPage.locator(
`[data-testid="sortable-tab"][data-tab-id="${activeTabId}"] [data-testid="tab-activity-bell"]`
)
.first()
await expect(activeTabBell).toBeVisible()

await focusActiveXterm(orcaPage, activeTabId)
await orcaPage.keyboard.press('Escape')

await expect
.poll(async () => (await getUnreadTerminalTabIds(orcaPage)).includes(activeTabId), {
timeout: 5_000,
message: 'Unread tab state did not clear after pressing Escape in xterm'
})
.toBe(false)
await expect
.poll(async () => (await getUnreadTerminalPaneKeys(orcaPage)).includes(activePaneKey), {
timeout: 5_000,
message: 'Unread pane state did not clear after pressing Escape in xterm'
})
.toBe(false)
await expect(activeTabBell).toBeHidden()
).toHaveCount(0)
})

// Why (restart regression guard): the original user-reported bug was that
Expand Down
9 changes: 9 additions & 0 deletions tests/e2e/terminal-split-pane-paste-ownership.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,15 @@ test.describe('split terminal pane paste ownership', () => {
}
const dataTransfer = new DataTransfer()
dataTransfer.setData('text/x-orca-file-path', pathValue)
// Internal drops are refused without the source owner an Explorer drag records.
dataTransfer.setData(
'application/x-orca-workspace-file-source',
JSON.stringify({
executionHostId: 'local',
workspaceId: state?.activeWorktreeId,
version: 1
})
)
const target = pane.container.querySelector('.xterm-screen, textarea') ?? pane.container
for (const eventType of ['dragenter', 'dragover', 'drop']) {
target.dispatchEvent(
Expand Down
Loading