mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user