From bbbb59e18ca9e63dc7faf676d461e97956f3d6e3 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Mon, 31 Aug 2026 14:56:15 -0400 Subject: [PATCH] test: cover quick commands, catalog links, and long discard dialogs (#17489) --- .../settings/QuickCommandsList.test.tsx | 143 ++++++++++++++++++ .../src/lib/agent-catalog-links.test.ts | 13 ++ ...ource-control-discard-confirmation.spec.ts | 61 +++++++- 3 files changed, 211 insertions(+), 6 deletions(-) create mode 100644 src/renderer/src/components/settings/QuickCommandsList.test.tsx create mode 100644 src/renderer/src/lib/agent-catalog-links.test.ts diff --git a/src/renderer/src/components/settings/QuickCommandsList.test.tsx b/src/renderer/src/components/settings/QuickCommandsList.test.tsx new file mode 100644 index 00000000000..d11e249a63d --- /dev/null +++ b/src/renderer/src/components/settings/QuickCommandsList.test.tsx @@ -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() + return { + ...actual, + translate: (_key: string, fallback: string, values?: Record) => + values + ? Object.entries(values).reduce( + (text, [token, value]) => text.replace(`{{${token}}}`, value), + fallback + ) + : fallback + } +}) + +afterEach(cleanup) + +function makeCommand(overrides: Partial = {}): 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( + + ) +} + +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( + + ) + expect(screen.getByText('No commands in the selected scopes.')).toBeDefined() + + view.rerender( + + ) + expect(screen.getByText('No commands match this search.')).toBeDefined() + + view.rerender( + + ) + expect(screen.getByText('Build', { exact: true })).toBeDefined() + expect(screen.queryByText('No commands in the selected scopes.')).toBeNull() + }) +}) diff --git a/src/renderer/src/lib/agent-catalog-links.test.ts b/src/renderer/src/lib/agent-catalog-links.test.ts new file mode 100644 index 00000000000..cc439827340 --- /dev/null +++ b/src/renderer/src/lib/agent-catalog-links.test.ts @@ -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' + ) + }) +}) diff --git a/tests/e2e/source-control-discard-confirmation.spec.ts b/tests/e2e/source-control-discard-confirmation.spec.ts index 17aedb6fd63..2db4f4b1a0a 100644 --- a/tests/e2e/source-control-discard-confirmation.spec.ts +++ b/tests/e2e/source-control-discard-confirmation.spec.ts @@ -16,8 +16,11 @@ async function openSourceControl(page: Page): Promise { await expect(page.getByPlaceholder(/Filter files/)).toBeVisible() } -async function seedUntrackedFile(page: Page): Promise { - return page.evaluate(async () => { +async function seedUntrackedFile( + page: Page, + requestedFileName?: string +): Promise { + 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 { } 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 { return { fileName } - }) + }, requestedFileName) } async function refreshGitStatus(page: Page): Promise { @@ -90,14 +93,54 @@ async function confirmPendingDelete(page: Page): Promise { await confirmButton.click() } +async function expectDeleteDialogLayout(page: Page, fileName: string): Promise { + 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('[data-slot="dialog-title"]') + const footer = element.querySelector('[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('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)