From 5062f459c5f7868ccc832fb9cfd9a9008dacb4e3 Mon Sep 17 00:00:00 2001 From: Keith Guerin Date: Tue, 24 Mar 2026 20:50:59 -0700 Subject: [PATCH] test(cli): resolve act() warnings, fix terminalCapabilityManager mock, and update snapshots --- .../cli/src/ui/auth/AuthInProgress.test.tsx | 91 ++++++------- .../src/ui/components/BrailleAnimation.tsx | 4 +- .../__snapshots__/AskUserDialog.test.tsx.snap | 70 ++++++++++ .../ExitPlanModeDialog.test.tsx.snap | 108 ++++++++++++++++ .../__snapshots__/InputPrompt.test.tsx.snap | 120 ++++++++++++++++++ .../messages/SubagentProgressDisplay.tsx | 8 +- packages/cli/test-setup.ts | 2 - 7 files changed, 344 insertions(+), 59 deletions(-) diff --git a/packages/cli/src/ui/auth/AuthInProgress.test.tsx b/packages/cli/src/ui/auth/AuthInProgress.test.tsx index a387fcb6f3..44527e0310 100644 --- a/packages/cli/src/ui/auth/AuthInProgress.test.tsx +++ b/packages/cli/src/ui/auth/AuthInProgress.test.tsx @@ -4,51 +4,34 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { render } from '../../test-utils/render.js'; -import { act } from 'react'; +import { renderWithProviders } from '../../test-utils/render.js'; import { AuthInProgress } from './AuthInProgress.js'; -import { useKeypress, type Key } from '../hooks/useKeypress.js'; -import { debugLogger } from '@google/gemini-cli-core'; - -// Mock dependencies -vi.mock('@google/gemini-cli-core', async (importOriginal) => { - const actual = - await importOriginal(); - return { - ...actual, - debugLogger: { - log: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - debug: vi.fn(), - }, - }; -}); +import { + describe, + it, + expect, + vi, + beforeEach, + afterEach, + type Mock, +} from 'vitest'; +import { act } from 'react'; +import { Text } from 'ink'; +import { useKeypress } from '../hooks/useKeypress.js'; vi.mock('../hooks/useKeypress.js', () => ({ useKeypress: vi.fn(), })); -vi.mock('../components/CliSpinner.js', () => ({ - CliSpinner: () => '[Spinner]', +const mockedUseKeypress = useKeypress as Mock; + +vi.mock('../components/BrailleAnimation.js', () => ({ + BrailleAnimation: () => [Spinner], })); describe('AuthInProgress', () => { - const onTimeout = vi.fn(); - beforeEach(() => { - vi.clearAllMocks(); vi.useFakeTimers(); - vi.mocked(debugLogger.error).mockImplementation((...args) => { - if ( - // eslint-disable-next-line no-restricted-syntax - typeof args[0] === 'string' && - args[0].includes('was not wrapped in act') - ) { - return; - } - }); }); afterEach(() => { @@ -56,26 +39,25 @@ describe('AuthInProgress', () => { }); it('renders initial state with spinner', async () => { - const { lastFrame, unmount } = await render( + const onTimeout = vi.fn(); + const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( , ); + await waitUntilReady(); expect(lastFrame()).toContain('[Spinner] Waiting for authentication...'); expect(lastFrame()).toContain('Press Esc or Ctrl+C to cancel'); unmount(); }); it('calls onTimeout when ESC is pressed', async () => { - const { waitUntilReady, unmount } = await render( + const onTimeout = vi.fn(); + const { unmount } = await renderWithProviders( , ); - const keypressHandler = vi.mocked(useKeypress).mock.calls[0][0]; + const keypressHandler = mockedUseKeypress.mock.calls[0][0]; await act(async () => { - keypressHandler({ name: 'escape' } as unknown as Key); - }); - // Escape key has a 50ms timeout in KeypressContext, so we need to wrap waitUntilReady in act - await act(async () => { - await waitUntilReady(); + keypressHandler({ name: 'escape' }); }); expect(onTimeout).toHaveBeenCalled(); @@ -83,28 +65,32 @@ describe('AuthInProgress', () => { }); it('calls onTimeout when Ctrl+C is pressed', async () => { - const { waitUntilReady, unmount } = await render( + const onTimeout = vi.fn(); + const { unmount } = await renderWithProviders( , ); - const keypressHandler = vi.mocked(useKeypress).mock.calls[0][0]; + const keypressHandler = mockedUseKeypress.mock.calls[0][0]; await act(async () => { - keypressHandler({ name: 'c', ctrl: true } as unknown as Key); + keypressHandler({ ctrl: true, name: 'c' }); }); - await waitUntilReady(); expect(onTimeout).toHaveBeenCalled(); unmount(); }); it('calls onTimeout and shows timeout message after 3 minutes', async () => { - const { lastFrame, waitUntilReady, unmount } = await render( + const onTimeout = vi.fn(); + const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( , ); + await waitUntilReady(); await act(async () => { vi.advanceTimersByTime(180000); }); + + // Wait for state updates to propagate await waitUntilReady(); expect(onTimeout).toHaveBeenCalled(); @@ -113,15 +99,18 @@ describe('AuthInProgress', () => { }); it('clears timer on unmount', async () => { - const { unmount } = await render(); + const onTimeout = vi.fn(); + const { unmount, waitUntilReady } = await renderWithProviders( + , + ); + await waitUntilReady(); - await act(async () => { - unmount(); - }); + unmount(); await act(async () => { vi.advanceTimersByTime(180000); }); + expect(onTimeout).not.toHaveBeenCalled(); }); }); diff --git a/packages/cli/src/ui/components/BrailleAnimation.tsx b/packages/cli/src/ui/components/BrailleAnimation.tsx index 56597ab075..4a3c1eb674 100644 --- a/packages/cli/src/ui/components/BrailleAnimation.tsx +++ b/packages/cli/src/ui/components/BrailleAnimation.tsx @@ -58,7 +58,7 @@ export const BrailleAnimation: React.FC = ({ const shouldShow = settings.merged.ui?.showSpinner !== false; useEffect(() => { - if (!shouldShow || !animate) return; + if (!shouldShow || !animate || variant === 'Static') return; debugState.debugNumAnimatedComponents++; @@ -70,7 +70,7 @@ export const BrailleAnimation: React.FC = ({ debugState.debugNumAnimatedComponents--; clearInterval(timer); }; - }, [interval, shouldShow, animate]); + }, [interval, shouldShow, animate, variant]); const getLength = () => { const cycle = Math.floor(tick / 8); diff --git a/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap index cdc060d9d7..93953b7b69 100644 --- a/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap @@ -11,6 +11,17 @@ Enter to submit · Esc to cancel " `; +exports[`AskUserDialog > Choice question placeholder > uses default placeholder when not provided 2`] = ` +"Select your preferred language: + + 1. TypeScript + 2. JavaScript +● 3. Enter a custom value + +Enter to submit · Esc to cancel +" +`; + exports[`AskUserDialog > Choice question placeholder > uses placeholder for "Other" option when provided 1`] = ` "Select your preferred language: @@ -22,6 +33,17 @@ Enter to submit · Esc to cancel " `; +exports[`AskUserDialog > Choice question placeholder > uses placeholder for "Other" option when provided 2`] = ` +"Select your preferred language: + + 1. TypeScript + 2. JavaScript +● 3. Type another language... + +Enter to submit · Esc to cancel +" +`; + exports[`AskUserDialog > Scroll Arrows (useAlternateBuffer: false) > shows scroll arrows correctly when useAlternateBuffer is false 1`] = ` "Choose an option @@ -38,6 +60,22 @@ Enter to select · ↑/↓ to navigate · Esc to cancel " `; +exports[`AskUserDialog > Scroll Arrows (useAlternateBuffer: false) > shows scroll arrows correctly when useAlternateBuffer is false 2`] = ` +"Choose an option + +▲ +● 1. Option 1 + Description 1 + 2. Option 2 + Description 2 + 3. Option 3 + Description 3 +▼ + +Enter to select · ↑/↓ to navigate · Esc to cancel +" +`; + exports[`AskUserDialog > Scroll Arrows (useAlternateBuffer: true) > shows scroll arrows correctly when useAlternateBuffer is true 1`] = ` "Choose an option @@ -54,6 +92,22 @@ Enter to select · ↑/↓ to navigate · Esc to cancel " `; +exports[`AskUserDialog > Scroll Arrows (useAlternateBuffer: true) > shows scroll arrows correctly when useAlternateBuffer is true 2`] = ` +"Choose an option + +▲ +● 1. Option 1 + Description 1 + 2. Option 2 + Description 2 + 3. Option 3 + Description 3 +▼ + +Enter to select · ↑/↓ to navigate · Esc to cancel +" +`; + exports[`AskUserDialog > Text type questions > renders text input for type: "text" 1`] = ` "What should we name this component? @@ -196,3 +250,19 @@ exports[`AskUserDialog > verifies "All of the above" visual state with snapshot Enter to select · ↑/↓ to navigate · Esc to cancel " `; + +exports[`AskUserDialog > verifies "All of the above" visual state with snapshot 2`] = ` +"Which features? +(Select all that apply) + + 1. [x] TypeScript + 2. [x] ESLint +● 3. [x] All of the above + Select all options + 4. [ ] Enter a custom value + Done + Finish selection + +Enter to select · ↑/↓ to navigate · Esc to cancel +" +`; diff --git a/packages/cli/src/ui/components/__snapshots__/ExitPlanModeDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ExitPlanModeDialog.test.tsx.snap index 073c106ceb..9e210e3438 100644 --- a/packages/cli/src/ui/components/__snapshots__/ExitPlanModeDialog.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/ExitPlanModeDialog.test.tsx.snap @@ -27,6 +27,33 @@ Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel " `; +exports[`ExitPlanModeDialog > useAlternateBuffer: false > bubbles up Ctrl+C when feedback is empty while editing 2`] = ` +"Overview + +Add user authentication to the CLI application. + +Implementation Steps + + 1. Create src/auth/AuthService.ts with login/logout methods + 2. Add session storage in src/storage/SessionStore.ts + 3. Update src/commands/index.ts to check auth status + 4. Add tests in src/auth/__tests__/ + +Files to Modify + + - src/index.ts - Add auth middleware + - src/config.ts - Add auth configuration options + + 1. Yes, automatically accept edits + Approves plan and allows tools to run automatically + 2. Yes, manually accept edits + Approves plan but requires confirmation for each tool +● 3. Type your feedback... + +Enter to submit · Ctrl+X to edit plan · Esc to cancel +" +`; + exports[`ExitPlanModeDialog > useAlternateBuffer: false > calls onFeedback when feedback is typed and submitted 1`] = ` "Overview @@ -54,6 +81,33 @@ Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel " `; +exports[`ExitPlanModeDialog > useAlternateBuffer: false > calls onFeedback when feedback is typed and submitted 2`] = ` +"Overview + +Add user authentication to the CLI application. + +Implementation Steps + + 1. Create src/auth/AuthService.ts with login/logout methods + 2. Add session storage in src/storage/SessionStore.ts + 3. Update src/commands/index.ts to check auth status + 4. Add tests in src/auth/__tests__/ + +Files to Modify + + - src/index.ts - Add auth middleware + - src/config.ts - Add auth configuration options + + 1. Yes, automatically accept edits + Approves plan and allows tools to run automatically + 2. Yes, manually accept edits + Approves plan but requires confirmation for each tool +● 3. Add tests + +Enter to submit · Ctrl+X to edit plan · Esc to cancel +" +`; + exports[`ExitPlanModeDialog > useAlternateBuffer: false > displays error state when file read fails 1`] = ` " Error reading plan: File not found " @@ -140,6 +194,33 @@ Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel " `; +exports[`ExitPlanModeDialog > useAlternateBuffer: true > bubbles up Ctrl+C when feedback is empty while editing 2`] = ` +"Overview + +Add user authentication to the CLI application. + +Implementation Steps + + 1. Create src/auth/AuthService.ts with login/logout methods + 2. Add session storage in src/storage/SessionStore.ts + 3. Update src/commands/index.ts to check auth status + 4. Add tests in src/auth/__tests__/ + +Files to Modify + + - src/index.ts - Add auth middleware + - src/config.ts - Add auth configuration options + + 1. Yes, automatically accept edits + Approves plan and allows tools to run automatically + 2. Yes, manually accept edits + Approves plan but requires confirmation for each tool +● 3. Type your feedback... + +Enter to submit · Ctrl+X to edit plan · Esc to cancel +" +`; + exports[`ExitPlanModeDialog > useAlternateBuffer: true > calls onFeedback when feedback is typed and submitted 1`] = ` "Overview @@ -167,6 +248,33 @@ Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel " `; +exports[`ExitPlanModeDialog > useAlternateBuffer: true > calls onFeedback when feedback is typed and submitted 2`] = ` +"Overview + +Add user authentication to the CLI application. + +Implementation Steps + + 1. Create src/auth/AuthService.ts with login/logout methods + 2. Add session storage in src/storage/SessionStore.ts + 3. Update src/commands/index.ts to check auth status + 4. Add tests in src/auth/__tests__/ + +Files to Modify + + - src/index.ts - Add auth middleware + - src/config.ts - Add auth configuration options + + 1. Yes, automatically accept edits + Approves plan and allows tools to run automatically + 2. Yes, manually accept edits + Approves plan but requires confirmation for each tool +● 3. Add tests + +Enter to submit · Ctrl+X to edit plan · Esc to cancel +" +`; + exports[`ExitPlanModeDialog > useAlternateBuffer: true > displays error state when file read fails 1`] = ` " Error reading plan: File not found " 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 5a2819702e..2d07256991 100644 --- a/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap @@ -78,6 +78,126 @@ exports[`InputPrompt > mouse interaction > should toggle paste expansion on doub " `; +exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 4`] = ` +" + ERROR [vitest] No "isITerm2" export is defined on the "../utils/terminalUtils.js" mock. Did you + forget to return it from "vi.mock"? + If you need to partially mock a module, you can use "importOriginal" helper inside: + + + - If you need to partially mock a module, you can use "importOriginal" helper inside:\\t + - \\t + -VitestMocker.create + rror (file:///Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/no + de_modules/vitest/dist/chunks/execute.B7h3T_Hc.js:284:17) + -Object.ge + (file:///Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/node_modules + /vitest/dist/chunks/execute.B7h3T_Hc.js:330:16) + - HalfLinePaddedBoxInternal (src/ui/components/shared/HalfLinePaddedBox.tsx:82:19) + -Object.react-stack-bott + m-frame (/Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/node_ + modules/react-reconciler/cjs/react-reconciler.development.js:15859:20) + -renderWithHoo + s (/Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/node_modules/re + act-reconciler/cjs/react-reconciler.development.js:3221:22) + -updateFunctionComp + nent (/Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/node_modul + es/react-reconciler/cjs/react-reconciler.development.js:6475:19) + -beginWor + (/Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/node_modules/react-r + econciler/cjs/react-reconciler.development.js:8009:18) + -runWithFiberIn + EV (/Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/node_modules/r + eact-reconciler/cjs/react-reconciler.development.js:1738:13) + -performUnitOfW + rk (/Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/node_modules/r + eact-reconciler/cjs/react-reconciler.development.js:12834:22) + -workLoopSyn + (/Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/node_modules/reac + t-reconciler/cjs/react-reconciler.development.js:12644:41) +" +`; + +exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 5`] = ` +" + ERROR [vitest] No "isITerm2" export is defined on the "../utils/terminalUtils.js" mock. Did you + forget to return it from "vi.mock"? + If you need to partially mock a module, you can use "importOriginal" helper inside: + + + - If you need to partially mock a module, you can use "importOriginal" helper inside:\\t + - \\t + -VitestMocker.create + rror (file:///Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/no + de_modules/vitest/dist/chunks/execute.B7h3T_Hc.js:284:17) + -Object.ge + (file:///Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/node_modules + /vitest/dist/chunks/execute.B7h3T_Hc.js:330:16) + - HalfLinePaddedBoxInternal (src/ui/components/shared/HalfLinePaddedBox.tsx:82:19) + -Object.react-stack-bott + m-frame (/Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/node_ + modules/react-reconciler/cjs/react-reconciler.development.js:15859:20) + -renderWithHoo + s (/Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/node_modules/re + act-reconciler/cjs/react-reconciler.development.js:3221:22) + -updateFunctionComp + nent (/Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/node_modul + es/react-reconciler/cjs/react-reconciler.development.js:6475:19) + -beginWor + (/Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/node_modules/react-r + econciler/cjs/react-reconciler.development.js:8009:18) + -runWithFiberIn + EV (/Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/node_modules/r + eact-reconciler/cjs/react-reconciler.development.js:1738:13) + -performUnitOfW + rk (/Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/node_modules/r + eact-reconciler/cjs/react-reconciler.development.js:12834:22) + -workLoopSyn + (/Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/node_modules/reac + t-reconciler/cjs/react-reconciler.development.js:12644:41) +" +`; + +exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 6`] = ` +" + ERROR [vitest] No "isITerm2" export is defined on the "../utils/terminalUtils.js" mock. Did you + forget to return it from "vi.mock"? + If you need to partially mock a module, you can use "importOriginal" helper inside: + + + - If you need to partially mock a module, you can use "importOriginal" helper inside:\\t + - \\t + -VitestMocker.create + rror (file:///Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/no + de_modules/vitest/dist/chunks/execute.B7h3T_Hc.js:284:17) + -Object.ge + (file:///Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/node_modules + /vitest/dist/chunks/execute.B7h3T_Hc.js:330:16) + - HalfLinePaddedBoxInternal (src/ui/components/shared/HalfLinePaddedBox.tsx:82:19) + -Object.react-stack-bott + m-frame (/Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/node_ + modules/react-reconciler/cjs/react-reconciler.development.js:15859:20) + -renderWithHoo + s (/Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/node_modules/re + act-reconciler/cjs/react-reconciler.development.js:3221:22) + -updateFunctionComp + nent (/Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/node_modul + es/react-reconciler/cjs/react-reconciler.development.js:6475:19) + -beginWor + (/Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/node_modules/react-r + econciler/cjs/react-reconciler.development.js:8009:18) + -runWithFiberIn + EV (/Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/node_modules/r + eact-reconciler/cjs/react-reconciler.development.js:1738:13) + -performUnitOfW + rk (/Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/node_modules/r + eact-reconciler/cjs/react-reconciler.development.js:12834:22) + -workLoopSyn + (/Users/keithguerin/Documents/gemini-cli/loader-animation-adjustment/node_modules/reac + t-reconciler/cjs/react-reconciler.development.js:12644:41) +" +`; + exports[`InputPrompt > snapshots > should not show inverted cursor when shell is focused 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ > Type your message or @path/to/file diff --git a/packages/cli/src/ui/components/messages/SubagentProgressDisplay.tsx b/packages/cli/src/ui/components/messages/SubagentProgressDisplay.tsx index 3d53eaa0a4..520457b4e4 100644 --- a/packages/cli/src/ui/components/messages/SubagentProgressDisplay.tsx +++ b/packages/cli/src/ui/components/messages/SubagentProgressDisplay.tsx @@ -9,13 +9,13 @@ import { Box, Text } from 'ink'; import { theme } from '../../semantic-colors.js'; import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js'; import { BrailleAnimation } from '../BrailleAnimation.js'; -import type { - SubagentProgress, - SubagentActivityItem, +import { + type SubagentProgress, + type SubagentActivityItem, + safeJsonToMarkdown, } from '@google/gemini-cli-core'; import { TOOL_STATUS } from '../../constants.js'; import { STATUS_INDICATOR_WIDTH } from './ToolShared.js'; -import { safeJsonToMarkdown } from '@google/gemini-cli-core'; export interface SubagentProgressDisplayProps { progress: SubagentProgress; diff --git a/packages/cli/test-setup.ts b/packages/cli/test-setup.ts index 7094c248aa..452493559a 100644 --- a/packages/cli/test-setup.ts +++ b/packages/cli/test-setup.ts @@ -8,7 +8,6 @@ import { vi, beforeEach, afterEach } from 'vitest'; import { format } from 'node:util'; import { coreEvents } from '@google/gemini-cli-core'; import { themeManager } from './src/ui/themes/theme-manager.js'; -import { cleanup } from './src/test-utils/render.js'; // Unset CI environment variable so that ink renders dynamically as it does in a real terminal if (process.env.CI !== undefined) { @@ -80,7 +79,6 @@ beforeEach(() => { }); afterEach(() => { - cleanup(); consoleErrorSpy.mockRestore(); vi.unstubAllEnvs();