test: cover quick commands, catalog links, and long discard dialogs (#17489)

This commit is contained in:
Jinwoo Hong
2026-08-31 14:56:15 -04:00
committed by GitHub
parent d1350735ef
commit bbbb59e18c
3 changed files with 211 additions and 6 deletions
@@ -0,0 +1,143 @@
// @vitest-environment happy-dom
import '@testing-library/jest-dom/vitest'
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as I18nModule from '@/i18n/i18n'
import type {
TerminalCommandQuickCommand,
TerminalQuickCommand
} from '../../../../shared/terminal-quick-command-types'
import { QuickCommandsList } from './QuickCommandsList'
vi.mock('@/i18n/i18n', async (importOriginal) => {
const actual = await importOriginal<typeof I18nModule>()
return {
...actual,
translate: (_key: string, fallback: string, values?: Record<string, string>) =>
values
? Object.entries(values).reduce(
(text, [token, value]) => text.replace(`{{${token}}}`, value),
fallback
)
: fallback
}
})
afterEach(cleanup)
function makeCommand(overrides: Partial<TerminalCommandQuickCommand> = {}): TerminalQuickCommand {
return {
id: 'build',
label: 'Build',
action: 'terminal-command',
command: 'pnpm build',
appendEnter: true,
scope: { type: 'global' },
...overrides
}
}
function renderList(
commands: TerminalQuickCommand[],
visibleCommands: TerminalQuickCommand[] = commands,
hasQuery = false,
onEdit = vi.fn(),
onRemove = vi.fn()
) {
return render(
<QuickCommandsList
commands={commands}
visibleCommands={visibleCommands}
hasQuery={hasQuery}
repoById={new Map()}
onEdit={onEdit}
onRemove={onRemove}
/>
)
}
describe('QuickCommandsList', () => {
beforeEach(() => {
Object.assign(window, {
api: { ui: { writeClipboardText: vi.fn().mockResolvedValue(undefined) } }
})
})
it('keeps row actions reachable and gives each action its own semantics', async () => {
const build = makeCommand()
const empty = makeCommand({ id: 'empty', label: 'Empty', command: ' ', appendEnter: false })
const onEdit = vi.fn()
const onRemove = vi.fn()
renderList([build, empty], [build, empty], false, onEdit, onRemove)
const edit = screen.getByRole('button', { name: 'Edit Build' })
const copy = screen.getByRole('button', { name: 'Copy Build' })
const remove = screen.getByRole('button', { name: 'Remove Build' })
const emptyCopy = screen.getByRole('button', { name: 'Nothing to copy' })
for (const button of [edit, copy, remove]) {
button.focus()
expect(document.activeElement).toBe(button)
}
expect(emptyCopy).toBeDisabled()
fireEvent.click(screen.getByText('Build', { exact: true }))
expect(onEdit).not.toHaveBeenCalled()
expect(onRemove).not.toHaveBeenCalled()
fireEvent.click(edit)
expect(onEdit).toHaveBeenCalledWith(build)
fireEvent.click(remove)
expect(onRemove).toHaveBeenCalledWith(build)
fireEvent.click(copy)
await waitFor(() => expect(screen.getByRole('button', { name: 'Copied' })).toBeInTheDocument())
expect(window.api.ui.writeClipboardText).toHaveBeenCalledWith('pnpm build')
expect(onEdit).toHaveBeenCalledTimes(1)
expect(onRemove).toHaveBeenCalledTimes(1)
})
it('distinguishes empty, filtered, and unmatched states', () => {
const command = makeCommand()
const view = renderList([], [], false)
expect(screen.getByText('No quick commands saved.')).toBeDefined()
view.rerender(
<QuickCommandsList
commands={[command]}
visibleCommands={[]}
hasQuery={false}
repoById={new Map()}
onEdit={vi.fn()}
onRemove={vi.fn()}
/>
)
expect(screen.getByText('No commands in the selected scopes.')).toBeDefined()
view.rerender(
<QuickCommandsList
commands={[command]}
visibleCommands={[]}
hasQuery={true}
repoById={new Map()}
onEdit={vi.fn()}
onRemove={vi.fn()}
/>
)
expect(screen.getByText('No commands match this search.')).toBeDefined()
view.rerender(
<QuickCommandsList
commands={[command]}
visibleCommands={[command]}
hasQuery={false}
repoById={new Map()}
onEdit={vi.fn()}
onRemove={vi.fn()}
/>
)
expect(screen.getByText('Build', { exact: true })).toBeDefined()
expect(screen.queryByText('No commands in the selected scopes.')).toBeNull()
})
})
@@ -0,0 +1,13 @@
import { describe, expect, it } from 'vitest'
import { getAgentCatalog } from './agent-catalog'
describe('agent catalog documentation links', () => {
it('keeps Claude links on the canonical documentation site', () => {
const entries = new Map(getAgentCatalog().map((entry) => [entry.id, entry]))
expect(entries.get('claude')?.homepageUrl).toBe('https://code.claude.com/docs')
expect(entries.get('claude-agent-teams')?.homepageUrl).toBe(
'https://code.claude.com/docs/en/agent-teams'
)
})
})
@@ -16,8 +16,11 @@ async function openSourceControl(page: Page): Promise<void> {
await expect(page.getByPlaceholder(/Filter files/)).toBeVisible()
}
async function seedUntrackedFile(page: Page): Promise<SeededUntrackedFile> {
return page.evaluate(async () => {
async function seedUntrackedFile(
page: Page,
requestedFileName?: string
): Promise<SeededUntrackedFile> {
return page.evaluate(async (requestedFileName) => {
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
@@ -33,7 +36,7 @@ async function seedUntrackedFile(page: Page): Promise<SeededUntrackedFile> {
}
const separator = worktree.path.includes('\\') ? '\\' : '/'
const fileName = `orca-discard-confirm-${Date.now()}.txt`
const fileName = requestedFileName ?? `orca-discard-confirm-${Date.now()}.txt`
const relativePath = fileName
await window.api.fs.writeFile({
filePath: `${worktree.path}${separator}${relativePath}`,
@@ -50,7 +53,7 @@ async function seedUntrackedFile(page: Page): Promise<SeededUntrackedFile> {
return {
fileName
}
})
}, requestedFileName)
}
async function refreshGitStatus(page: Page): Promise<void> {
@@ -90,14 +93,54 @@ async function confirmPendingDelete(page: Page): Promise<void> {
await confirmButton.click()
}
async function expectDeleteDialogLayout(page: Page, fileName: string): Promise<void> {
const dialog = page.getByRole('dialog', { name: `Delete "${fileName}"?` })
await expect(dialog).toBeVisible()
await expect
.poll(
async () =>
dialog.evaluate((element) => {
const panel = element.getBoundingClientRect()
const title = element.querySelector<HTMLElement>('[data-slot="dialog-title"]')
const footer = element.querySelector<HTMLElement>('[data-slot="dialog-footer"]')
if (!title || !footer) {
return false
}
const titleRect = title.getBoundingClientRect()
const footerRect = footer.getBoundingClientRect()
const lineHeight = Number.parseFloat(getComputedStyle(title).lineHeight) || 16
const buttonsFit = [...footer.querySelectorAll<HTMLElement>('button')].every((button) => {
const rect = button.getBoundingClientRect()
return rect.left >= panel.left && rect.right <= panel.right
})
return (
titleRect.height > lineHeight * 1.5 &&
titleRect.left >= panel.left &&
titleRect.right <= panel.right &&
footerRect.left >= panel.left &&
footerRect.right <= panel.right &&
buttonsFit &&
element.scrollWidth <= element.clientWidth
)
}),
{ timeout: 5_000, message: 'long delete-dialog title or footer escaped the panel' }
)
.toBe(true)
}
test.describe('Source Control discard confirmation', () => {
test.beforeEach(async ({ orcaPage }) => {
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
})
test('deletes an untracked file without confirmation', async ({ orcaPage }) => {
const seededFile = await seedUntrackedFile(orcaPage)
test('keeps long untracked-file confirmation usable and deletes on confirm', async ({
orcaPage
}) => {
const seededFile = await seedUntrackedFile(
orcaPage,
`orca-discard-confirm-${'x'.repeat(96)}.txt`
)
await openSourceControl(orcaPage)
const row = orcaPage
@@ -105,6 +148,12 @@ test.describe('Source Control discard confirmation', () => {
.filter({ hasText: seededFile.fileName })
await expect(row).toBeVisible()
await deleteUntrackedFileFromRow(row)
await expectDeleteDialogLayout(orcaPage, seededFile.fileName)
await orcaPage.getByRole('button', { name: 'Cancel' }).click()
await expect(row).toBeVisible()
await deleteUntrackedFileFromRow(row)
await confirmPendingDelete(orcaPage)