diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx index 5d2bf20c..a2b9354e 100644 --- a/packages/cli/src/ui/App.test.tsx +++ b/packages/cli/src/ui/App.test.tsx @@ -6,6 +6,7 @@ import type { Mock } from 'vitest'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { waitFor } from '@testing-library/react'; import { renderWithProviders } from '../test-utils/render.js'; import { AppWrapper as App } from './App.js'; import type { @@ -399,9 +400,10 @@ describe('App UI', () => { ); currentUnmount = unmount; - await new Promise((resolve) => setTimeout(resolve, 10)); - - expect(spawn).not.toHaveBeenCalled(); + // Wait for any potential async operations to complete + await waitFor(() => { + expect(spawn).not.toHaveBeenCalled(); + }); }); it('should show a success message when update succeeds', async () => { @@ -427,11 +429,12 @@ describe('App UI', () => { updateEventEmitter.emit('update-success', info); - await new Promise((resolve) => setTimeout(resolve, 10)); - - expect(lastFrame()).toContain( - 'Update successful! The new version will be used on your next run.', - ); + // Wait for the success message to appear + await waitFor(() => { + expect(lastFrame()).toContain( + 'Update successful! The new version will be used on your next run.', + ); + }); }); it('should show an error message when update fails', async () => { @@ -457,11 +460,12 @@ describe('App UI', () => { updateEventEmitter.emit('update-failed', info); - await new Promise((resolve) => setTimeout(resolve, 10)); - - expect(lastFrame()).toContain( - 'Automatic update failed. Please try updating manually', - ); + // Wait for the error message to appear + await waitFor(() => { + expect(lastFrame()).toContain( + 'Automatic update failed. Please try updating manually', + ); + }); }); it('should show an error message when spawn fails', async () => { @@ -489,11 +493,12 @@ describe('App UI', () => { // which is what should be emitted when a spawn error occurs elsewhere. updateEventEmitter.emit('update-failed', info); - await new Promise((resolve) => setTimeout(resolve, 10)); - - expect(lastFrame()).toContain( - 'Automatic update failed. Please try updating manually', - ); + // Wait for the error message to appear + await waitFor(() => { + expect(lastFrame()).toContain( + 'Automatic update failed. Please try updating manually', + ); + }); }); it('should not auto-update if GEMINI_CLI_DISABLE_AUTOUPDATER is true', async () => { @@ -519,9 +524,10 @@ describe('App UI', () => { ); currentUnmount = unmount; - await new Promise((resolve) => setTimeout(resolve, 10)); - - expect(spawn).not.toHaveBeenCalled(); + // Wait for any potential async operations to complete + await waitFor(() => { + expect(spawn).not.toHaveBeenCalled(); + }); }); }); diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index faea0b82..994f2ba3 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -5,7 +5,7 @@ */ import { renderWithProviders } from '../../test-utils/render.js'; -import { waitFor } from '@testing-library/react'; +import { waitFor, act } from '@testing-library/react'; import type { InputPromptProps } from './InputPrompt.js'; import { InputPrompt } from './InputPrompt.js'; import type { TextBuffer } from './shared/text-buffer.js'; @@ -1412,12 +1412,20 @@ describe('InputPrompt', () => { const { stdin, stdout, unmount } = renderWithProviders( , ); - stdin.write('\x12'); + + // Enter reverse search mode with Ctrl+R + act(() => { + stdin.write('\x12'); + }); await wait(); + // Verify reverse search is active expect(stdout.lastFrame()).toContain('(r:)'); - stdin.write('\t'); + // Press Tab to complete the highlighted entry + act(() => { + stdin.write('\t'); + }); await waitFor( () => { diff --git a/packages/cli/src/ui/hooks/useAtCompletion.test.ts b/packages/cli/src/ui/hooks/useAtCompletion.test.ts index c64eeb68..8d35a6cf 100644 --- a/packages/cli/src/ui/hooks/useAtCompletion.test.ts +++ b/packages/cli/src/ui/hooks/useAtCompletion.test.ts @@ -201,12 +201,14 @@ describe('useAtCompletion', () => { }); await realFileSearch.initialize(); + // Mock that returns results immediately but we'll control timing with fake timers const mockFileSearch: FileSearch = { initialize: vi.fn().mockResolvedValue(undefined), - search: vi.fn().mockImplementation(async (...args) => { - await new Promise((resolve) => setTimeout(resolve, 300)); - return realFileSearch.search(...args); - }), + search: vi + .fn() + .mockImplementation(async (...args) => + realFileSearch.search(...args), + ), }; vi.spyOn(FileSearchFactory, 'create').mockReturnValue(mockFileSearch); @@ -216,33 +218,42 @@ describe('useAtCompletion', () => { { initialProps: { pattern: 'a' } }, ); - // Wait for the initial (slow) search to complete + // Wait for the initial search to complete (using real timers) await waitFor(() => { expect(result.current.suggestions.map((s) => s.value)).toEqual([ 'a.txt', ]); }); - // Now, rerender to trigger the second search - rerender({ pattern: 'b' }); + // Now switch to fake timers for precise control of the loading behavior + vi.useFakeTimers(); - // Wait for the loading indicator to appear - await waitFor(() => { - expect(result.current.isLoadingSuggestions).toBe(true); + // Trigger the second search + act(() => { + rerender({ pattern: 'b' }); }); - // Suggestions should be cleared while loading + // Initially, loading should be false (before 200ms timer) + expect(result.current.isLoadingSuggestions).toBe(false); + + // Advance time by exactly 200ms to trigger the loading state + act(() => { + vi.advanceTimersByTime(200); + }); + + // Now loading should be true and suggestions should be cleared + expect(result.current.isLoadingSuggestions).toBe(true); expect(result.current.suggestions).toEqual([]); - // Wait for the final (slow) search to complete - await waitFor( - () => { - expect(result.current.suggestions.map((s) => s.value)).toEqual([ - 'b.txt', - ]); - }, - { timeout: 1000 }, - ); // Increase timeout for the slow search + // Switch back to real timers for the final waitFor + vi.useRealTimers(); + + // Wait for the search results to be processed + await waitFor(() => { + expect(result.current.suggestions.map((s) => s.value)).toEqual([ + 'b.txt', + ]); + }); expect(result.current.isLoadingSuggestions).toBe(false); });