From f9f916e1dc41d62998c7ea2c173c6e910837ed5e Mon Sep 17 00:00:00 2001 From: Jacob Richman Date: Wed, 25 Feb 2026 15:31:35 -0800 Subject: [PATCH] test: support tests that include color information (#20220) --- .../cli/src/commands/extensions/link.test.ts | 33 +-- .../cli/src/commands/extensions/list.test.ts | 39 ++-- .../cli/src/commands/skills/disable.test.ts | 13 +- .../cli/src/commands/skills/enable.test.ts | 13 +- .../cli/src/commands/skills/install.test.ts | 28 ++- packages/cli/src/commands/skills/link.test.ts | 11 +- packages/cli/src/commands/skills/list.test.ts | 50 ++--- .../cli/src/commands/skills/uninstall.test.ts | 21 +- ...-a-consent-string-with-all-fields.snap.svg | 18 ++ ...de-warning-when-hooks-are-present.snap.svg | 14 ++ ...-request-consent-if-skills-change.snap.svg | 28 +++ ...he-skill-directory-cannot-be-read.snap.svg | 22 ++ ...erate-a-consent-string-for-skills.snap.svg | 17 ++ .../__snapshots__/consent.test.ts.snap | 93 +++++++++ .../cli/src/config/extensions/consent.test.ts | 94 ++++----- packages/cli/src/test-utils/customMatchers.ts | 89 ++++++-- .../cli/src/test-utils/mockDebugLogger.ts | 76 +++++++ packages/cli/src/test-utils/render.tsx | 52 ++++- packages/cli/src/test-utils/svg.ts | 190 ++++++++++++++++++ .../src/ui/__snapshots__/App.test.tsx.snap | 48 ++--- .../cli/src/ui/components/AnsiOutput.test.tsx | 6 +- .../src/ui/components/AskUserDialog.test.tsx | 13 +- .../ui/components/ConfigInitDisplay.test.tsx | 14 +- .../src/ui/components/ConfigInitDisplay.tsx | 2 +- .../ui/components/FolderTrustDialog.test.tsx | 2 +- .../GeminiRespondingSpinner.test.tsx | 16 +- .../ui/components/GeminiRespondingSpinner.tsx | 55 +---- .../cli/src/ui/components/GeminiSpinner.tsx | 63 ++++++ .../src/ui/components/InputPrompt.test.tsx | 48 +++-- .../src/ui/components/SettingsDialog.test.tsx | 26 +-- packages/cli/src/ui/components/Table.test.tsx | 38 +++- .../components/ToolConfirmationQueue.test.tsx | 6 +- ...ternateBufferQuittingDisplay.test.tsx.snap | 74 +++---- .../__snapshots__/AppHeader.test.tsx.snap | 48 ++--- .../__snapshots__/InputPrompt.test.tsx.snap | 32 +-- ...tings-list-with-visual-indicators.snap.svg | 133 ++++++++++++ ...bility-settings-enabled-correctly.snap.svg | 133 ++++++++++++ ...olean-settings-disabled-correctly.snap.svg | 131 ++++++++++++ ...ld-render-default-state-correctly.snap.svg | 133 ++++++++++++ ...ing-settings-configured-correctly.snap.svg | 133 ++++++++++++ ...cused-on-scope-selector-correctly.snap.svg | 131 ++++++++++++ ...ean-and-number-settings-correctly.snap.svg | 132 ++++++++++++ ...s-and-security-settings-correctly.snap.svg | 133 ++++++++++++ ...oolean-settings-enabled-correctly.snap.svg | 131 ++++++++++++ .../SettingsDialog.test.tsx.snap | 27 +-- ...render-headers-and-data-correctly.snap.svg | 12 ++ ...uld-support-custom-cell-rendering.snap.svg | 11 + ...ld-support-inverse-text-rendering.snap.svg | 12 ++ .../__snapshots__/Table.test.tsx.snap | 12 +- .../__snapshots__/UserMessage.test.tsx.snap | 10 +- .../components/shared/ExpandableText.test.tsx | 37 ++-- .../__snapshots__/EnumSelector.test.tsx.snap | 2 +- ...indow-around-match-when-collapsed.snap.svg | 13 ++ ...-when-expanded-text-only-visible-.snap.svg | 12 ++ ...-label-when-no-match-short-label-.snap.svg | 9 + ...ableText-respects-custom-maxWidth.snap.svg | 9 + ...-label-when-expanded-and-no-match.snap.svg | 10 + ...label-when-collapsed-and-no-match.snap.svg | 10 + ...ch-itself-when-match-is-very-long.snap.svg | 12 ++ .../ExpandableText.test.tsx.snap | 27 +-- .../HalfLinePaddedBox.test.tsx.snap | 4 +- .../SearchableList.test.tsx.snap | 8 +- .../cli/src/ui/hooks/usePhraseCycler.test.tsx | 2 +- .../cli/src/ui/utils/MarkdownDisplay.test.tsx | 4 +- packages/cli/test-setup.ts | 3 + packages/core/src/utils/fileUtils.test.ts | 2 +- packages/core/src/utils/fileUtils.ts | 2 +- packages/core/src/utils/schemaValidator.ts | 2 +- 68 files changed, 2342 insertions(+), 492 deletions(-) create mode 100644 packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-generate-a-consent-string-with-all-fields.snap.svg create mode 100644 packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-include-warning-when-hooks-are-present.snap.svg create mode 100644 packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-request-consent-if-skills-change.snap.svg create mode 100644 packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-show-a-warning-if-the-skill-directory-cannot-be-read.snap.svg create mode 100644 packages/cli/src/config/extensions/__snapshots__/consent-consent-skillsConsentString-should-generate-a-consent-string-for-skills.snap.svg create mode 100644 packages/cli/src/config/extensions/__snapshots__/consent.test.ts.snap create mode 100644 packages/cli/src/test-utils/mockDebugLogger.ts create mode 100644 packages/cli/src/test-utils/svg.ts create mode 100644 packages/cli/src/ui/components/GeminiSpinner.tsx create mode 100644 packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Initial-Rendering-should-render-settings-list-with-visual-indicators.snap.svg create mode 100644 packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-accessibility-settings-enabled-correctly.snap.svg create mode 100644 packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-all-boolean-settings-disabled-correctly.snap.svg create mode 100644 packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-default-state-correctly.snap.svg create mode 100644 packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-file-filtering-settings-configured-correctly.snap.svg create mode 100644 packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-focused-on-scope-selector-correctly.snap.svg create mode 100644 packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-mixed-boolean-and-number-settings-correctly.snap.svg create mode 100644 packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-tools-and-security-settings-correctly.snap.svg create mode 100644 packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-various-boolean-settings-enabled-correctly.snap.svg create mode 100644 packages/cli/src/ui/components/__snapshots__/Table-Table-should-render-headers-and-data-correctly.snap.svg create mode 100644 packages/cli/src/ui/components/__snapshots__/Table-Table-should-support-custom-cell-rendering.snap.svg create mode 100644 packages/cli/src/ui/components/__snapshots__/Table-Table-should-support-inverse-text-rendering.snap.svg create mode 100644 packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-creates-centered-window-around-match-when-collapsed.snap.svg create mode 100644 packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-highlights-matched-substring-when-expanded-text-only-visible-.snap.svg create mode 100644 packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-renders-plain-label-when-no-match-short-label-.snap.svg create mode 100644 packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-respects-custom-maxWidth.snap.svg create mode 100644 packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-shows-full-long-label-when-expanded-and-no-match.snap.svg create mode 100644 packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-truncates-long-label-when-collapsed-and-no-match.snap.svg create mode 100644 packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-truncates-match-itself-when-match-is-very-long.snap.svg diff --git a/packages/cli/src/commands/extensions/link.test.ts b/packages/cli/src/commands/extensions/link.test.ts index bdd8c06b49..67351a5456 100644 --- a/packages/cli/src/commands/extensions/link.test.ts +++ b/packages/cli/src/commands/extensions/link.test.ts @@ -13,34 +13,21 @@ import { afterEach, type Mock, } from 'vitest'; -import { format } from 'node:util'; +import { coreEvents } from '@google/gemini-cli-core'; import { type Argv } from 'yargs'; import { handleLink, linkCommand } from './link.js'; import { ExtensionManager } from '../../config/extension-manager.js'; import { loadSettings, type LoadedSettings } from '../../config/settings.js'; import { getErrorMessage } from '../../utils/errors.js'; -// Mock dependencies -const emitConsoleLog = vi.hoisted(() => vi.fn()); -const debugLogger = vi.hoisted(() => ({ - log: vi.fn((message, ...args) => { - emitConsoleLog('log', format(message, ...args)); - }), - error: vi.fn((message, ...args) => { - emitConsoleLog('error', format(message, ...args)); - }), -})); - vi.mock('@google/gemini-cli-core', async (importOriginal) => { - const actual = - await importOriginal(); - return { - ...actual, - coreEvents: { - emitConsoleLog, - }, - debugLogger, - }; + const { mockCoreDebugLogger } = await import( + '../../test-utils/mockDebugLogger.js' + ); + return mockCoreDebugLogger( + await importOriginal(), + { stripAnsi: true }, + ); }); vi.mock('../../config/extension-manager.js'); @@ -95,7 +82,7 @@ describe('extensions link command', () => { source: '/local/path/to/extension', type: 'link', }); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', 'Extension "my-linked-extension" linked successfully and enabled.', ); @@ -116,7 +103,7 @@ describe('extensions link command', () => { await handleLink({ path: '/local/path/to/extension' }); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'error', 'Link failed message', ); diff --git a/packages/cli/src/commands/extensions/list.test.ts b/packages/cli/src/commands/extensions/list.test.ts index 6967719be8..f0f0168f79 100644 --- a/packages/cli/src/commands/extensions/list.test.ts +++ b/packages/cli/src/commands/extensions/list.test.ts @@ -5,33 +5,22 @@ */ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { format } from 'node:util'; +import { coreEvents } from '@google/gemini-cli-core'; import { handleList, listCommand } from './list.js'; import { ExtensionManager } from '../../config/extension-manager.js'; import { loadSettings, type LoadedSettings } from '../../config/settings.js'; import { getErrorMessage } from '../../utils/errors.js'; -// Mock dependencies -const emitConsoleLog = vi.hoisted(() => vi.fn()); -const debugLogger = vi.hoisted(() => ({ - log: vi.fn((message, ...args) => { - emitConsoleLog('log', format(message, ...args)); - }), - error: vi.fn((message, ...args) => { - emitConsoleLog('error', format(message, ...args)); - }), -})); - vi.mock('@google/gemini-cli-core', async (importOriginal) => { - const actual = - await importOriginal(); - return { - ...actual, - coreEvents: { - emitConsoleLog, + const { mockCoreDebugLogger } = await import( + '../../test-utils/mockDebugLogger.js' + ); + return mockCoreDebugLogger( + await importOriginal(), + { + stripAnsi: false, }, - debugLogger, - }; + ); }); vi.mock('../../config/extension-manager.js'); @@ -71,7 +60,7 @@ describe('extensions list command', () => { .mockResolvedValue([]); await handleList(); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', 'No extensions installed.', ); @@ -85,7 +74,7 @@ describe('extensions list command', () => { .mockResolvedValue([]); await handleList({ outputFormat: 'json' }); - expect(emitConsoleLog).toHaveBeenCalledWith('log', '[]'); + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith('log', '[]'); mockCwd.mockRestore(); }); @@ -103,7 +92,7 @@ describe('extensions list command', () => { ); await handleList(); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', 'ext1@1.0.0\n\next2@2.0.0', ); @@ -121,7 +110,7 @@ describe('extensions list command', () => { .mockResolvedValue(extensions); await handleList({ outputFormat: 'json' }); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', JSON.stringify(extensions, null, 2), ); @@ -142,7 +131,7 @@ describe('extensions list command', () => { await handleList(); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'error', 'List failed message', ); diff --git a/packages/cli/src/commands/skills/disable.test.ts b/packages/cli/src/commands/skills/disable.test.ts index 4a5097471b..531a08d21b 100644 --- a/packages/cli/src/commands/skills/disable.test.ts +++ b/packages/cli/src/commands/skills/disable.test.ts @@ -5,7 +5,6 @@ */ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { format } from 'node:util'; import { handleDisable, disableCommand } from './disable.js'; import { loadSettings, @@ -14,12 +13,12 @@ import { type LoadableSettingScope, } from '../../config/settings.js'; -const emitConsoleLog = vi.hoisted(() => vi.fn()); -const debugLogger = vi.hoisted(() => ({ - log: vi.fn((message, ...args) => { - emitConsoleLog('log', format(message, ...args)); - }), -})); +const { emitConsoleLog, debugLogger } = await vi.hoisted(async () => { + const { createMockDebugLogger } = await import( + '../../test-utils/mockDebugLogger.js' + ); + return createMockDebugLogger({ stripAnsi: true }); +}); vi.mock('@google/gemini-cli-core', async (importOriginal) => { const actual = diff --git a/packages/cli/src/commands/skills/enable.test.ts b/packages/cli/src/commands/skills/enable.test.ts index e204da2f66..d34737d2df 100644 --- a/packages/cli/src/commands/skills/enable.test.ts +++ b/packages/cli/src/commands/skills/enable.test.ts @@ -5,7 +5,6 @@ */ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { format } from 'node:util'; import { handleEnable, enableCommand } from './enable.js'; import { loadSettings, @@ -13,12 +12,12 @@ import { type LoadedSettings, } from '../../config/settings.js'; -const emitConsoleLog = vi.hoisted(() => vi.fn()); -const debugLogger = vi.hoisted(() => ({ - log: vi.fn((message, ...args) => { - emitConsoleLog('log', format(message, ...args)); - }), -})); +const { emitConsoleLog, debugLogger } = await vi.hoisted(async () => { + const { createMockDebugLogger } = await import( + '../../test-utils/mockDebugLogger.js' + ); + return createMockDebugLogger({ stripAnsi: true }); +}); vi.mock('@google/gemini-cli-core', async (importOriginal) => { const actual = diff --git a/packages/cli/src/commands/skills/install.test.ts b/packages/cli/src/commands/skills/install.test.ts index 9fd05affcd..faaa7f31c6 100644 --- a/packages/cli/src/commands/skills/install.test.ts +++ b/packages/cli/src/commands/skills/install.test.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { vi, describe, it, expect, beforeEach } from 'vitest'; const mockInstallSkill = vi.hoisted(() => vi.fn()); const mockRequestConsentNonInteractive = vi.hoisted(() => vi.fn()); @@ -19,11 +19,17 @@ vi.mock('../../config/extensions/consent.js', () => ({ skillsConsentString: mockSkillsConsentString, })); +const { debugLogger, emitConsoleLog } = await vi.hoisted(async () => { + const { createMockDebugLogger } = await import( + '../../test-utils/mockDebugLogger.js' + ); + return createMockDebugLogger({ stripAnsi: true }); +}); + vi.mock('@google/gemini-cli-core', () => ({ - debugLogger: { log: vi.fn(), error: vi.fn() }, + debugLogger, })); -import { debugLogger } from '@google/gemini-cli-core'; import { handleInstall, installCommand } from './install.js'; describe('skill install command', () => { @@ -63,10 +69,12 @@ describe('skill install command', () => { expect.any(Function), expect.any(Function), ); - expect(debugLogger.log).toHaveBeenCalledWith( + expect(emitConsoleLog).toHaveBeenCalledWith( + 'log', expect.stringContaining('Successfully installed skill: test-skill'), ); - expect(debugLogger.log).toHaveBeenCalledWith( + expect(emitConsoleLog).toHaveBeenCalledWith( + 'log', expect.stringContaining('location: /mock/user/skills/test-skill'), ); expect(mockRequestConsentNonInteractive).toHaveBeenCalledWith( @@ -86,10 +94,11 @@ describe('skill install command', () => { }); expect(mockRequestConsentNonInteractive).not.toHaveBeenCalled(); - expect(debugLogger.log).toHaveBeenCalledWith( + expect(emitConsoleLog).toHaveBeenCalledWith( + 'log', 'You have consented to the following:', ); - expect(debugLogger.log).toHaveBeenCalledWith('Mock Consent String'); + expect(emitConsoleLog).toHaveBeenCalledWith('log', 'Mock Consent String'); expect(mockInstallSkill).toHaveBeenCalled(); }); @@ -106,7 +115,8 @@ describe('skill install command', () => { source: 'https://example.com/repo.git', }); - expect(debugLogger.error).toHaveBeenCalledWith( + expect(emitConsoleLog).toHaveBeenCalledWith( + 'error', 'Skill installation cancelled by user.', ); expect(process.exit).toHaveBeenCalledWith(1); @@ -137,7 +147,7 @@ describe('skill install command', () => { await handleInstall({ source: '/local/path' }); - expect(debugLogger.error).toHaveBeenCalledWith('Install failed'); + expect(emitConsoleLog).toHaveBeenCalledWith('error', 'Install failed'); expect(process.exit).toHaveBeenCalledWith(1); }); }); diff --git a/packages/cli/src/commands/skills/link.test.ts b/packages/cli/src/commands/skills/link.test.ts index 404c1d9f66..24c3d3ff64 100644 --- a/packages/cli/src/commands/skills/link.test.ts +++ b/packages/cli/src/commands/skills/link.test.ts @@ -15,8 +15,15 @@ vi.mock('../../utils/skillUtils.js', () => ({ linkSkill: mockLinkSkill, })); +const { debugLogger } = await vi.hoisted(async () => { + const { createMockDebugLogger } = await import( + '../../test-utils/mockDebugLogger.js' + ); + return createMockDebugLogger({ stripAnsi: false }); +}); + vi.mock('@google/gemini-cli-core', () => ({ - debugLogger: { log: vi.fn(), error: vi.fn() }, + debugLogger, })); vi.mock('../../config/extensions/consent.js', () => ({ @@ -24,8 +31,6 @@ vi.mock('../../config/extensions/consent.js', () => ({ skillsConsentString: mockSkillsConsentString, })); -import { debugLogger } from '@google/gemini-cli-core'; - describe('skills link command', () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/packages/cli/src/commands/skills/list.test.ts b/packages/cli/src/commands/skills/list.test.ts index e7e25a2736..c330af75ba 100644 --- a/packages/cli/src/commands/skills/list.test.ts +++ b/packages/cli/src/commands/skills/list.test.ts @@ -5,33 +5,23 @@ */ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { format } from 'node:util'; +import { coreEvents } from '@google/gemini-cli-core'; import { handleList, listCommand } from './list.js'; import { loadSettings, type LoadedSettings } from '../../config/settings.js'; import { loadCliConfig } from '../../config/config.js'; import type { Config } from '@google/gemini-cli-core'; import chalk from 'chalk'; -const emitConsoleLog = vi.hoisted(() => vi.fn()); -const debugLogger = vi.hoisted(() => ({ - log: vi.fn((message, ...args) => { - emitConsoleLog('log', format(message, ...args)); - }), - error: vi.fn((message, ...args) => { - emitConsoleLog('error', format(message, ...args)); - }), -})); - vi.mock('@google/gemini-cli-core', async (importOriginal) => { - const actual = - await importOriginal(); - return { - ...actual, - coreEvents: { - emitConsoleLog, + const { mockCoreDebugLogger } = await import( + '../../test-utils/mockDebugLogger.js' + ); + return mockCoreDebugLogger( + await importOriginal(), + { + stripAnsi: false, }, - debugLogger, - }; + ); }); vi.mock('../../config/settings.js'); @@ -67,7 +57,7 @@ describe('skills list command', () => { await handleList({}); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', 'No skills discovered.', ); @@ -98,23 +88,23 @@ describe('skills list command', () => { await handleList({}); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', chalk.bold('Discovered Agent Skills:'), ); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', expect.stringContaining('skill1'), ); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', expect.stringContaining(chalk.green('[Enabled]')), ); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', expect.stringContaining('skill2'), ); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', expect.stringContaining(chalk.red('[Disabled]')), ); @@ -146,11 +136,11 @@ describe('skills list command', () => { // Default await handleList({ all: false }); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', expect.stringContaining('regular'), ); - expect(emitConsoleLog).not.toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).not.toHaveBeenCalledWith( 'log', expect.stringContaining('builtin'), ); @@ -159,15 +149,15 @@ describe('skills list command', () => { // With all: true await handleList({ all: true }); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', expect.stringContaining('regular'), ); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', expect.stringContaining('builtin'), ); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', expect.stringContaining(chalk.gray(' [Built-in]')), ); diff --git a/packages/cli/src/commands/skills/uninstall.test.ts b/packages/cli/src/commands/skills/uninstall.test.ts index 74f1730590..ab51db5b53 100644 --- a/packages/cli/src/commands/skills/uninstall.test.ts +++ b/packages/cli/src/commands/skills/uninstall.test.ts @@ -12,11 +12,17 @@ vi.mock('../../utils/skillUtils.js', () => ({ uninstallSkill: mockUninstallSkill, })); +const { debugLogger, emitConsoleLog } = await vi.hoisted(async () => { + const { createMockDebugLogger } = await import( + '../../test-utils/mockDebugLogger.js' + ); + return createMockDebugLogger({ stripAnsi: true }); +}); + vi.mock('@google/gemini-cli-core', () => ({ - debugLogger: { log: vi.fn(), error: vi.fn() }, + debugLogger, })); -import { debugLogger } from '@google/gemini-cli-core'; import { handleUninstall, uninstallCommand } from './uninstall.js'; describe('skill uninstall command', () => { @@ -45,10 +51,12 @@ describe('skill uninstall command', () => { }); expect(mockUninstallSkill).toHaveBeenCalledWith('test-skill', 'user'); - expect(debugLogger.log).toHaveBeenCalledWith( + expect(emitConsoleLog).toHaveBeenCalledWith( + 'log', expect.stringContaining('Successfully uninstalled skill: test-skill'), ); - expect(debugLogger.log).toHaveBeenCalledWith( + expect(emitConsoleLog).toHaveBeenCalledWith( + 'log', expect.stringContaining('location: /mock/user/skills/test-skill'), ); }); @@ -71,7 +79,8 @@ describe('skill uninstall command', () => { await handleUninstall({ name: 'test-skill' }); - expect(debugLogger.error).toHaveBeenCalledWith( + expect(emitConsoleLog).toHaveBeenCalledWith( + 'error', 'Skill "test-skill" is not installed in the user scope.', ); }); @@ -81,7 +90,7 @@ describe('skill uninstall command', () => { await handleUninstall({ name: 'test-skill' }); - expect(debugLogger.error).toHaveBeenCalledWith('Uninstall failed'); + expect(emitConsoleLog).toHaveBeenCalledWith('error', 'Uninstall failed'); expect(process.exit).toHaveBeenCalledWith(1); }); }); diff --git a/packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-generate-a-consent-string-with-all-fields.snap.svg b/packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-generate-a-consent-string-with-all-fields.snap.svg new file mode 100644 index 0000000000..d42af4490c --- /dev/null +++ b/packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-generate-a-consent-string-with-all-fields.snap.svg @@ -0,0 +1,18 @@ + + + + + Installing extension "test-ext". + This extension will run the following MCP servers: + * server1 (local): npm start + * server2 (remote): https://remote.com + This extension will append info to your gemini.md context using my-context.md + This extension will exclude the following core tools: tool1,tool2 + The extension you are about to install may have been created by a third-party developer and sourced + from a public repository. Google does not vet, endorse, or guarantee the functionality or security + of extensions. Please carefully inspect any extension and its source code before installing to + understand the permissions it requires and the actions it may perform. + + \ No newline at end of file diff --git a/packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-include-warning-when-hooks-are-present.snap.svg b/packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-include-warning-when-hooks-are-present.snap.svg new file mode 100644 index 0000000000..9f4866dbdd --- /dev/null +++ b/packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-include-warning-when-hooks-are-present.snap.svg @@ -0,0 +1,14 @@ + + + + + Installing extension "test-ext". + ⚠️ This extension contains Hooks which can automatically execute commands. + The extension you are about to install may have been created by a third-party developer and sourced + from a public repository. Google does not vet, endorse, or guarantee the functionality or security + of extensions. Please carefully inspect any extension and its source code before installing to + understand the permissions it requires and the actions it may perform. + + \ No newline at end of file diff --git a/packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-request-consent-if-skills-change.snap.svg b/packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-request-consent-if-skills-change.snap.svg new file mode 100644 index 0000000000..6f5879df4c --- /dev/null +++ b/packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-request-consent-if-skills-change.snap.svg @@ -0,0 +1,28 @@ + + + + + Installing extension "test-ext". + This extension will run the following MCP servers: + * server1 (local): npm start + * server2 (remote): https://remote.com + This extension will append info to your gemini.md context using my-context.md + This extension will exclude the following core tools: tool1,tool2 + Agent Skills: + This extension will install the following agent skills: + * skill1: desc1 + (Source: /mock/temp/dir/skill1/SKILL.md) (2 items in directory) + * skill2: desc2 + (Source: /mock/temp/dir/skill2/SKILL.md) (1 items in directory) + The extension you are about to install may have been created by a third-party developer and sourced + from a public repository. Google does not vet, endorse, or guarantee the functionality or security + of extensions. Please carefully inspect any extension and its source code before installing to + understand the permissions it requires and the actions it may perform. + Agent skills inject specialized instructions and domain-specific knowledge into the agent's system + prompt. This can change how the agent interprets your requests and interacts with your environment. + Review the skill definitions at the location(s) provided below to ensure they meet your security + standards. + + \ No newline at end of file diff --git a/packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-show-a-warning-if-the-skill-directory-cannot-be-read.snap.svg b/packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-show-a-warning-if-the-skill-directory-cannot-be-read.snap.svg new file mode 100644 index 0000000000..3fff32664a --- /dev/null +++ b/packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-show-a-warning-if-the-skill-directory-cannot-be-read.snap.svg @@ -0,0 +1,22 @@ + + + + + Installing extension "test-ext". + Agent Skills: + This extension will install the following agent skills: + * locked-skill: A skill in a locked dir + (Source: /mock/temp/dir/locked/SKILL.md) + ⚠️ (Could not count items in directory) + The extension you are about to install may have been created by a third-party developer and sourced + from a public repository. Google does not vet, endorse, or guarantee the functionality or security + of extensions. Please carefully inspect any extension and its source code before installing to + understand the permissions it requires and the actions it may perform. + Agent skills inject specialized instructions and domain-specific knowledge into the agent's system + prompt. This can change how the agent interprets your requests and interacts with your environment. + Review the skill definitions at the location(s) provided below to ensure they meet your security + standards. + + \ No newline at end of file diff --git a/packages/cli/src/config/extensions/__snapshots__/consent-consent-skillsConsentString-should-generate-a-consent-string-for-skills.snap.svg b/packages/cli/src/config/extensions/__snapshots__/consent-consent-skillsConsentString-should-generate-a-consent-string-for-skills.snap.svg new file mode 100644 index 0000000000..c52724836e --- /dev/null +++ b/packages/cli/src/config/extensions/__snapshots__/consent-consent-skillsConsentString-should-generate-a-consent-string-for-skills.snap.svg @@ -0,0 +1,17 @@ + + + + + Installing agent skill(s) from "https://example.com/repo.git". + The following agent skill(s) will be installing: + * skill1: desc1 + (Source: /mock/temp/dir/skill1/SKILL.md) (1 items in directory) + Install Destination: /mock/target/dir + Agent skills inject specialized instructions and domain-specific knowledge into the agent's system + prompt. This can change how the agent interprets your requests and interacts with your environment. + Review the skill definitions at the location(s) provided below to ensure they meet your security + standards. + + \ No newline at end of file diff --git a/packages/cli/src/config/extensions/__snapshots__/consent.test.ts.snap b/packages/cli/src/config/extensions/__snapshots__/consent.test.ts.snap new file mode 100644 index 0000000000..d8fe99d004 --- /dev/null +++ b/packages/cli/src/config/extensions/__snapshots__/consent.test.ts.snap @@ -0,0 +1,93 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`consent > maybeRequestConsentOrFail > consent string generation > should generate a consent string with all fields 1`] = ` +"Installing extension "test-ext". +This extension will run the following MCP servers: + * server1 (local): npm start + * server2 (remote): https://remote.com +This extension will append info to your gemini.md context using my-context.md +This extension will exclude the following core tools: tool1,tool2 + +The extension you are about to install may have been created by a third-party developer and sourced +from a public repository. Google does not vet, endorse, or guarantee the functionality or security +of extensions. Please carefully inspect any extension and its source code before installing to +understand the permissions it requires and the actions it may perform." +`; + +exports[`consent > maybeRequestConsentOrFail > consent string generation > should include warning when hooks are present 1`] = ` +"Installing extension "test-ext". +⚠️ This extension contains Hooks which can automatically execute commands. + +The extension you are about to install may have been created by a third-party developer and sourced +from a public repository. Google does not vet, endorse, or guarantee the functionality or security +of extensions. Please carefully inspect any extension and its source code before installing to +understand the permissions it requires and the actions it may perform." +`; + +exports[`consent > maybeRequestConsentOrFail > consent string generation > should request consent if skills change 1`] = ` +"Installing extension "test-ext". +This extension will run the following MCP servers: + * server1 (local): npm start + * server2 (remote): https://remote.com +This extension will append info to your gemini.md context using my-context.md +This extension will exclude the following core tools: tool1,tool2 + +Agent Skills: + +This extension will install the following agent skills: + + * skill1: desc1 + (Source: /mock/temp/dir/skill1/SKILL.md) (2 items in directory) + + * skill2: desc2 + (Source: /mock/temp/dir/skill2/SKILL.md) (1 items in directory) + + +The extension you are about to install may have been created by a third-party developer and sourced +from a public repository. Google does not vet, endorse, or guarantee the functionality or security +of extensions. Please carefully inspect any extension and its source code before installing to +understand the permissions it requires and the actions it may perform. + +Agent skills inject specialized instructions and domain-specific knowledge into the agent's system +prompt. This can change how the agent interprets your requests and interacts with your environment. +Review the skill definitions at the location(s) provided below to ensure they meet your security +standards." +`; + +exports[`consent > maybeRequestConsentOrFail > consent string generation > should show a warning if the skill directory cannot be read 1`] = ` +"Installing extension "test-ext". + +Agent Skills: + +This extension will install the following agent skills: + + * locked-skill: A skill in a locked dir + (Source: /mock/temp/dir/locked/SKILL.md) ⚠️ (Could not count items in directory) + + +The extension you are about to install may have been created by a third-party developer and sourced +from a public repository. Google does not vet, endorse, or guarantee the functionality or security +of extensions. Please carefully inspect any extension and its source code before installing to +understand the permissions it requires and the actions it may perform. + +Agent skills inject specialized instructions and domain-specific knowledge into the agent's system +prompt. This can change how the agent interprets your requests and interacts with your environment. +Review the skill definitions at the location(s) provided below to ensure they meet your security +standards." +`; + +exports[`consent > skillsConsentString > should generate a consent string for skills 1`] = ` +"Installing agent skill(s) from "https://example.com/repo.git". + +The following agent skill(s) will be installing: + + * skill1: desc1 + (Source: /mock/temp/dir/skill1/SKILL.md) (1 items in directory) + +Install Destination: /mock/target/dir + +Agent skills inject specialized instructions and domain-specific knowledge into the agent's system +prompt. This can change how the agent interprets your requests and interacts with your environment. +Review the skill definitions at the location(s) provided below to ensure they meet your security +standards." +`; diff --git a/packages/cli/src/config/extensions/consent.test.ts b/packages/cli/src/config/extensions/consent.test.ts index a7c07413b4..04e6cae69f 100644 --- a/packages/cli/src/config/extensions/consent.test.ts +++ b/packages/cli/src/config/extensions/consent.test.ts @@ -4,17 +4,17 @@ * SPDX-License-Identifier: Apache-2.0 */ +import React from 'react'; +import { Text } from 'ink'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import chalk from 'chalk'; import * as fs from 'node:fs/promises'; import * as path from 'node:path'; import * as os from 'node:os'; +import { render, cleanup } from '../../test-utils/render.js'; import { requestConsentNonInteractive, requestConsentInteractive, maybeRequestConsentOrFail, - INSTALL_WARNING_MESSAGE, - SKILLS_WARNING_MESSAGE, } from './consent.js'; import type { ConfirmationRequest } from '../../ui/types.js'; import type { ExtensionConfig } from '../extension.js'; @@ -58,6 +58,21 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { }; }); +async function expectConsentSnapshot(consentString: string) { + const renderResult = render(React.createElement(Text, null, consentString)); + await renderResult.waitUntilReady(); + await expect(renderResult).toMatchSvgSnapshot(); +} + +/** + * Normalizes a consent string for snapshot testing by: + * 1. Replacing the dynamic temp directory path with a static placeholder. + * 2. Converting Windows backslashes to forward slashes for platform-agnosticism. + */ +function normalizePathsForSnapshot(str: string, tempDir: string): string { + return str.replaceAll(tempDir, '/mock/temp/dir').replaceAll('\\', '/'); +} + describe('consent', () => { let tempDir: string; @@ -75,6 +90,7 @@ describe('consent', () => { if (tempDir) { await fs.rm(tempDir, { recursive: true, force: true }); } + cleanup(); }); describe('requestConsentNonInteractive', () => { @@ -189,18 +205,9 @@ describe('consent', () => { undefined, ); - const expectedConsentString = [ - 'Installing extension "test-ext".', - 'This extension will run the following MCP servers:', - ' * server1 (local): npm start', - ' * server2 (remote): https://remote.com', - 'This extension will append info to your gemini.md context using my-context.md', - 'This extension will exclude the following core tools: tool1,tool2', - '', - INSTALL_WARNING_MESSAGE, - ].join('\n'); - - expect(requestConsent).toHaveBeenCalledWith(expectedConsentString); + expect(requestConsent).toHaveBeenCalledTimes(1); + const consentString = requestConsent.mock.calls[0][0] as string; + await expectConsentSnapshot(consentString); }); it('should request consent if mcpServers change', async () => { @@ -263,11 +270,9 @@ describe('consent', () => { undefined, ); - expect(requestConsent).toHaveBeenCalledWith( - expect.stringContaining( - '⚠️ This extension contains Hooks which can automatically execute commands.', - ), - ); + expect(requestConsent).toHaveBeenCalledTimes(1); + const consentString = requestConsent.mock.calls[0][0] as string; + await expectConsentSnapshot(consentString); }); it('should request consent if hooks status changes', async () => { @@ -323,29 +328,10 @@ describe('consent', () => { [skill1, skill2], ); - const expectedConsentString = [ - 'Installing extension "test-ext".', - 'This extension will run the following MCP servers:', - ' * server1 (local): npm start', - ' * server2 (remote): https://remote.com', - 'This extension will append info to your gemini.md context using my-context.md', - 'This extension will exclude the following core tools: tool1,tool2', - '', - chalk.bold('Agent Skills:'), - '\nThis extension will install the following agent skills:\n', - ` * ${chalk.bold('skill1')}: desc1`, - chalk.dim(` (Source: ${skill1.location}) (2 items in directory)`), - '', - ` * ${chalk.bold('skill2')}: desc2`, - chalk.dim(` (Source: ${skill2.location}) (1 items in directory)`), - '', - '', - INSTALL_WARNING_MESSAGE, - '', - SKILLS_WARNING_MESSAGE, - ].join('\n'); - - expect(requestConsent).toHaveBeenCalledWith(expectedConsentString); + expect(requestConsent).toHaveBeenCalledTimes(1); + let consentString = requestConsent.mock.calls[0][0] as string; + consentString = normalizePathsForSnapshot(consentString, tempDir); + await expectConsentSnapshot(consentString); }); it('should show a warning if the skill directory cannot be read', async () => { @@ -377,11 +363,10 @@ describe('consent', () => { [skill], ); - expect(requestConsent).toHaveBeenCalledWith( - expect.stringContaining( - ` (Source: ${skill.location}) ${chalk.red('⚠️ (Could not count items in directory)')}`, - ), - ); + expect(requestConsent).toHaveBeenCalledTimes(1); + let consentString = requestConsent.mock.calls[0][0] as string; + consentString = normalizePathsForSnapshot(consentString, tempDir); + await expectConsentSnapshot(consentString); }); }); }); @@ -400,21 +385,14 @@ describe('consent', () => { }; const { skillsConsentString } = await import('./consent.js'); - const consentString = await skillsConsentString( + let consentString = await skillsConsentString( [skill1], 'https://example.com/repo.git', '/mock/target/dir', ); - expect(consentString).toContain( - 'Installing agent skill(s) from "https://example.com/repo.git".', - ); - expect(consentString).toContain('Install Destination: /mock/target/dir'); - expect(consentString).toContain('\n' + SKILLS_WARNING_MESSAGE); - expect(consentString).toContain(` * ${chalk.bold('skill1')}: desc1`); - expect(consentString).toContain( - chalk.dim(`(Source: ${skill1.location}) (1 items in directory)`), - ); + consentString = normalizePathsForSnapshot(consentString, tempDir); + await expectConsentSnapshot(consentString); }); }); }); diff --git a/packages/cli/src/test-utils/customMatchers.ts b/packages/cli/src/test-utils/customMatchers.ts index 0259c064a6..ae9b44ee44 100644 --- a/packages/cli/src/test-utils/customMatchers.ts +++ b/packages/cli/src/test-utils/customMatchers.ts @@ -6,20 +6,78 @@ /// -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import type { Assertion } from 'vitest'; -import { expect } from 'vitest'; +import { expect, type Assertion } from 'vitest'; +import path from 'node:path'; +import stripAnsi from 'strip-ansi'; import type { TextBuffer } from '../ui/components/shared/text-buffer.js'; // RegExp to detect invalid characters: backspace, and ANSI escape codes // eslint-disable-next-line no-control-regex const invalidCharsRegex = /[\b\x1b]/; +const callCountByTest = new Map(); + +export async function toMatchSvgSnapshot( + this: Assertion, + renderInstance: { + lastFrameRaw?: (options?: { allowEmpty?: boolean }) => string; + lastFrame?: (options?: { allowEmpty?: boolean }) => string; + generateSvg: () => string; + }, + options?: { allowEmpty?: boolean; name?: string }, +) { + const currentTestName = expect.getState().currentTestName; + if (!currentTestName) { + throw new Error('toMatchSvgSnapshot must be called within a test'); + } + const testPath = expect.getState().testPath; + if (!testPath) { + throw new Error('toMatchSvgSnapshot requires testPath'); + } + + let textContent: string; + if (renderInstance.lastFrameRaw) { + textContent = renderInstance.lastFrameRaw({ + allowEmpty: options?.allowEmpty, + }); + } else if (renderInstance.lastFrame) { + textContent = renderInstance.lastFrame({ allowEmpty: options?.allowEmpty }); + } else { + throw new Error( + 'toMatchSvgSnapshot requires a renderInstance with either lastFrameRaw or lastFrame', + ); + } + const svgContent = renderInstance.generateSvg(); + + const sanitize = (name: string) => + name.replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-'); + + const testId = testPath + ':' + currentTestName; + let count = callCountByTest.get(testId) ?? 0; + count++; + callCountByTest.set(testId, count); + + const snapshotName = + options?.name ?? + (count > 1 ? `${currentTestName}-${count}` : currentTestName); + + const svgFileName = + sanitize(path.basename(testPath).replace(/\.test\.tsx?$/, '')) + + '-' + + sanitize(snapshotName) + + '.snap.svg'; + const svgDir = path.join(path.dirname(testPath), '__snapshots__'); + const svgFilePath = path.join(svgDir, svgFileName); + + // Assert the text matches standard snapshot, stripping ANSI for stability + expect(stripAnsi(textContent)).toMatchSnapshot(); + + // Assert the SVG matches the file snapshot + await expect(svgContent).toMatchFileSnapshot(svgFilePath); + + return { pass: true, message: () => '' }; +} + function toHaveOnlyValidCharacters(this: Assertion, buffer: TextBuffer) { // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment const { isNot } = this as any; @@ -53,15 +111,22 @@ function toHaveOnlyValidCharacters(this: Assertion, buffer: TextBuffer) { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion expect.extend({ toHaveOnlyValidCharacters, + toMatchSvgSnapshot, // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); // Extend Vitest's `expect` interface with the custom matcher's type definition. declare module 'vitest' { - interface Assertion { + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-empty-object-type + interface Assertion extends CustomMatchers {} + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + interface AsymmetricMatchersContaining extends CustomMatchers {} + + interface CustomMatchers { toHaveOnlyValidCharacters(): T; - } - interface AsymmetricMatchersContaining { - toHaveOnlyValidCharacters(): void; + toMatchSvgSnapshot(options?: { + allowEmpty?: boolean; + name?: string; + }): Promise; } } diff --git a/packages/cli/src/test-utils/mockDebugLogger.ts b/packages/cli/src/test-utils/mockDebugLogger.ts new file mode 100644 index 0000000000..02eb3b05d9 --- /dev/null +++ b/packages/cli/src/test-utils/mockDebugLogger.ts @@ -0,0 +1,76 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { vi } from 'vitest'; +import stripAnsi from 'strip-ansi'; +import { format } from 'node:util'; + +export function createMockDebugLogger(options: { stripAnsi?: boolean } = {}) { + const emitConsoleLog = vi.fn(); + const debugLogger = { + log: vi.fn((message: unknown, ...args: unknown[]) => { + let formatted = + typeof message === 'string' ? format(message, ...args) : message; + if (options.stripAnsi && typeof formatted === 'string') { + formatted = stripAnsi(formatted); + } + emitConsoleLog('log', formatted); + }), + error: vi.fn((message: unknown, ...args: unknown[]) => { + let formatted = + typeof message === 'string' ? format(message, ...args) : message; + if (options.stripAnsi && typeof formatted === 'string') { + formatted = stripAnsi(formatted); + } + emitConsoleLog('error', formatted); + }), + warn: vi.fn((message: unknown, ...args: unknown[]) => { + let formatted = + typeof message === 'string' ? format(message, ...args) : message; + if (options.stripAnsi && typeof formatted === 'string') { + formatted = stripAnsi(formatted); + } + emitConsoleLog('warn', formatted); + }), + debug: vi.fn(), + info: vi.fn(), + }; + + return { emitConsoleLog, debugLogger }; +} + +/** + * A helper specifically designed for `vi.mock('@google/gemini-cli-core', ...)` to easily + * mock both `debugLogger` and `coreEvents.emitConsoleLog`. + * + * Example: + * ```typescript + * vi.mock('@google/gemini-cli-core', async (importOriginal) => { + * const { mockCoreDebugLogger } = await import('../../test-utils/mockDebugLogger.js'); + * return mockCoreDebugLogger( + * await importOriginal(), + * { stripAnsi: true } + * ); + * }); + * ``` + */ +export function mockCoreDebugLogger>( + actual: T, + options?: { stripAnsi?: boolean }, +): T { + const { emitConsoleLog, debugLogger } = createMockDebugLogger(options); + return { + ...actual, + coreEvents: { + ...(typeof actual['coreEvents'] === 'object' && + actual['coreEvents'] !== null + ? actual['coreEvents'] + : {}), + emitConsoleLog, + }, + debugLogger, + } as T; +} diff --git a/packages/cli/src/test-utils/render.tsx b/packages/cli/src/test-utils/render.tsx index 73ec9af2d3..455a84b8e0 100644 --- a/packages/cli/src/test-utils/render.tsx +++ b/packages/cli/src/test-utils/render.tsx @@ -51,6 +51,7 @@ import { SessionStatsProvider } from '../ui/contexts/SessionContext.js'; import { themeManager, DEFAULT_THEME } from '../ui/themes/theme-manager.js'; import { DefaultLight } from '../ui/themes/default-light.js'; import { pickDefaultThemeName } from '../ui/themes/theme.js'; +import { generateSvgForTerminal } from './svg.js'; export const persistentStateMock = new FakePersistentState(); @@ -105,7 +106,12 @@ class XtermStdout extends EventEmitter { private queue: { promise: Promise }; isTTY = true; + getColorDepth(): number { + return 24; + } + private lastRenderOutput: string | undefined = undefined; + private lastRenderStaticContent: string | undefined = undefined; constructor(state: TerminalState, queue: { promise: Promise }) { super(); @@ -138,6 +144,7 @@ class XtermStdout extends EventEmitter { clear = () => { this.state.terminal.reset(); this.lastRenderOutput = undefined; + this.lastRenderStaticContent = undefined; }; dispose = () => { @@ -146,10 +153,32 @@ class XtermStdout extends EventEmitter { onRender = (staticContent: string, output: string) => { this.renderCount++; + this.lastRenderStaticContent = staticContent; this.lastRenderOutput = output; this.emit('render'); }; + private normalizeFrame = (text: string): string => + text.replace(/\r\n/g, '\n'); + + generateSvg = (): string => generateSvgForTerminal(this.state.terminal); + + lastFrameRaw = (options: { allowEmpty?: boolean } = {}) => { + const result = + (this.lastRenderStaticContent ?? '') + (this.lastRenderOutput ?? ''); + + const normalized = this.normalizeFrame(result); + + if (normalized === '' && !options.allowEmpty) { + throw new Error( + 'lastFrameRaw() returned an empty string. If this is intentional, use lastFrameRaw({ allowEmpty: true }). ' + + 'Otherwise, ensure you are calling await waitUntilReady() and that the component is rendering correctly.', + ); + } + + return normalized; + }; + lastFrame = (options: { allowEmpty?: boolean } = {}) => { const buffer = this.state.terminal.buffer.active; const allLines: string[] = []; @@ -163,9 +192,7 @@ class XtermStdout extends EventEmitter { } const result = trimmed.join('\n'); - // Normalize for cross-platform snapshot stability: - // Normalize any \r\n to \n - const normalized = result.replace(/\r\n/g, '\n'); + const normalized = this.normalizeFrame(result); if (normalized === '' && !options.allowEmpty) { throw new Error( @@ -213,9 +240,11 @@ class XtermStdout extends EventEmitter { const currentFrame = stripAnsi( this.lastFrame({ allowEmpty: true }), ).trim(); - const expectedFrame = stripAnsi(this.lastRenderOutput ?? '') - .trim() - .replace(/\r\n/g, '\n'); + const expectedFrame = this.normalizeFrame( + stripAnsi( + (this.lastRenderStaticContent ?? '') + (this.lastRenderOutput ?? ''), + ), + ).trim(); lastCurrent = currentFrame; lastExpected = expectedFrame; @@ -340,6 +369,8 @@ export type RenderInstance = { stdin: XtermStdin; frames: string[]; lastFrame: (options?: { allowEmpty?: boolean }) => string; + lastFrameRaw: (options?: { allowEmpty?: boolean }) => string; + generateSvg: () => string; terminal: Terminal; waitUntilReady: () => Promise; capturedOverflowState: OverflowState | undefined; @@ -424,6 +455,8 @@ export const render = ( stdin, frames: stdout.frames, lastFrame: stdout.lastFrame, + lastFrameRaw: stdout.lastFrameRaw, + generateSvg: stdout.generateSvg, terminal: state.terminal, waitUntilReady: () => stdout.waitUntilReady(), }; @@ -767,6 +800,7 @@ export function renderHook( rerender: (props?: Props) => void; unmount: () => void; waitUntilReady: () => Promise; + generateSvg: () => string; } { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const result = { current: undefined as unknown as Result }; @@ -789,6 +823,7 @@ export function renderHook( let inkRerender: (tree: React.ReactElement) => void = () => {}; let unmount: () => void = () => {}; let waitUntilReady: () => Promise = async () => {}; + let generateSvg: () => string = () => ''; act(() => { const renderResult = render( @@ -799,6 +834,7 @@ export function renderHook( inkRerender = renderResult.rerender; unmount = renderResult.unmount; waitUntilReady = renderResult.waitUntilReady; + generateSvg = renderResult.generateSvg; }); function rerender(props?: Props) { @@ -815,7 +851,7 @@ export function renderHook( }); } - return { result, rerender, unmount, waitUntilReady }; + return { result, rerender, unmount, waitUntilReady, generateSvg }; } export function renderHookWithProviders( @@ -837,6 +873,7 @@ export function renderHookWithProviders( rerender: (props?: Props) => void; unmount: () => void; waitUntilReady: () => Promise; + generateSvg: () => string; } { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const result = { current: undefined as unknown as Result }; @@ -887,5 +924,6 @@ export function renderHookWithProviders( }); }, waitUntilReady: () => renderResult.waitUntilReady(), + generateSvg: () => renderResult.generateSvg(), }; } diff --git a/packages/cli/src/test-utils/svg.ts b/packages/cli/src/test-utils/svg.ts new file mode 100644 index 0000000000..10528ca6b7 --- /dev/null +++ b/packages/cli/src/test-utils/svg.ts @@ -0,0 +1,190 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { Terminal } from '@xterm/headless'; + +export const generateSvgForTerminal = (terminal: Terminal): string => { + const activeBuffer = terminal.buffer.active; + + const getHexColor = ( + isRGB: boolean, + isPalette: boolean, + isDefault: boolean, + colorCode: number, + ): string | null => { + if (isDefault) return null; + if (isRGB) { + return `#${colorCode.toString(16).padStart(6, '0')}`; + } + if (isPalette) { + if (colorCode >= 0 && colorCode <= 15) { + return ( + [ + '#000000', + '#cd0000', + '#00cd00', + '#cdcd00', + '#0000ee', + '#cd00cd', + '#00cdcd', + '#e5e5e5', + '#7f7f7f', + '#ff0000', + '#00ff00', + '#ffff00', + '#5c5cff', + '#ff00ff', + '#00ffff', + '#ffffff', + ][colorCode] || null + ); + } else if (colorCode >= 16 && colorCode <= 231) { + const v = [0, 95, 135, 175, 215, 255]; + const c = colorCode - 16; + const b = v[c % 6]; + const g = v[Math.floor(c / 6) % 6]; + const r = v[Math.floor(c / 36) % 6]; + return `#${[r, g, b].map((x) => x?.toString(16).padStart(2, '0')).join('')}`; + } else if (colorCode >= 232 && colorCode <= 255) { + const gray = 8 + (colorCode - 232) * 10; + const hex = gray.toString(16).padStart(2, '0'); + return `#${hex}${hex}${hex}`; + } + } + return null; + }; + + const escapeXml = (unsafe: string): string => + // eslint-disable-next-line no-control-regex + unsafe.replace(/[<>&'"\x00-\x08\x0B-\x0C\x0E-\x1F]/g, (c) => { + switch (c) { + case '<': + return '<'; + case '>': + return '>'; + case '&': + return '&'; + case "'": + return '''; + case '"': + return '"'; + default: + return ''; + } + }); + + const charWidth = 9; + const charHeight = 17; + const padding = 10; + + // Find the actual number of rows with content to avoid rendering trailing blank space. + let contentRows = terminal.rows; + for (let y = terminal.rows - 1; y >= 0; y--) { + const line = activeBuffer.getLine(y); + if (line && line.translateToString(true).trim().length > 0) { + contentRows = y + 1; + break; + } + } + if (contentRows === 0) contentRows = 1; // Minimum 1 row + + const width = terminal.cols * charWidth + padding * 2; + const height = contentRows * charHeight + padding * 2; + + let svg = ` +`; + svg += ` +`; + svg += ` +`; // Terminal background + svg += ` +`; + + for (let y = 0; y < contentRows; y++) { + const line = activeBuffer.getLine(y); + if (!line) continue; + + let currentFgHex: string | null = null; + let currentBgHex: string | null = null; + let currentBlockStartCol = -1; + let currentBlockText = ''; + let currentBlockNumCells = 0; + + const finalizeBlock = (_endCol: number) => { + if (currentBlockStartCol !== -1) { + if (currentBlockText.length > 0) { + const xPos = currentBlockStartCol * charWidth; + const yPos = y * charHeight; + + if (currentBgHex) { + const rectWidth = currentBlockNumCells * charWidth; + svg += ` +`; + } + if (currentBlockText.trim().length > 0) { + const fill = currentFgHex || '#ffffff'; // Default text color + const textWidth = currentBlockNumCells * charWidth; + // Use textLength to ensure the block fits exactly into its designated cells + svg += ` ${escapeXml(currentBlockText)} +`; + } + } + } + }; + + for (let x = 0; x < line.length; x++) { + const cell = line.getCell(x); + if (!cell) continue; + const cellWidth = cell.getWidth(); + if (cellWidth === 0) continue; // Skip continuation cells of wide characters + + let fgHex = getHexColor( + cell.isFgRGB(), + cell.isFgPalette(), + cell.isFgDefault(), + cell.getFgColor(), + ); + let bgHex = getHexColor( + cell.isBgRGB(), + cell.isBgPalette(), + cell.isBgDefault(), + cell.getBgColor(), + ); + + if (cell.isInverse()) { + const tempFgHex = fgHex; + fgHex = bgHex || '#000000'; + bgHex = tempFgHex || '#ffffff'; + } + + let chars = cell.getChars(); + if (chars === '') chars = ' '.repeat(cellWidth); + + if ( + fgHex !== currentFgHex || + bgHex !== currentBgHex || + currentBlockStartCol === -1 + ) { + finalizeBlock(x); + currentFgHex = fgHex; + currentBgHex = bgHex; + currentBlockStartCol = x; + currentBlockText = chars; + currentBlockNumCells = cellWidth; + } else { + currentBlockText += chars; + currentBlockNumCells += cellWidth; + } + } + finalizeBlock(line.length); + } + svg += ` \n`; + return svg; +}; diff --git a/packages/cli/src/ui/__snapshots__/App.test.tsx.snap b/packages/cli/src/ui/__snapshots__/App.test.tsx.snap index d95adcda95..450da8362e 100644 --- a/packages/cli/src/ui/__snapshots__/App.test.tsx.snap +++ b/packages/cli/src/ui/__snapshots__/App.test.tsx.snap @@ -2,14 +2,14 @@ exports[`App > Snapshots > renders default layout correctly 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ Tips for getting started: 1. Ask questions, edit files, or run commands. @@ -47,14 +47,14 @@ exports[`App > Snapshots > renders screen reader layout correctly 1`] = ` "Notifications Footer - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ Tips for getting started: 1. Ask questions, edit files, or run commands. @@ -67,14 +67,14 @@ Composer exports[`App > Snapshots > renders with dialogs visible 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ @@ -110,14 +110,14 @@ DialogManager exports[`App > should render ToolConfirmationQueue along with Composer when tool is confirming and experiment is on 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ Tips for getting started: 1. Ask questions, edit files, or run commands. diff --git a/packages/cli/src/ui/components/AnsiOutput.test.tsx b/packages/cli/src/ui/components/AnsiOutput.test.tsx index 770eb9b056..ac824fefe6 100644 --- a/packages/cli/src/ui/components/AnsiOutput.test.tsx +++ b/packages/cli/src/ui/components/AnsiOutput.test.tsx @@ -33,7 +33,7 @@ describe('', () => { , ); await waitUntilReady(); - expect(lastFrame()).toBe('Hello, world!\n'); + expect(lastFrame().trim()).toBe('Hello, world!'); unmount(); }); @@ -51,7 +51,7 @@ describe('', () => { , ); await waitUntilReady(); - expect(lastFrame()).toBe(text + '\n'); + expect(lastFrame().trim()).toBe(text); unmount(); }); @@ -65,7 +65,7 @@ describe('', () => { , ); await waitUntilReady(); - expect(lastFrame()).toBe(text + '\n'); + expect(lastFrame().trim()).toBe(text); unmount(); }); diff --git a/packages/cli/src/ui/components/AskUserDialog.test.tsx b/packages/cli/src/ui/components/AskUserDialog.test.tsx index ef04e51499..1bd29241db 100644 --- a/packages/cli/src/ui/components/AskUserDialog.test.tsx +++ b/packages/cli/src/ui/components/AskUserDialog.test.tsx @@ -10,7 +10,6 @@ import { renderWithProviders } from '../../test-utils/render.js'; import { waitFor } from '../../test-utils/async.js'; import { AskUserDialog } from './AskUserDialog.js'; import { QuestionType, type Question } from '@google/gemini-cli-core'; -import chalk from 'chalk'; import { UIStateContext, type UIState } from '../contexts/UIStateContext.js'; // Helper to write to stdin with proper act() wrapping @@ -1104,7 +1103,7 @@ describe('AskUserDialog', () => { await waitUntilReady(); const frame = lastFrame(); // Plain text should be rendered as bold - expect(frame).toContain(chalk.bold('Which option do you prefer?')); + expect(frame).toContain('Which option do you prefer?'); }); }); @@ -1136,7 +1135,7 @@ describe('AskUserDialog', () => { // Should NOT have double-bold (the whole question bolded AND "this" bolded) // "Is " should not be bold, only "this" should be bold expect(frame).toContain('Is '); - expect(frame).toContain(chalk.bold('this')); + expect(frame).toContain('this'); expect(frame).not.toContain('**this**'); }); }); @@ -1166,8 +1165,8 @@ describe('AskUserDialog', () => { await waitFor(async () => { await waitUntilReady(); const frame = lastFrame(); - // Check for chalk.bold('this') - asterisks should be gone, text should be bold - expect(frame).toContain(chalk.bold('this')); + // Check for 'this' - asterisks should be gone + expect(frame).toContain('this'); expect(frame).not.toContain('**this**'); }); }); @@ -1198,8 +1197,8 @@ describe('AskUserDialog', () => { await waitUntilReady(); const frame = lastFrame(); // Backticks should be removed - expect(frame).toContain('npm start'); - expect(frame).not.toContain('`npm start`'); + expect(frame).toContain('Run npm start?'); + expect(frame).not.toContain('`'); }); }); }); diff --git a/packages/cli/src/ui/components/ConfigInitDisplay.test.tsx b/packages/cli/src/ui/components/ConfigInitDisplay.test.tsx index d942f8c55f..36ecbcbe5f 100644 --- a/packages/cli/src/ui/components/ConfigInitDisplay.test.tsx +++ b/packages/cli/src/ui/components/ConfigInitDisplay.test.tsx @@ -6,7 +6,7 @@ import { act } from 'react'; import type { EventEmitter } from 'node:events'; -import { render } from '../../test-utils/render.js'; +import { renderWithProviders } from '../../test-utils/render.js'; import { waitFor } from '../../test-utils/async.js'; import { ConfigInitDisplay } from './ConfigInitDisplay.js'; import { @@ -27,7 +27,7 @@ import { import { Text } from 'ink'; // Mock GeminiSpinner -vi.mock('./GeminiRespondingSpinner.js', () => ({ +vi.mock('./GeminiSpinner.js', () => ({ GeminiSpinner: () => Spinner, })); @@ -43,7 +43,9 @@ describe('ConfigInitDisplay', () => { }); it('renders initial state', async () => { - const { lastFrame, waitUntilReady } = render(); + const { lastFrame, waitUntilReady } = renderWithProviders( + , + ); await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); }); @@ -57,7 +59,7 @@ describe('ConfigInitDisplay', () => { return coreEvents; }); - const { lastFrame } = render(); + const { lastFrame } = renderWithProviders(); // Wait for listener to be registered await waitFor(() => { @@ -95,7 +97,7 @@ describe('ConfigInitDisplay', () => { return coreEvents; }); - const { lastFrame } = render(); + const { lastFrame } = renderWithProviders(); await waitFor(() => { if (!listener) throw new Error('Listener not registered yet'); @@ -131,7 +133,7 @@ describe('ConfigInitDisplay', () => { return coreEvents; }); - const { lastFrame } = render(); + const { lastFrame } = renderWithProviders(); await waitFor(() => { if (!listener) throw new Error('Listener not registered yet'); diff --git a/packages/cli/src/ui/components/ConfigInitDisplay.tsx b/packages/cli/src/ui/components/ConfigInitDisplay.tsx index a47e16daff..d421da211e 100644 --- a/packages/cli/src/ui/components/ConfigInitDisplay.tsx +++ b/packages/cli/src/ui/components/ConfigInitDisplay.tsx @@ -12,7 +12,7 @@ import { type McpClient, MCPServerStatus, } from '@google/gemini-cli-core'; -import { GeminiSpinner } from './GeminiRespondingSpinner.js'; +import { GeminiSpinner } from './GeminiSpinner.js'; import { theme } from '../semantic-colors.js'; export const ConfigInitDisplay = ({ diff --git a/packages/cli/src/ui/components/FolderTrustDialog.test.tsx b/packages/cli/src/ui/components/FolderTrustDialog.test.tsx index 07693db151..bbda51d8f0 100644 --- a/packages/cli/src/ui/components/FolderTrustDialog.test.tsx +++ b/packages/cli/src/ui/components/FolderTrustDialog.test.tsx @@ -7,7 +7,7 @@ import { renderWithProviders } from '../../test-utils/render.js'; import { waitFor } from '../../test-utils/async.js'; import { act } from 'react'; -import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; import { FolderTrustDialog } from './FolderTrustDialog.js'; import { ExitCodes } from '@google/gemini-cli-core'; import * as processUtils from '../../utils/processUtils.js'; diff --git a/packages/cli/src/ui/components/GeminiRespondingSpinner.test.tsx b/packages/cli/src/ui/components/GeminiRespondingSpinner.test.tsx index 84241b05ce..a60f91cd80 100644 --- a/packages/cli/src/ui/components/GeminiRespondingSpinner.test.tsx +++ b/packages/cli/src/ui/components/GeminiRespondingSpinner.test.tsx @@ -8,7 +8,7 @@ import { render } from '../../test-utils/render.js'; import { GeminiRespondingSpinner } from './GeminiRespondingSpinner.js'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { useStreamingContext } from '../contexts/StreamingContext.js'; -import { useIsScreenReaderEnabled } from 'ink'; +import { Text, useIsScreenReaderEnabled } from 'ink'; import { StreamingState } from '../types.js'; import { SCREEN_READER_LOADING, @@ -24,8 +24,10 @@ vi.mock('ink', async (importOriginal) => { }; }); -vi.mock('./CliSpinner.js', () => ({ - CliSpinner: () => 'Spinner', +vi.mock('./GeminiSpinner.js', () => ({ + GeminiSpinner: ({ altText }: { altText?: string }) => ( + GeminiSpinner {altText} + ), })); describe('GeminiRespondingSpinner', () => { @@ -33,23 +35,17 @@ describe('GeminiRespondingSpinner', () => { const mockUseIsScreenReaderEnabled = vi.mocked(useIsScreenReaderEnabled); beforeEach(() => { - vi.useFakeTimers(); vi.clearAllMocks(); mockUseIsScreenReaderEnabled.mockReturnValue(false); }); - afterEach(() => { - vi.useRealTimers(); - }); - it('renders spinner when responding', async () => { mockUseStreamingContext.mockReturnValue(StreamingState.Responding); const { lastFrame, waitUntilReady, unmount } = render( , ); await waitUntilReady(); - // Spinner output varies, but it shouldn't be empty - expect(lastFrame()).not.toBe(''); + expect(lastFrame()).toContain('GeminiSpinner'); unmount(); }); diff --git a/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx b/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx index da2fef686a..2e6821355f 100644 --- a/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx +++ b/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx @@ -5,9 +5,7 @@ */ import type React from 'react'; -import { useState, useEffect, useMemo } from 'react'; import { Text, useIsScreenReaderEnabled } from 'ink'; -import { CliSpinner } from './CliSpinner.js'; import type { SpinnerName } from 'cli-spinners'; import { useStreamingContext } from '../contexts/StreamingContext.js'; import { StreamingState } from '../types.js'; @@ -16,10 +14,7 @@ import { SCREEN_READER_RESPONDING, } from '../textConstants.js'; import { theme } from '../semantic-colors.js'; -import { Colors } from '../colors.js'; -import tinygradient from 'tinygradient'; - -const COLOR_CYCLE_DURATION_MS = 4000; +import { GeminiSpinner } from './GeminiSpinner.js'; interface GeminiRespondingSpinnerProps { /** @@ -54,51 +49,3 @@ export const GeminiRespondingSpinner: React.FC< return null; }; - -interface GeminiSpinnerProps { - spinnerType?: SpinnerName; - altText?: string; -} - -export const GeminiSpinner: React.FC = ({ - spinnerType = 'dots', - altText, -}) => { - const isScreenReaderEnabled = useIsScreenReaderEnabled(); - const [time, setTime] = useState(0); - - const googleGradient = useMemo(() => { - const brandColors = [ - Colors.AccentPurple, - Colors.AccentBlue, - Colors.AccentCyan, - Colors.AccentGreen, - Colors.AccentYellow, - Colors.AccentRed, - ]; - return tinygradient([...brandColors, brandColors[0]]); - }, []); - - useEffect(() => { - if (isScreenReaderEnabled) { - return; - } - - const interval = setInterval(() => { - setTime((prevTime) => prevTime + 30); - }, 30); // ~33fps for smooth color transitions - - return () => clearInterval(interval); - }, [isScreenReaderEnabled]); - - const progress = (time % COLOR_CYCLE_DURATION_MS) / COLOR_CYCLE_DURATION_MS; - const currentColor = googleGradient.rgbAt(progress).toHexString(); - - return isScreenReaderEnabled ? ( - {altText} - ) : ( - - - - ); -}; diff --git a/packages/cli/src/ui/components/GeminiSpinner.tsx b/packages/cli/src/ui/components/GeminiSpinner.tsx new file mode 100644 index 0000000000..37d1930625 --- /dev/null +++ b/packages/cli/src/ui/components/GeminiSpinner.tsx @@ -0,0 +1,63 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type React from 'react'; +import { useState, useEffect, useMemo } from 'react'; +import { Text, useIsScreenReaderEnabled } from 'ink'; +import { CliSpinner } from './CliSpinner.js'; +import type { SpinnerName } from 'cli-spinners'; +import { Colors } from '../colors.js'; +import tinygradient from 'tinygradient'; + +const COLOR_CYCLE_DURATION_MS = 4000; + +interface GeminiSpinnerProps { + spinnerType?: SpinnerName; + altText?: string; +} + +export const GeminiSpinner: React.FC = ({ + spinnerType = 'dots', + altText, +}) => { + const isScreenReaderEnabled = useIsScreenReaderEnabled(); + const [time, setTime] = useState(0); + + const googleGradient = useMemo(() => { + const brandColors = [ + Colors.AccentPurple, + Colors.AccentBlue, + Colors.AccentCyan, + Colors.AccentGreen, + Colors.AccentYellow, + Colors.AccentRed, + ]; + return tinygradient([...brandColors, brandColors[0]]); + }, []); + + useEffect(() => { + if (isScreenReaderEnabled) { + return; + } + + const interval = setInterval(() => { + setTime((prevTime) => prevTime + 30); + }, 30); // ~33fps for smooth color transitions + + return () => clearInterval(interval); + }, [isScreenReaderEnabled]); + + const progress = (time % COLOR_CYCLE_DURATION_MS) / COLOR_CYCLE_DURATION_MS; + const currentColor = googleGradient.rgbAt(progress).toHexString(); + + return isScreenReaderEnabled ? ( + {altText} + ) : ( + + + + ); +}; diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index 1576cef2e8..bf906d4a80 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -1537,7 +1537,7 @@ describe('InputPrompt', () => { const { stdout, unmount } = renderWithProviders(); await waitFor(() => { - const frame = stdout.lastFrame(); + const frame = stdout.lastFrameRaw(); // In plan mode it uses '>' but with success color. // We check that it contains '>' and not '*' or '!'. expect(frame).toContain('>'); @@ -1593,7 +1593,7 @@ describe('InputPrompt', () => { ); await waitFor(() => { - const frame = stdout.lastFrame(); + const frame = stdout.lastFrameRaw(); expect(frame).toContain('▀'); expect(frame).toContain('▄'); }); @@ -1626,7 +1626,7 @@ describe('InputPrompt', () => { const expectedBgColor = isWhite ? '#eeeeee' : '#1c1c1c'; await waitFor(() => { - const frame = stdout.lastFrame(); + const frame = stdout.lastFrameRaw(); // Use chalk to get the expected background color escape sequence const bgCheck = chalk.bgHex(expectedBgColor)(' '); @@ -1658,7 +1658,7 @@ describe('InputPrompt', () => { ); await waitFor(() => { - const frame = stdout.lastFrame(); + const frame = stdout.lastFrameRaw(); expect(frame).not.toContain('▀'); expect(frame).not.toContain('▄'); // It SHOULD have horizontal fallback lines @@ -1681,7 +1681,7 @@ describe('InputPrompt', () => { ); await waitFor(() => { - const frame = stdout.lastFrame(); + const frame = stdout.lastFrameRaw(); expect(frame).toContain('▀'); @@ -1705,7 +1705,7 @@ describe('InputPrompt', () => { ); await waitFor(() => { - const frame = stdout.lastFrame(); + const frame = stdout.lastFrameRaw(); // Should NOT have background characters @@ -1734,7 +1734,7 @@ describe('InputPrompt', () => { ); await waitFor(() => { - const frame = stdout.lastFrame(); + const frame = stdout.lastFrameRaw(); expect(frame).not.toContain('▀'); expect(frame).not.toContain('▄'); // Check for Box borders (round style uses unicode box chars) @@ -1974,7 +1974,7 @@ describe('InputPrompt', () => { name: 'at the end of a line with unicode characters', text: 'hello 👍', visualCursor: [0, 8], - expected: `hello 👍${chalk.inverse(' ')}`, + expected: `hello 👍`, // skip checking inverse ansi due to ink truncation bug }, { name: 'at the end of a short line with unicode characters', @@ -1996,7 +1996,7 @@ describe('InputPrompt', () => { }, ])( 'should display cursor correctly $name', - async ({ text, visualCursor, expected }) => { + async ({ name, text, visualCursor, expected }) => { mockBuffer.text = text; mockBuffer.lines = [text]; mockBuffer.viewportVisualLines = [text]; @@ -2007,8 +2007,14 @@ describe('InputPrompt', () => { , ); await waitFor(() => { - const frame = stdout.lastFrame(); - expect(frame).toContain(expected); + const frame = stdout.lastFrameRaw(); + expect(stripAnsi(frame)).toContain(stripAnsi(expected)); + if ( + name !== 'at the end of a line with unicode characters' && + name !== 'on a highlighted token' + ) { + expect(frame).toContain('\u001b[7m'); + } }); unmount(); }, @@ -2050,7 +2056,7 @@ describe('InputPrompt', () => { }, ])( 'should display cursor correctly $name in a multiline block', - async ({ text, visualCursor, expected, visualToLogicalMap }) => { + async ({ name, text, visualCursor, expected, visualToLogicalMap }) => { mockBuffer.text = text; mockBuffer.lines = text.split('\n'); mockBuffer.viewportVisualLines = text.split('\n'); @@ -2064,8 +2070,14 @@ describe('InputPrompt', () => { , ); await waitFor(() => { - const frame = stdout.lastFrame(); - expect(frame).toContain(expected); + const frame = stdout.lastFrameRaw(); + expect(stripAnsi(frame)).toContain(stripAnsi(expected)); + if ( + name !== 'at the end of a line with unicode characters' && + name !== 'on a highlighted token' + ) { + expect(frame).toContain('\u001b[7m'); + } }); unmount(); }, @@ -2088,7 +2100,7 @@ describe('InputPrompt', () => { , ); await waitFor(() => { - const frame = stdout.lastFrame(); + const frame = stdout.lastFrameRaw(); const lines = frame.split('\n'); // The line with the cursor should just be an inverted space inside the box border expect( @@ -2120,7 +2132,7 @@ describe('InputPrompt', () => { , ); await waitFor(() => { - const frame = stdout.lastFrame(); + const frame = stdout.lastFrameRaw(); // Check that all lines, including the empty one, are rendered. // This implicitly tests that the Box wrapper provides height for the empty line. expect(frame).toContain('hello'); @@ -2655,7 +2667,7 @@ describe('InputPrompt', () => { }); await waitFor(() => { - const frame = stdout.lastFrame(); + const frame = stdout.lastFrameRaw(); expect(frame).toContain('(r:)'); expect(frame).toContain('echo hello'); expect(frame).toContain('echo world'); @@ -2926,7 +2938,7 @@ describe('InputPrompt', () => { }); await waitFor(() => { - const frame = stdout.lastFrame() ?? ''; + const frame = stdout.lastFrameRaw() ?? ''; expect(frame).toContain('(r:)'); expect(frame).toContain('git commit'); expect(frame).toContain('git push'); diff --git a/packages/cli/src/ui/components/SettingsDialog.test.tsx b/packages/cli/src/ui/components/SettingsDialog.test.tsx index 72ef839ea3..3dd5374a18 100644 --- a/packages/cli/src/ui/components/SettingsDialog.test.tsx +++ b/packages/cli/src/ui/components/SettingsDialog.test.tsx @@ -263,16 +263,11 @@ describe('SettingsDialog', () => { const settings = createMockSettings(); const onSelect = vi.fn(); - const { lastFrame, waitUntilReady, unmount } = renderDialog( - settings, - onSelect, - ); - await waitUntilReady(); + const renderResult = renderDialog(settings, onSelect); + await renderResult.waitUntilReady(); - const output = lastFrame(); - // Use snapshot to capture visual layout including indicators - expect(output).toMatchSnapshot(); - unmount(); + await expect(renderResult).toMatchSvgSnapshot(); + renderResult.unmount(); }); it('should use almost full height of the window but no more when the window height is 25 rows', async () => { @@ -1830,18 +1825,15 @@ describe('SettingsDialog', () => { }); const onSelect = vi.fn(); - const { lastFrame, stdin, waitUntilReady, unmount } = renderDialog( - settings, - onSelect, - ); - await waitUntilReady(); + const renderResult = renderDialog(settings, onSelect); + await renderResult.waitUntilReady(); if (stdinActions) { - await stdinActions(stdin, waitUntilReady); + await stdinActions(renderResult.stdin, renderResult.waitUntilReady); } - expect(lastFrame()).toMatchSnapshot(); - unmount(); + await expect(renderResult).toMatchSvgSnapshot(); + renderResult.unmount(); }, ); }); diff --git a/packages/cli/src/ui/components/Table.test.tsx b/packages/cli/src/ui/components/Table.test.tsx index 889872f35e..e8f312d9af 100644 --- a/packages/cli/src/ui/components/Table.test.tsx +++ b/packages/cli/src/ui/components/Table.test.tsx @@ -19,10 +19,8 @@ describe('Table', () => { { id: 2, name: 'Bob' }, ]; - const { lastFrame, waitUntilReady } = render( - , - 100, - ); + const renderResult = render(
, 100); + const { lastFrame, waitUntilReady } = renderResult; await waitUntilReady?.(); const output = lastFrame(); @@ -32,7 +30,7 @@ describe('Table', () => { expect(output).toContain('Alice'); expect(output).toContain('2'); expect(output).toContain('Bob'); - expect(lastFrame()).toMatchSnapshot(); + await expect(renderResult).toMatchSvgSnapshot(); }); it('should support custom cell rendering', async () => { @@ -48,15 +46,13 @@ describe('Table', () => { ]; const data = [{ value: 10 }]; - const { lastFrame, waitUntilReady } = render( -
, - 100, - ); + const renderResult = render(
, 100); + const { lastFrame, waitUntilReady } = renderResult; await waitUntilReady?.(); const output = lastFrame(); expect(output).toContain('20'); - expect(lastFrame()).toMatchSnapshot(); + await expect(renderResult).toMatchSvgSnapshot(); }); it('should handle undefined values gracefully', async () => { @@ -70,4 +66,26 @@ describe('Table', () => { const output = lastFrame(); expect(output).toContain('undefined'); }); + + it('should support inverse text rendering', async () => { + const columns = [ + { + key: 'status', + header: 'Status', + flexGrow: 1, + renderCell: (item: { status: string }) => ( + {item.status} + ), + }, + ]; + const data = [{ status: 'Active' }]; + + const renderResult = render(
, 100); + const { lastFrame, waitUntilReady } = renderResult; + await waitUntilReady?.(); + const output = lastFrame(); + + expect(output).toContain('Active'); + await expect(renderResult).toMatchSvgSnapshot(); + }); }); diff --git a/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx b/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx index ab7d080b37..75612add4c 100644 --- a/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx +++ b/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx @@ -255,7 +255,11 @@ describe('ToolConfirmationQueue', () => { total: 1, }; - const { lastFrame, waitUntilReady, unmount } = renderWithProviders( + const { + lastFrame, + waitUntilReady, + unmount = vi.fn(), + } = renderWithProviders( , diff --git a/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap index 8fb49b8b71..18e75b75e2 100644 --- a/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap @@ -2,14 +2,14 @@ exports[`AlternateBufferQuittingDisplay > renders with a tool awaiting confirmation > with_confirming_tool 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ Tips for getting started: 1. Ask questions, edit files, or run commands. @@ -25,14 +25,14 @@ Action Required (was prompted): exports[`AlternateBufferQuittingDisplay > renders with active and pending tool messages > with_history_and_pending 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ Tips for getting started: 1. Ask questions, edit files, or run commands. @@ -52,14 +52,14 @@ Tips for getting started: exports[`AlternateBufferQuittingDisplay > renders with empty history and no pending items > empty 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ Tips for getting started: 1. Ask questions, edit files, or run commands. @@ -71,14 +71,14 @@ Tips for getting started: exports[`AlternateBufferQuittingDisplay > renders with history but no pending items > with_history_no_pending 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ Tips for getting started: 1. Ask questions, edit files, or run commands. @@ -98,14 +98,14 @@ Tips for getting started: exports[`AlternateBufferQuittingDisplay > renders with pending items but no history > with_pending_no_history 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ Tips for getting started: 1. Ask questions, edit files, or run commands. @@ -117,14 +117,14 @@ Tips for getting started: exports[`AlternateBufferQuittingDisplay > renders with user and gemini messages > with_user_gemini_messages 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ Tips for getting started: 1. Ask questions, edit files, or run commands. @@ -132,7 +132,7 @@ Tips for getting started: 3. Create GEMINI.md files to customize your interactions with Gemini. 4. /help for more information. ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > Hello Gemini + > Hello Gemini ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ✦ Hello User! " diff --git a/packages/cli/src/ui/components/__snapshots__/AppHeader.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/AppHeader.test.tsx.snap index 59cf561759..324274fddd 100644 --- a/packages/cli/src/ui/components/__snapshots__/AppHeader.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/AppHeader.test.tsx.snap @@ -2,14 +2,14 @@ exports[` > should not render the banner when no flags are set 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ Tips for getting started: 1. Ask questions, edit files, or run commands. @@ -21,14 +21,14 @@ Tips for getting started: exports[` > should not render the default banner if shown count is 5 or more 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ Tips for getting started: 1. Ask questions, edit files, or run commands. @@ -40,14 +40,14 @@ Tips for getting started: exports[` > should render the banner with default text 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │ This is the default banner │ @@ -62,14 +62,14 @@ Tips for getting started: exports[` > should render the banner with warning text 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │ There are capacity issues │ diff --git a/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap index 6a9bf5aeac..88a1b0486f 100644 --- a/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap @@ -2,16 +2,16 @@ exports[`InputPrompt > History Navigation and Completion Suppression > should not render suggestions during history navigation 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > second message + > second message ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and collapses long suggestion via Right/Left arrows > command-search-render-collapsed-match 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - (r:) Type your message or @path/to/file + (r:) Type your message or @path/to/file ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ - lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll → + lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll → lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll ... " @@ -19,9 +19,9 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and c exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and collapses long suggestion via Right/Left arrows > command-search-render-expanded-match 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - (r:) Type your message or @path/to/file + (r:) Type your message or @path/to/file ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ - lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll ← + lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll ← lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll llllllllllllllllllllllllllllllllllllllllllllllllll " @@ -29,7 +29,7 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and c exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-render-collapsed-match 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - (r:) commit + (r:) commit ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ git commit -m "feat: add search" in src/app " @@ -37,7 +37,7 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-render-expanded-match 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - (r:) commit + (r:) commit ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ git commit -m "feat: add search" in src/app " @@ -45,63 +45,63 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match exports[`InputPrompt > image path transformation snapshots > should snapshot collapsed image path 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > [Image ...reenshot2x.png] + > [Image ...reenshot2x.png] ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; exports[`InputPrompt > image path transformation snapshots > should snapshot expanded image path when cursor is on it 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > @/path/to/screenshots/screenshot2x.png + > @/path/to/screenshots/screenshot2x.png ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > [Pasted Text: 10 lines] + > [Pasted Text: 10 lines] ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 2`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > [Pasted Text: 10 lines] + > [Pasted Text: 10 lines] ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 3`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > [Pasted Text: 10 lines] + > [Pasted Text: 10 lines] ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; exports[`InputPrompt > snapshots > should not show inverted cursor when shell is focused 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > Type your message or @path/to/file + > Type your message or @path/to/file ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; exports[`InputPrompt > snapshots > should render correctly in shell mode 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - ! Type your message or @path/to/file + ! Type your message or @path/to/file ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; exports[`InputPrompt > snapshots > should render correctly in yolo mode 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - * Type your message or @path/to/file + * Type your message or @path/to/file ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; exports[`InputPrompt > snapshots > should render correctly when accepting edits 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > Type your message or @path/to/file + > Type your message or @path/to/file ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Initial-Rendering-should-render-settings-list-with-visual-indicators.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Initial-Rendering-should-render-settings-list-with-visual-indicators.snap.svg new file mode 100644 index 0000000000..b7ad1d10db --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Initial-Rendering-should-render-settings-list-with-visual-indicators.snap.svg @@ -0,0 +1,133 @@ + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + > Settings + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + + S + earch to filter + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + + + + + + Vim Mode + false + + + Enable Vim keybindings + + + + + Default Approval Mode + Default + + + The default approval mode for tool execution. 'default' prompts for approval, 'au… + + + + + Enable Auto Update + true + + + Enable automatic updates. + + + + + Enable Notifications + false + + + Enable run-event notifications for action-required prompts and session completion. … + + + + + Plan Directory + undefined + + + The directory where planning artifacts are stored. If not specified, defaults t… + + + + + Plan Model Routing + true + + + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + + + + + Max Chat Model Attempts + 10 + + + Maximum number of attempts for requests to the main chat model. Cannot exceed 10. + + + + + Debug Keystroke Logging + false + + + Enable debug logging of keystrokes to the console. + + + + + + + + + + Apply To + + + + User Settings + + + Workspace Settings + + + System Settings + + + + + (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-accessibility-settings-enabled-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-accessibility-settings-enabled-correctly.snap.svg new file mode 100644 index 0000000000..c088c69139 --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-accessibility-settings-enabled-correctly.snap.svg @@ -0,0 +1,133 @@ + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + > Settings + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + + S + earch to filter + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + + + + + + Vim Mode + true* + + + Enable Vim keybindings + + + + + Default Approval Mode + Default + + + The default approval mode for tool execution. 'default' prompts for approval, 'au… + + + + + Enable Auto Update + true + + + Enable automatic updates. + + + + + Enable Notifications + false + + + Enable run-event notifications for action-required prompts and session completion. … + + + + + Plan Directory + undefined + + + The directory where planning artifacts are stored. If not specified, defaults t… + + + + + Plan Model Routing + true + + + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + + + + + Max Chat Model Attempts + 10 + + + Maximum number of attempts for requests to the main chat model. Cannot exceed 10. + + + + + Debug Keystroke Logging + false + + + Enable debug logging of keystrokes to the console. + + + + + + + + + + Apply To + + + + User Settings + + + Workspace Settings + + + System Settings + + + + + (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-all-boolean-settings-disabled-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-all-boolean-settings-disabled-correctly.snap.svg new file mode 100644 index 0000000000..0b981a31c8 --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-all-boolean-settings-disabled-correctly.snap.svg @@ -0,0 +1,131 @@ + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + > Settings + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + + S + earch to filter + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + + + + + + Vim Mode + false* + + + Enable Vim keybindings + + + + + Default Approval Mode + Default + + + The default approval mode for tool execution. 'default' prompts for approval, 'au… + + + + + Enable Auto Update true* + + + Enable automatic updates. + + + + + Enable Notifications + false + + + Enable run-event notifications for action-required prompts and session completion. … + + + + + Plan Directory + undefined + + + The directory where planning artifacts are stored. If not specified, defaults t… + + + + + Plan Model Routing + true + + + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + + + + + Max Chat Model Attempts + 10 + + + Maximum number of attempts for requests to the main chat model. Cannot exceed 10. + + + + + Debug Keystroke Logging false* + + + Enable debug logging of keystrokes to the console. + + + + + + + + + + Apply To + + + + User Settings + + + Workspace Settings + + + System Settings + + + + + (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-default-state-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-default-state-correctly.snap.svg new file mode 100644 index 0000000000..b7ad1d10db --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-default-state-correctly.snap.svg @@ -0,0 +1,133 @@ + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + > Settings + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + + S + earch to filter + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + + + + + + Vim Mode + false + + + Enable Vim keybindings + + + + + Default Approval Mode + Default + + + The default approval mode for tool execution. 'default' prompts for approval, 'au… + + + + + Enable Auto Update + true + + + Enable automatic updates. + + + + + Enable Notifications + false + + + Enable run-event notifications for action-required prompts and session completion. … + + + + + Plan Directory + undefined + + + The directory where planning artifacts are stored. If not specified, defaults t… + + + + + Plan Model Routing + true + + + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + + + + + Max Chat Model Attempts + 10 + + + Maximum number of attempts for requests to the main chat model. Cannot exceed 10. + + + + + Debug Keystroke Logging + false + + + Enable debug logging of keystrokes to the console. + + + + + + + + + + Apply To + + + + User Settings + + + Workspace Settings + + + System Settings + + + + + (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-file-filtering-settings-configured-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-file-filtering-settings-configured-correctly.snap.svg new file mode 100644 index 0000000000..b7ad1d10db --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-file-filtering-settings-configured-correctly.snap.svg @@ -0,0 +1,133 @@ + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + > Settings + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + + S + earch to filter + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + + + + + + Vim Mode + false + + + Enable Vim keybindings + + + + + Default Approval Mode + Default + + + The default approval mode for tool execution. 'default' prompts for approval, 'au… + + + + + Enable Auto Update + true + + + Enable automatic updates. + + + + + Enable Notifications + false + + + Enable run-event notifications for action-required prompts and session completion. … + + + + + Plan Directory + undefined + + + The directory where planning artifacts are stored. If not specified, defaults t… + + + + + Plan Model Routing + true + + + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + + + + + Max Chat Model Attempts + 10 + + + Maximum number of attempts for requests to the main chat model. Cannot exceed 10. + + + + + Debug Keystroke Logging + false + + + Enable debug logging of keystrokes to the console. + + + + + + + + + + Apply To + + + + User Settings + + + Workspace Settings + + + System Settings + + + + + (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-focused-on-scope-selector-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-focused-on-scope-selector-correctly.snap.svg new file mode 100644 index 0000000000..81d4868518 --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-focused-on-scope-selector-correctly.snap.svg @@ -0,0 +1,131 @@ + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + Settings + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + Search to filter + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + + + + + Vim Mode + false + + + Enable Vim keybindings + + + + + Default Approval Mode + Default + + + The default approval mode for tool execution. 'default' prompts for approval, 'au… + + + + + Enable Auto Update + true + + + Enable automatic updates. + + + + + Enable Notifications + false + + + Enable run-event notifications for action-required prompts and session completion. … + + + + + Plan Directory + undefined + + + The directory where planning artifacts are stored. If not specified, defaults t… + + + + + Plan Model Routing + true + + + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + + + + + Max Chat Model Attempts + 10 + + + Maximum number of attempts for requests to the main chat model. Cannot exceed 10. + + + + + Debug Keystroke Logging + false + + + Enable debug logging of keystrokes to the console. + + + + + + + + + + > Apply To + + + + 1. + User Settings + + + 2. Workspace Settings + + + 3. System Settings + + + + + (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-mixed-boolean-and-number-settings-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-mixed-boolean-and-number-settings-correctly.snap.svg new file mode 100644 index 0000000000..324ed5c2cb --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-mixed-boolean-and-number-settings-correctly.snap.svg @@ -0,0 +1,132 @@ + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + > Settings + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + + S + earch to filter + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + + + + + + Vim Mode + false* + + + Enable Vim keybindings + + + + + Default Approval Mode + Default + + + The default approval mode for tool execution. 'default' prompts for approval, 'au… + + + + + Enable Auto Update false* + + + Enable automatic updates. + + + + + Enable Notifications + false + + + Enable run-event notifications for action-required prompts and session completion. … + + + + + Plan Directory + undefined + + + The directory where planning artifacts are stored. If not specified, defaults t… + + + + + Plan Model Routing + true + + + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + + + + + Max Chat Model Attempts + 10 + + + Maximum number of attempts for requests to the main chat model. Cannot exceed 10. + + + + + Debug Keystroke Logging + false + + + Enable debug logging of keystrokes to the console. + + + + + + + + + + Apply To + + + + User Settings + + + Workspace Settings + + + System Settings + + + + + (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-tools-and-security-settings-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-tools-and-security-settings-correctly.snap.svg new file mode 100644 index 0000000000..b7ad1d10db --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-tools-and-security-settings-correctly.snap.svg @@ -0,0 +1,133 @@ + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + > Settings + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + + S + earch to filter + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + + + + + + Vim Mode + false + + + Enable Vim keybindings + + + + + Default Approval Mode + Default + + + The default approval mode for tool execution. 'default' prompts for approval, 'au… + + + + + Enable Auto Update + true + + + Enable automatic updates. + + + + + Enable Notifications + false + + + Enable run-event notifications for action-required prompts and session completion. … + + + + + Plan Directory + undefined + + + The directory where planning artifacts are stored. If not specified, defaults t… + + + + + Plan Model Routing + true + + + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + + + + + Max Chat Model Attempts + 10 + + + Maximum number of attempts for requests to the main chat model. Cannot exceed 10. + + + + + Debug Keystroke Logging + false + + + Enable debug logging of keystrokes to the console. + + + + + + + + + + Apply To + + + + User Settings + + + Workspace Settings + + + System Settings + + + + + (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-various-boolean-settings-enabled-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-various-boolean-settings-enabled-correctly.snap.svg new file mode 100644 index 0000000000..e99a5b4cdd --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-various-boolean-settings-enabled-correctly.snap.svg @@ -0,0 +1,131 @@ + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + > Settings + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + + S + earch to filter + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + + + + + + Vim Mode + true* + + + Enable Vim keybindings + + + + + Default Approval Mode + Default + + + The default approval mode for tool execution. 'default' prompts for approval, 'au… + + + + + Enable Auto Update false* + + + Enable automatic updates. + + + + + Enable Notifications + false + + + Enable run-event notifications for action-required prompts and session completion. … + + + + + Plan Directory + undefined + + + The directory where planning artifacts are stored. If not specified, defaults t… + + + + + Plan Model Routing + true + + + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + + + + + Max Chat Model Attempts + 10 + + + Maximum number of attempts for requests to the main chat model. Cannot exceed 10. + + + + + Debug Keystroke Logging true* + + + Enable debug logging of keystrokes to the console. + + + + + + + + + + Apply To + + + + User Settings + + + Workspace Settings + + + System Settings + + + + + (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap index f1bd8d3852..be2dd8d9a2 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap @@ -43,8 +43,7 @@ exports[`SettingsDialog > Initial Rendering > should render settings list with v │ │ │ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ -" +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; exports[`SettingsDialog > Snapshot Tests > should render 'accessibility settings enabled' correctly 1`] = ` @@ -90,8 +89,7 @@ exports[`SettingsDialog > Snapshot Tests > should render 'accessibility settings │ │ │ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ -" +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; exports[`SettingsDialog > Snapshot Tests > should render 'all boolean settings disabled' correctly 1`] = ` @@ -137,8 +135,7 @@ exports[`SettingsDialog > Snapshot Tests > should render 'all boolean settings d │ │ │ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ -" +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; exports[`SettingsDialog > Snapshot Tests > should render 'default state' correctly 1`] = ` @@ -184,8 +181,7 @@ exports[`SettingsDialog > Snapshot Tests > should render 'default state' correct │ │ │ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ -" +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; exports[`SettingsDialog > Snapshot Tests > should render 'file filtering settings configured' correctly 1`] = ` @@ -231,8 +227,7 @@ exports[`SettingsDialog > Snapshot Tests > should render 'file filtering setting │ │ │ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ -" +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; exports[`SettingsDialog > Snapshot Tests > should render 'focused on scope selector' correctly 1`] = ` @@ -278,8 +273,7 @@ exports[`SettingsDialog > Snapshot Tests > should render 'focused on scope selec │ │ │ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ -" +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; exports[`SettingsDialog > Snapshot Tests > should render 'mixed boolean and number settings' correctly 1`] = ` @@ -325,8 +319,7 @@ exports[`SettingsDialog > Snapshot Tests > should render 'mixed boolean and numb │ │ │ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ -" +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; exports[`SettingsDialog > Snapshot Tests > should render 'tools and security settings' correctly 1`] = ` @@ -372,8 +365,7 @@ exports[`SettingsDialog > Snapshot Tests > should render 'tools and security set │ │ │ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ -" +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; exports[`SettingsDialog > Snapshot Tests > should render 'various boolean settings enabled' correctly 1`] = ` @@ -419,6 +411,5 @@ exports[`SettingsDialog > Snapshot Tests > should render 'various boolean settin │ │ │ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ -" +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; diff --git a/packages/cli/src/ui/components/__snapshots__/Table-Table-should-render-headers-and-data-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/Table-Table-should-render-headers-and-data-correctly.snap.svg new file mode 100644 index 0000000000..6042642abd --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/Table-Table-should-render-headers-and-data-correctly.snap.svg @@ -0,0 +1,12 @@ + + + + + ID Name + ──────────────────────────────────────────────────────────────────────────────────────────────────── + 1 Alice + 2 Bob + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/Table-Table-should-support-custom-cell-rendering.snap.svg b/packages/cli/src/ui/components/__snapshots__/Table-Table-should-support-custom-cell-rendering.snap.svg new file mode 100644 index 0000000000..359b4ee76d --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/Table-Table-should-support-custom-cell-rendering.snap.svg @@ -0,0 +1,11 @@ + + + + + Value + ──────────────────────────────────────────────────────────────────────────────────────────────────── + 20 + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/Table-Table-should-support-inverse-text-rendering.snap.svg b/packages/cli/src/ui/components/__snapshots__/Table-Table-should-support-inverse-text-rendering.snap.svg new file mode 100644 index 0000000000..4473a2e810 --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/Table-Table-should-support-inverse-text-rendering.snap.svg @@ -0,0 +1,12 @@ + + + + + Status + ──────────────────────────────────────────────────────────────────────────────────────────────────── + + Active + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/Table.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/Table.test.tsx.snap index 27a1e6e6f6..8356ef4345 100644 --- a/packages/cli/src/ui/components/__snapshots__/Table.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/Table.test.tsx.snap @@ -4,13 +4,17 @@ exports[`Table > should render headers and data correctly 1`] = ` "ID Name ──────────────────────────────────────────────────────────────────────────────────────────────────── 1 Alice -2 Bob -" +2 Bob" `; exports[`Table > should support custom cell rendering 1`] = ` "Value ──────────────────────────────────────────────────────────────────────────────────────────────────── -20 -" +20" +`; + +exports[`Table > should support inverse text rendering 1`] = ` +"Status +──────────────────────────────────────────────────────────────────────────────────────────────────── +Active" `; diff --git a/packages/cli/src/ui/components/messages/__snapshots__/UserMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/UserMessage.test.tsx.snap index 9488a20ba3..679a5885d1 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/UserMessage.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/UserMessage.test.tsx.snap @@ -2,29 +2,29 @@ exports[`UserMessage > renders multiline user message 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > Line 1 - Line 2 + > Line 1 + Line 2 ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; exports[`UserMessage > renders normal user message with correct prefix 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > Hello Gemini + > Hello Gemini ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; exports[`UserMessage > renders slash command message 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > /help + > /help ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; exports[`UserMessage > transforms image paths in user message 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > Check out this image: [Image my-image.png] + > Check out this image: [Image my-image.png] ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; diff --git a/packages/cli/src/ui/components/shared/ExpandableText.test.tsx b/packages/cli/src/ui/components/shared/ExpandableText.test.tsx index 3634aafa8d..00c82a009d 100644 --- a/packages/cli/src/ui/components/shared/ExpandableText.test.tsx +++ b/packages/cli/src/ui/components/shared/ExpandableText.test.tsx @@ -4,7 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import chalk from 'chalk'; import { describe, it, expect } from 'vitest'; import { render } from '../../../test-utils/render.js'; import { ExpandableText, MAX_WIDTH } from './ExpandableText.js'; @@ -14,7 +13,7 @@ describe('ExpandableText', () => { const flat = (s: string | undefined) => (s ?? '').replace(/\n/g, ''); it('renders plain label when no match (short label)', async () => { - const { lastFrame, waitUntilReady, unmount } = render( + const renderResult = render( { isExpanded={false} />, ); + const { waitUntilReady, unmount } = renderResult; await waitUntilReady(); - expect(lastFrame()).toMatchSnapshot(); + await expect(renderResult).toMatchSvgSnapshot(); unmount(); }); it('truncates long label when collapsed and no match', async () => { const long = 'x'.repeat(MAX_WIDTH + 25); - const { lastFrame, waitUntilReady, unmount } = render( + const renderResult = render( { isExpanded={false} />, ); + const { lastFrame, waitUntilReady, unmount } = renderResult; await waitUntilReady(); const out = lastFrame(); const f = flat(out); expect(f.endsWith('...')).toBe(true); expect(f.length).toBe(MAX_WIDTH + 3); - expect(out).toMatchSnapshot(); + await expect(renderResult).toMatchSvgSnapshot(); unmount(); }); it('shows full long label when expanded and no match', async () => { const long = 'y'.repeat(MAX_WIDTH + 25); - const { lastFrame, waitUntilReady, unmount } = render( + const renderResult = render( { isExpanded={true} />, ); + const { lastFrame, waitUntilReady, unmount } = renderResult; await waitUntilReady(); const out = lastFrame(); const f = flat(out); expect(f.length).toBe(long.length); - expect(out).toMatchSnapshot(); + await expect(renderResult).toMatchSvgSnapshot(); unmount(); }); @@ -69,7 +71,7 @@ describe('ExpandableText', () => { const label = 'run: git commit -m "feat: add search"'; const userInput = 'commit'; const matchedIndex = label.indexOf(userInput); - const { lastFrame, waitUntilReady, unmount } = render( + const renderResult = render( { />, 100, ); + const { waitUntilReady, unmount } = renderResult; await waitUntilReady(); - expect(lastFrame()).toMatchSnapshot(); - expect(lastFrame()).toContain(chalk.inverse(userInput)); + await expect(renderResult).toMatchSvgSnapshot(); unmount(); }); @@ -91,7 +93,7 @@ describe('ExpandableText', () => { const suffix = '/and/then/some/more/components/'.repeat(3); const label = prefix + core + suffix; const matchedIndex = prefix.length; - const { lastFrame, waitUntilReady, unmount } = render( + const renderResult = render( { />, 100, ); + const { lastFrame, waitUntilReady, unmount } = renderResult; await waitUntilReady(); const out = lastFrame(); const f = flat(out); expect(f.includes(core)).toBe(true); expect(f.startsWith('...')).toBe(true); expect(f.endsWith('...')).toBe(true); - expect(out).toMatchSnapshot(); + await expect(renderResult).toMatchSvgSnapshot(); unmount(); }); @@ -117,7 +120,7 @@ describe('ExpandableText', () => { const suffix = ' in this text'; const label = prefix + core + suffix; const matchedIndex = prefix.length; - const { lastFrame, waitUntilReady, unmount } = render( + const renderResult = render( { isExpanded={false} />, ); + const { lastFrame, waitUntilReady, unmount } = renderResult; await waitUntilReady(); const out = lastFrame(); const f = flat(out); @@ -133,14 +137,14 @@ describe('ExpandableText', () => { expect(f.startsWith('...')).toBe(false); expect(f.endsWith('...')).toBe(true); expect(f.length).toBe(MAX_WIDTH + 2); - expect(out).toMatchSnapshot(); + await expect(renderResult).toMatchSvgSnapshot(); unmount(); }); it('respects custom maxWidth', async () => { const customWidth = 50; const long = 'z'.repeat(100); - const { lastFrame, waitUntilReady, unmount } = render( + const renderResult = render( { maxWidth={customWidth} />, ); + const { lastFrame, waitUntilReady, unmount } = renderResult; await waitUntilReady(); const out = lastFrame(); const f = flat(out); expect(f.endsWith('...')).toBe(true); expect(f.length).toBe(customWidth + 3); - expect(out).toMatchSnapshot(); + await expect(renderResult).toMatchSvgSnapshot(); unmount(); }); }); diff --git a/packages/cli/src/ui/components/shared/__snapshots__/EnumSelector.test.tsx.snap b/packages/cli/src/ui/components/shared/__snapshots__/EnumSelector.test.tsx.snap index 8fd19b3868..203ceb61d6 100644 --- a/packages/cli/src/ui/components/shared/__snapshots__/EnumSelector.test.tsx.snap +++ b/packages/cli/src/ui/components/shared/__snapshots__/EnumSelector.test.tsx.snap @@ -11,7 +11,7 @@ exports[` > renders with numeric options and matches snapshot 1` `; exports[` > renders with single option and matches snapshot 1`] = ` -" Only Option +" Only Option " `; diff --git a/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-creates-centered-window-around-match-when-collapsed.snap.svg b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-creates-centered-window-around-match-when-collapsed.snap.svg new file mode 100644 index 0000000000..1f6239e48c --- /dev/null +++ b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-creates-centered-window-around-match-when-collapsed.snap.svg @@ -0,0 +1,13 @@ + + + + + ...ry/long/path/that/keeps/going/cd_/very/long/path/that/keeps/going/ + + search-here + /and/then/some/more/ + components//and/then/some/more/components//and/... + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-highlights-matched-substring-when-expanded-text-only-visible-.snap.svg b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-highlights-matched-substring-when-expanded-text-only-visible-.snap.svg new file mode 100644 index 0000000000..67899017a3 --- /dev/null +++ b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-highlights-matched-substring-when-expanded-text-only-visible-.snap.svg @@ -0,0 +1,12 @@ + + + + + run: git + + commit + -m "feat: add search" + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-renders-plain-label-when-no-match-short-label-.snap.svg b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-renders-plain-label-when-no-match-short-label-.snap.svg new file mode 100644 index 0000000000..3d858a18af --- /dev/null +++ b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-renders-plain-label-when-no-match-short-label-.snap.svg @@ -0,0 +1,9 @@ + + + + + simple command + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-respects-custom-maxWidth.snap.svg b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-respects-custom-maxWidth.snap.svg new file mode 100644 index 0000000000..3bca3c74e9 --- /dev/null +++ b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-respects-custom-maxWidth.snap.svg @@ -0,0 +1,9 @@ + + + + + zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz... + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-shows-full-long-label-when-expanded-and-no-match.snap.svg b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-shows-full-long-label-when-expanded-and-no-match.snap.svg new file mode 100644 index 0000000000..283466b773 --- /dev/null +++ b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-shows-full-long-label-when-expanded-and-no-match.snap.svg @@ -0,0 +1,10 @@ + + + + + yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy + yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-truncates-long-label-when-collapsed-and-no-match.snap.svg b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-truncates-long-label-when-collapsed-and-no-match.snap.svg new file mode 100644 index 0000000000..79e13d7486 --- /dev/null +++ b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-truncates-long-label-when-collapsed-and-no-match.snap.svg @@ -0,0 +1,10 @@ + + + + + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx... + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-truncates-match-itself-when-match-is-very-long.snap.svg b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-truncates-match-itself-when-match-is-very-long.snap.svg new file mode 100644 index 0000000000..3eeb5c3250 --- /dev/null +++ b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-truncates-match-itself-when-match-is-very-long.snap.svg @@ -0,0 +1,12 @@ + + + + + + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx... + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText.test.tsx.snap b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText.test.tsx.snap index 7baf47e628..8716c962ea 100644 --- a/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText.test.tsx.snap +++ b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText.test.tsx.snap @@ -2,39 +2,26 @@ exports[`ExpandableText > creates centered window around match when collapsed 1`] = ` "...ry/long/path/that/keeps/going/cd_/very/long/path/that/keeps/going/search-here/and/then/some/more/ -components//and/then/some/more/components//and/... -" +components//and/then/some/more/components//and/..." `; -exports[`ExpandableText > highlights matched substring when expanded (text only visible) 1`] = ` -"run: git commit -m "feat: add search" -" -`; +exports[`ExpandableText > highlights matched substring when expanded (text only visible) 1`] = `"run: git commit -m "feat: add search""`; -exports[`ExpandableText > renders plain label when no match (short label) 1`] = ` -"simple command -" -`; +exports[`ExpandableText > renders plain label when no match (short label) 1`] = `"simple command"`; -exports[`ExpandableText > respects custom maxWidth 1`] = ` -"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz... -" -`; +exports[`ExpandableText > respects custom maxWidth 1`] = `"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz..."`; exports[`ExpandableText > shows full long label when expanded and no match 1`] = ` "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy -yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy -" +yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy" `; exports[`ExpandableText > truncates long label when collapsed and no match 1`] = ` "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx... -" +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx..." `; exports[`ExpandableText > truncates match itself when match is very long 1`] = ` "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx... -" +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx..." `; diff --git a/packages/cli/src/ui/components/shared/__snapshots__/HalfLinePaddedBox.test.tsx.snap b/packages/cli/src/ui/components/shared/__snapshots__/HalfLinePaddedBox.test.tsx.snap index 5dcbfda73d..dbb9af2991 100644 --- a/packages/cli/src/ui/components/shared/__snapshots__/HalfLinePaddedBox.test.tsx.snap +++ b/packages/cli/src/ui/components/shared/__snapshots__/HalfLinePaddedBox.test.tsx.snap @@ -2,7 +2,7 @@ exports[` > renders iTerm2-specific blocks when iTerm2 is detected 1`] = ` "▄▄▄▄▄▄▄▄▄▄ -Content +Content ▀▀▀▀▀▀▀▀▀▀ " `; @@ -19,7 +19,7 @@ exports[` > renders nothing when useBackgroundColor is fals exports[` > renders standard background and blocks when not iTerm2 1`] = ` "▀▀▀▀▀▀▀▀▀▀ -Content +Content ▄▄▄▄▄▄▄▄▄▄ " `; diff --git a/packages/cli/src/ui/components/shared/__snapshots__/SearchableList.test.tsx.snap b/packages/cli/src/ui/components/shared/__snapshots__/SearchableList.test.tsx.snap index 35f21daee3..803ec8dd98 100644 --- a/packages/cli/src/ui/components/shared/__snapshots__/SearchableList.test.tsx.snap +++ b/packages/cli/src/ui/components/shared/__snapshots__/SearchableList.test.tsx.snap @@ -7,7 +7,7 @@ exports[`SearchableList > should match snapshot 1`] = ` │ Search... │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ - ● Item One + ● Item One Description for item one Item Two @@ -28,7 +28,7 @@ exports[`SearchableList > should reset selection to top when items change if res Item One Description for item one - ● Item Two + ● Item Two Description for item two Item Three @@ -43,7 +43,7 @@ exports[`SearchableList > should reset selection to top when items change if res │ One │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ - ● Item One + ● Item One Description for item one " `; @@ -55,7 +55,7 @@ exports[`SearchableList > should reset selection to top when items change if res │ Search... │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ - ● Item One + ● Item One Description for item one Item Two diff --git a/packages/cli/src/ui/hooks/usePhraseCycler.test.tsx b/packages/cli/src/ui/hooks/usePhraseCycler.test.tsx index 837d953c3c..ca89c623ac 100644 --- a/packages/cli/src/ui/hooks/usePhraseCycler.test.tsx +++ b/packages/cli/src/ui/hooks/usePhraseCycler.test.tsx @@ -337,7 +337,7 @@ describe('usePhraseCycler', () => { await act(async () => { setStateExternally?.({ isActive: true, - customPhrases: [], + customPhrases: [] as string[], }); }); await waitUntilReady(); diff --git a/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx b/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx index 98a63b6838..8dddb69f82 100644 --- a/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx +++ b/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx @@ -235,7 +235,7 @@ Another paragraph. ); await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); - expect(lastFrame()).not.toContain(' 1 '); + expect(lastFrame()).not.toContain('1 const x = 1;'); unmount(); }); @@ -246,7 +246,7 @@ Another paragraph. ); await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); - expect(lastFrame()).toContain(' 1 '); + expect(lastFrame()).toContain('1 const x = 1;'); unmount(); }); }); diff --git a/packages/cli/test-setup.ts b/packages/cli/test-setup.ts index 541f7a6a72..dc75dd217b 100644 --- a/packages/cli/test-setup.ts +++ b/packages/cli/test-setup.ts @@ -23,6 +23,9 @@ if (process.env.NO_COLOR !== undefined) { delete process.env.NO_COLOR; } +// Force true color output for ink so that snapshots always include color information. +process.env.FORCE_COLOR = '3'; + import './src/test-utils/customMatchers.js'; let consoleErrorSpy: vi.SpyInstance; diff --git a/packages/core/src/utils/fileUtils.test.ts b/packages/core/src/utils/fileUtils.test.ts index c2f413a27e..de668db3ad 100644 --- a/packages/core/src/utils/fileUtils.test.ts +++ b/packages/core/src/utils/fileUtils.test.ts @@ -20,7 +20,7 @@ import fsPromises from 'node:fs/promises'; import path from 'node:path'; import os from 'node:os'; import { fileURLToPath } from 'node:url'; -// eslint-disable-next-line import/no-internal-modules + import mime from 'mime/lite'; import { diff --git a/packages/core/src/utils/fileUtils.ts b/packages/core/src/utils/fileUtils.ts index a5b32a3cb4..42119c3f18 100644 --- a/packages/core/src/utils/fileUtils.ts +++ b/packages/core/src/utils/fileUtils.ts @@ -8,7 +8,7 @@ import fs from 'node:fs'; import fsPromises from 'node:fs/promises'; import path from 'node:path'; import type { PartUnion } from '@google/genai'; -// eslint-disable-next-line import/no-internal-modules + import mime from 'mime/lite'; import type { FileSystemService } from '../services/fileSystemService.js'; import { ToolErrorType } from '../tools/tool-error.js'; diff --git a/packages/core/src/utils/schemaValidator.ts b/packages/core/src/utils/schemaValidator.ts index e58b7b8d9b..db5dee11ba 100644 --- a/packages/core/src/utils/schemaValidator.ts +++ b/packages/core/src/utils/schemaValidator.ts @@ -6,7 +6,7 @@ import AjvPkg, { type AnySchema, type Ajv } from 'ajv'; // Ajv2020 is the documented way to use draft-2020-12: https://ajv.js.org/json-schema.html#draft-2020-12 -// eslint-disable-next-line import/no-internal-modules + import Ajv2020Pkg from 'ajv/dist/2020.js'; import * as addFormats from 'ajv-formats'; import { debugLogger } from './debugLogger.js';