Files
orca/tests/e2e/native-chat-send-focus.spec.ts
T
dd69e7dd80 fix(renderer): restore focus after floating tab rename (#31510)
* fix(renderer): restore focus after floating tab rename

* fix: limit one-shot chat composer focus to tab rename return

Other onlyIfFocusUnclaimed callers (e.g. floating terminal) keep main's
replayable composer focus request.

---------

Co-authored-by: MimeLyc <11363886+MimeLyc@users.noreply.github.com>
Co-authored-by: Neil <neil@stably.ai>
2026-10-24 00:24:59 -07:00

87 lines
3.5 KiB
TypeScript

// Completed tab and composer actions return keyboard input to the chat draft.
import type { Locator, Page } from '@stablyai/playwright-test'
import { expect } from './helpers/orca-app'
import { waitForActiveWorktree, waitForSessionReady } from './helpers/store'
import { buildFakeAgentCommandOverride } from './helpers/fake-agent-command-override'
import { SUPPORTED, createChatOnHost, test } from './helpers/structured-claude-stub'
function focusIsIn(composer: Locator): Promise<boolean> {
return composer.evaluate((element) => element.contains(element.ownerDocument.activeElement))
}
async function openChat(page: Page, claudeCommand: string) {
await waitForSessionReady(page)
const worktreeId = await waitForActiveWorktree(page)
await page.evaluate(async (command) => {
await window.__store?.getState().updateSettings({
experimentalNativeChat: true,
defaultTuiAgent: 'claude',
agentCmdOverrides: { claude: command }
})
}, buildFakeAgentCommandOverride(claudeCommand))
const sessionId = await createChatOnHost(page, worktreeId)
const readTabId = () =>
page.evaluate(
({ worktreeId, sessionId }) =>
window.__store
?.getState()
.unifiedTabsByWorktree[worktreeId]?.find((tab) => tab.entityId === sessionId)?.id,
{ worktreeId, sessionId }
)
await expect.poll(readTabId, { timeout: 30_000 }).toBeTruthy()
const tab = page.locator(`[data-testid="sortable-tab"][data-tab-id="${await readTabId()}"]`)
await tab.click()
const chat = page.locator('[data-native-chat-root="true"]')
const composer = chat.getByRole('textbox')
await composer.click()
return {
tab,
composer,
send: chat.getByRole('button', { name: 'Send', exact: true }),
stop: chat.getByRole('button', { name: 'Stop the agent' }),
reply: chat.getByText('Stub reply.')
}
}
test.describe('native chat composer focus', () => {
test.skip(!SUPPORTED, 'the Claude stub wrapper is POSIX-only')
test('keyboard tab rename returns input to the native chat composer', async ({
orcaPage: page,
claudeStub
}) => {
const { tab, composer } = await openChat(page, claudeStub.command)
await tab.dblclick()
const rename = tab.locator('[data-tab-rename-input="true"]')
await expect(rename).toBeFocused()
await rename.fill('')
await rename.pressSequentially('Renamed chat', { delay: 60 })
await page.keyboard.press('Enter')
await expect(rename).toHaveCount(0)
await expect(tab).toHaveAttribute('data-tab-title', 'Renamed chat')
await expect.poll(() => focusIsIn(composer)).toBe(true)
await page.keyboard.type('Draft after rename')
await expect(composer).toContainText('Draft after rename')
})
test('when Send disables at the end of the turn', async ({ orcaPage: page, claudeStub }) => {
const { composer, send, stop, reply } = await openChat(page, claudeStub.command)
await page.keyboard.type('hello')
await send.click()
await expect(reply).toBeVisible({ timeout: 30_000 })
await expect(stop).toHaveCount(0)
await expect.poll(() => focusIsIn(composer)).toBe(true)
})
test('when Stop disables while the stop is in flight', async ({ orcaPage: page, claudeStub }) => {
const { composer, send, stop } = await openChat(page, claudeStub.command)
// The stub runs a prompt containing "slow" until it is interrupted.
await page.keyboard.type('slow')
await send.click()
await stop.click()
await expect(stop).toHaveCount(0, { timeout: 30_000 })
await expect.poll(() => focusIsIn(composer)).toBe(true)
})
})