diff --git a/tests/e2e/diff-word-wrap.spec.ts b/tests/e2e/diff-word-wrap.spec.ts index 2b2941605df0..87d543e4c900 100644 --- a/tests/e2e/diff-word-wrap.spec.ts +++ b/tests/e2e/diff-word-wrap.spec.ts @@ -17,7 +17,7 @@ async function toggleWordWrap(page: Page, surface: DiffSurface): Promise { 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() } diff --git a/tests/e2e/github-created-issue-start-prefill.spec.ts b/tests/e2e/github-created-issue-start-prefill.spec.ts index 50d08acc0dda..8934349e863c 100644 --- a/tests/e2e/github-created-issue-start-prefill.spec.ts +++ b/tests/e2e/github-created-issue-start-prefill.spec.ts @@ -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 diff --git a/tests/e2e/github-project-board.spec.ts b/tests/e2e/github-project-board.spec.ts index 0a6f2c80416a..5e8ca0f909f3 100644 --- a/tests/e2e/github-project-board.spec.ts +++ b/tests/e2e/github-project-board.spec.ts @@ -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() diff --git a/tests/e2e/landing-preflight-runtime-routing.spec.ts b/tests/e2e/landing-preflight-runtime-routing.spec.ts index 47fac4646f75..b1bdc78ed613 100644 --- a/tests/e2e/landing-preflight-runtime-routing.spec.ts +++ b/tests/e2e/landing-preflight-runtime-routing.spec.ts @@ -20,10 +20,15 @@ test.afterAll(() => { rmSync(missingGitPath, { recursive: true, force: true }) }) -async function setProcessPath(app: ElectronApplication, value: string): Promise { - await app.evaluate((_electron, nextPath) => { - process.env.PATH = nextPath - }, value) +async function hideLocalGit(app: ElectronApplication, value: string): Promise { + // 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 { @@ -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()) diff --git a/tests/e2e/large-markdown-preview.spec.ts b/tests/e2e/large-markdown-preview.spec.ts index 24d3b75add1d..827d441bf1bc 100644 --- a/tests/e2e/large-markdown-preview.spec.ts +++ b/tests/e2e/large-markdown-preview.spec.ts @@ -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') diff --git a/tests/e2e/native-chat-ask-user-question-card.spec.ts b/tests/e2e/native-chat-ask-user-question-card.spec.ts index 41cbc5771937..3bd71098bb8b 100644 --- a/tests/e2e/native-chat-ask-user-question-card.spec.ts +++ b/tests/e2e/native-chat-ask-user-question-card.spec.ts @@ -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 diff --git a/tests/e2e/native-chat-history-prepend-anchor.spec.ts b/tests/e2e/native-chat-history-prepend-anchor.spec.ts index 236d30c4475c..cd0428c50bf2 100644 --- a/tests/e2e/native-chat-history-prepend-anchor.spec.ts +++ b/tests/e2e/native-chat-history-prepend-anchor.spec.ts @@ -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((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) diff --git a/tests/e2e/terminal-attention.spec.ts b/tests/e2e/terminal-attention.spec.ts index 1d7f857b22ca..4aab7ba60617 100644 --- a/tests/e2e/terminal-attention.spec.ts +++ b/tests/e2e/terminal-attention.spec.ts @@ -106,28 +106,6 @@ async function getUnreadTerminalTabIds(page: Page): Promise { }) } -async function getUnreadTerminalPaneKeys(page: Page): Promise { - return page.evaluate(() => { - const store = window.__store - if (!store) { - return [] - } - return Object.keys(store.getState().unreadTerminalPanes) - }) -} - -async function getActivePaneKey(page: Page, tabId: string): Promise { - 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 { await page.evaluate((targetTabId) => { const manager = window.__paneManagers?.get(targetTabId) @@ -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) @@ -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 diff --git a/tests/e2e/terminal-split-pane-paste-ownership.spec.ts b/tests/e2e/terminal-split-pane-paste-ownership.spec.ts index 3422a409f393..fd2f42dc2b4b 100644 --- a/tests/e2e/terminal-split-pane-paste-ownership.spec.ts +++ b/tests/e2e/terminal-split-pane-paste-ownership.spec.ts @@ -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(