mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
test: cover quick commands, catalog links, and long discard dialogs (#17489)
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user