test(e2e): follow intentional product changes in stale full-suite specs (#30897)

The daily full E2E run on main fails these specs every time because the
product moved on and PR CI only runs specs a PR touches:
- editor header menu renamed to File actions (#30129)
- terminal attention: an on-screen bell leaves no unread marker (#28637)
- terminal file drops carry their source host (#26830)
- single-choice questions auto-submit (#26288)
- phone terminal rows carry customTitle (#30037)
- gh/git preflight re-reads the login shell PATH (#25183)
- board drag starts after the target column is in view
- chat history anchor read after scroll settles
This commit is contained in:
Brennan Benson
2026-10-20 12:41:00 -07:00
committed by GitHub
parent 821ab37369
commit 2fc2f92a38
9 changed files with 63 additions and 142 deletions
+1 -1
View File
@@ -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()
}
@@ -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
+2
View File
@@ -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()
@@ -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> {
@@ -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())
+1 -1
View File
@@ -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')
@@ -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
@@ -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)
+16 -127
View File
@@ -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)
@@ -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(
await expect
.poll(async () => (await getUnreadTerminalTabIds(orcaPage)).includes(activeTabId), {
timeout: 5_000,
message: 'A BEL on the focused tab left it unread'
})
.toBe(false)
await expect(
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'
})
.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(
`[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
@@ -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(