From 6c78eb7a39cc2fee281d50e245ebac8f259ed0a7 Mon Sep 17 00:00:00 2001 From: Tommaso Sciortino Date: Fri, 20 Mar 2026 20:08:29 +0000 Subject: [PATCH] test(cli): refactor tests for async render utilities (#23252) --- .../cli/src/config/extensions/consent.test.ts | 5 +- packages/cli/src/test-utils/render.test.tsx | 53 +- packages/cli/src/test-utils/render.tsx | 63 +- packages/cli/src/ui/App.test.tsx | 134 +-- packages/cli/src/ui/AppContainer.test.tsx | 942 +++++++----------- .../cli/src/ui/IdeIntegrationNudge.test.tsx | 13 +- .../cli/src/ui/auth/ApiAuthDialog.test.tsx | 15 +- packages/cli/src/ui/auth/AuthDialog.test.tsx | 75 +- .../cli/src/ui/auth/AuthInProgress.test.tsx | 17 +- .../src/ui/auth/BannedAccountDialog.test.tsx | 30 +- .../LoginWithGoogleRestartDialog.test.tsx | 9 +- packages/cli/src/ui/auth/useAuth.test.tsx | 203 ++-- .../cli/src/ui/components/AboutBox.test.tsx | 12 +- .../AdminSettingsChangedDialog.test.tsx | 9 +- .../ui/components/AgentConfigDialog.test.tsx | 1 - .../AlternateBufferQuittingDisplay.test.tsx | 18 +- .../cli/src/ui/components/AnsiOutput.test.tsx | 24 +- .../cli/src/ui/components/AppHeader.test.tsx | 26 +- .../components/ApprovalModeIndicator.test.tsx | 18 +- .../src/ui/components/AskUserDialog.test.tsx | 25 +- .../BackgroundShellDisplay.test.tsx | 36 +- .../cli/src/ui/components/Checklist.test.tsx | 15 +- .../src/ui/components/ChecklistItem.test.tsx | 9 +- .../cli/src/ui/components/CliSpinner.test.tsx | 13 +- .../src/ui/components/ColorsDisplay.test.tsx | 3 +- .../cli/src/ui/components/Composer.test.tsx | 3 +- .../ui/components/ConfigInitDisplay.test.tsx | 5 +- .../src/ui/components/ConsentPrompt.test.tsx | 15 +- .../components/ConsoleSummaryDisplay.test.tsx | 6 +- .../components/ContextSummaryDisplay.test.tsx | 3 +- .../components/ContextUsageDisplay.test.tsx | 15 +- .../ui/components/CopyModeWarning.test.tsx | 6 +- .../src/ui/components/DebugProfiler.test.tsx | 12 +- .../DetailedMessagesDisplay.test.tsx | 15 +- .../src/ui/components/DialogManager.test.tsx | 6 +- .../components/EditorSettingsDialog.test.tsx | 11 +- .../ui/components/EmptyWalletDialog.test.tsx | 33 +- .../ui/components/ExitPlanModeDialog.test.tsx | 60 +- .../src/ui/components/ExitWarning.test.tsx | 12 +- .../ui/components/FolderTrustDialog.test.tsx | 51 +- .../cli/src/ui/components/Footer.test.tsx | 728 ++++++-------- .../ui/components/FooterConfigDialog.test.tsx | 23 +- .../GeminiRespondingSpinner.test.tsx | 21 +- .../ui/components/GradientRegression.test.tsx | 28 +- .../cli/src/ui/components/Header.test.tsx | 22 +- packages/cli/src/ui/components/Help.test.tsx | 9 +- .../ui/components/HistoryItemDisplay.test.tsx | 157 ++- .../ui/components/HookStatusDisplay.test.tsx | 12 +- .../src/ui/components/HooksDialog.test.tsx | 28 +- .../components/IdeTrustChangeDialog.test.tsx | 12 +- .../ui/components/LoadingIndicator.test.tsx | 52 +- .../LogoutConfirmationDialog.test.tsx | 9 +- .../LoopDetectionConfirmation.test.tsx | 6 +- .../src/ui/components/MainContent.test.tsx | 73 +- .../ui/components/MemoryUsageDisplay.test.tsx | 8 +- .../src/ui/components/ModelDialog.test.tsx | 1 - .../ui/components/ModelStatsDisplay.test.tsx | 6 +- .../MultiFolderTrustDialog.test.tsx | 24 +- .../components/NewAgentsNotification.test.tsx | 6 +- .../src/ui/components/Notifications.test.tsx | 39 +- .../ui/components/OverageMenuDialog.test.tsx | 44 +- .../PermissionsModifyTrustDialog.test.tsx | 15 +- .../ui/components/PolicyUpdateDialog.test.tsx | 3 +- .../src/ui/components/ProQuotaDialog.test.tsx | 40 +- .../components/QueuedMessageDisplay.test.tsx | 15 +- .../ui/components/QuittingDisplay.test.tsx | 6 +- .../src/ui/components/QuotaDisplay.test.tsx | 27 +- .../components/RawMarkdownIndicator.test.tsx | 10 +- .../ui/components/RewindConfirmation.test.tsx | 12 +- .../src/ui/components/RewindViewer.test.tsx | 38 +- .../src/ui/components/SessionBrowser.test.tsx | 18 +- .../SessionBrowserSearchNav.test.tsx | 23 +- .../SessionBrowserStates.test.tsx | 9 +- .../src/ui/components/SettingsDialog.test.tsx | 146 +-- .../ui/components/ShellInputPrompt.test.tsx | 27 +- .../ui/components/ShellModeIndicator.test.tsx | 5 +- .../src/ui/components/ShortcutsHelp.test.tsx | 5 +- .../src/ui/components/ShowMoreLines.test.tsx | 15 +- .../components/ShowMoreLinesLayout.test.tsx | 6 +- .../src/ui/components/StatsDisplay.test.tsx | 60 +- .../src/ui/components/StatusDisplay.test.tsx | 3 +- .../src/ui/components/StickyHeader.test.tsx | 3 +- .../ui/components/SuggestionsDisplay.test.tsx | 21 +- packages/cli/src/ui/components/Table.test.tsx | 27 +- .../src/ui/components/ThemeDialog.test.tsx | 21 +- .../src/ui/components/ThemedGradient.test.tsx | 3 +- packages/cli/src/ui/components/Tips.test.tsx | 5 +- .../src/ui/components/ToastDisplay.test.tsx | 30 +- .../components/ToolConfirmationQueue.test.tsx | 46 +- .../ui/components/ToolStatsDisplay.test.tsx | 3 +- .../ui/components/UpdateNotification.test.tsx | 3 +- .../src/ui/components/UserIdentity.test.tsx | 21 +- .../ui/components/ValidationDialog.test.tsx | 24 +- .../ConfigInitDisplay.test.tsx.snap | 12 + .../messages/CompressionMessage.test.tsx | 42 +- .../components/messages/ErrorMessage.test.tsx | 6 +- .../messages/GeminiMessage.test.tsx | 9 +- .../components/messages/InfoMessage.test.tsx | 11 +- .../messages/RedirectionConfirmation.test.tsx | 3 +- .../messages/ShellToolMessage.test.tsx | 53 +- .../messages/SubagentGroupDisplay.test.tsx | 6 +- .../messages/SubagentProgressDisplay.test.tsx | 24 +- .../src/ui/components/messages/Todo.test.tsx | 6 +- .../messages/ToolConfirmationMessage.test.tsx | 96 +- .../messages/ToolGroupMessage.test.tsx | 102 +- .../components/messages/ToolMessage.test.tsx | 72 +- .../messages/ToolMessageFocusHint.test.tsx | 3 - .../messages/ToolMessageRawMarkdown.test.tsx | 3 +- .../components/messages/ToolShared.test.tsx | 15 +- .../components/messages/UserMessage.test.tsx | 12 +- .../messages/WarningMessage.test.tsx | 6 +- .../shared/BaseSelectionList.test.tsx | 2 - .../components/shared/EnumSelector.test.tsx | 25 +- .../components/shared/ExpandableText.test.tsx | 35 +- .../shared/HalfLinePaddedBox.test.tsx | 12 +- .../ui/components/shared/MaxSizedBox.test.tsx | 24 +- .../ui/components/shared/Scrollable.test.tsx | 45 +- .../components/shared/SearchableList.test.tsx | 9 +- .../components/shared/SectionHeader.test.tsx | 3 +- .../shared/SlicingMaxSizedBox.test.tsx | 15 +- .../ui/components/shared/TabHeader.test.tsx | 39 +- .../ui/components/shared/TextInput.test.tsx | 39 +- .../shared/VirtualizedList.test.tsx | 31 +- .../ui/components/shared/performance.test.ts | 8 +- .../ui/components/shared/text-buffer.test.ts | 522 +++++----- .../src/ui/components/views/ChatList.test.tsx | 13 +- .../views/ExtensionDetails.test.tsx | 9 +- .../views/ExtensionRegistryView.test.tsx | 3 +- .../components/views/ExtensionsList.test.tsx | 15 +- .../ui/components/views/McpStatus.test.tsx | 52 +- .../ui/components/views/SkillsList.test.tsx | 18 +- .../ui/components/views/ToolsList.test.tsx | 9 +- .../ui/contexts/ScrollProvider.drag.test.tsx | 12 +- .../src/ui/contexts/ScrollProvider.test.tsx | 22 +- .../src/ui/contexts/SessionContext.test.tsx | 20 +- .../src/ui/contexts/SettingsContext.test.tsx | 20 +- .../src/ui/contexts/TerminalContext.test.tsx | 6 +- .../ui/contexts/ToolActionsContext.test.tsx | 40 +- .../ui/hooks/shellCommandProcessor.test.tsx | 74 +- .../ui/hooks/slashCommandProcessor.test.tsx | 2 +- .../src/ui/hooks/useAlternateBuffer.test.ts | 12 +- .../ui/hooks/useAnimatedScrollbar.test.tsx | 26 +- .../ui/hooks/useApprovalModeIndicator.test.ts | 84 +- .../cli/src/ui/hooks/useAtCompletion.test.ts | 122 ++- .../ui/hooks/useAtCompletion_agents.test.ts | 4 +- .../hooks/useBackgroundShellManager.test.tsx | 28 +- packages/cli/src/ui/hooks/useBanner.test.ts | 20 +- .../cli/src/ui/hooks/useBatchedScroll.test.ts | 28 +- .../src/ui/hooks/useConsoleMessages.test.tsx | 24 +- .../src/ui/hooks/useEditorSettings.test.tsx | 40 +- .../src/ui/hooks/useExtensionUpdates.test.tsx | 8 +- .../src/ui/hooks/useFlickerDetector.test.ts | 26 +- .../cli/src/ui/hooks/useFolderTrust.test.ts | 40 +- .../cli/src/ui/hooks/useGeminiStream.test.tsx | 4 + .../src/ui/hooks/useGitBranchName.test.tsx | 143 +-- .../src/ui/hooks/useHistoryManager.test.ts | 44 +- .../src/ui/hooks/useHookDisplayState.test.ts | 24 +- .../src/ui/hooks/useIdeTrustListener.test.tsx | 29 +- .../src/ui/hooks/useIncludeDirsTrust.test.tsx | 18 +- .../src/ui/hooks/useInlineEditBuffer.test.ts | 36 +- .../cli/src/ui/hooks/useInputHistory.test.ts | 56 +- .../src/ui/hooks/useInputHistoryStore.test.ts | 40 +- .../src/ui/hooks/useLoadingIndicator.test.tsx | 36 +- packages/cli/src/ui/hooks/useLogger.test.tsx | 33 +- .../cli/src/ui/hooks/useMcpStatus.test.tsx | 20 +- .../src/ui/hooks/useMemoryMonitor.test.tsx | 12 +- .../cli/src/ui/hooks/useMessageQueue.test.tsx | 58 +- .../cli/src/ui/hooks/useModelCommand.test.tsx | 12 +- packages/cli/src/ui/hooks/useMouse.test.ts | 16 +- .../cli/src/ui/hooks/useMouseClick.test.ts | 4 +- .../hooks/usePermissionsModifyTrust.test.ts | 34 +- .../cli/src/ui/hooks/usePhraseCycler.test.tsx | 41 +- .../src/ui/hooks/usePrivacySettings.test.tsx | 34 +- .../src/ui/hooks/useQuotaAndFallback.test.ts | 62 +- packages/cli/src/ui/hooks/useRewind.test.ts | 20 +- .../src/ui/hooks/useSelectionList.test.tsx | 6 +- .../src/ui/hooks/useSessionBrowser.test.ts | 6 +- .../cli/src/ui/hooks/useSessionResume.test.ts | 46 +- .../ui/hooks/useSettingsNavigation.test.ts | 32 +- .../cli/src/ui/hooks/useShellHistory.test.ts | 14 +- .../ui/hooks/useShellInactivityStatus.test.ts | 14 +- .../src/ui/hooks/useSlashCompletion.test.ts | 533 +++++----- packages/cli/src/ui/hooks/useSuspend.test.ts | 12 +- .../src/ui/hooks/useTabbedNavigation.test.ts | 104 +- .../src/ui/hooks/useTerminalTheme.test.tsx | 33 +- packages/cli/src/ui/hooks/useTimer.test.tsx | 36 +- .../cli/src/ui/hooks/useToolScheduler.test.ts | 42 +- .../ui/hooks/useTurnActivityMonitor.test.ts | 19 +- .../cli/src/ui/hooks/vim-passthrough.test.tsx | 4 +- packages/cli/src/ui/hooks/vim.test.tsx | 498 ++++----- .../src/ui/layouts/DefaultAppLayout.test.tsx | 9 +- .../privacy/CloudFreePrivacyNotice.test.tsx | 9 +- .../privacy/CloudPaidPrivacyNotice.test.tsx | 6 +- .../ui/privacy/GeminiPrivacyNotice.test.tsx | 6 +- .../cli/src/ui/privacy/PrivacyNotice.test.tsx | 3 +- .../cli/src/ui/utils/CodeColorizer.test.tsx | 5 +- .../cli/src/ui/utils/MarkdownDisplay.test.tsx | 48 +- .../cli/src/ui/utils/TableRenderer.test.tsx | 47 +- 198 files changed, 3592 insertions(+), 4802 deletions(-) diff --git a/packages/cli/src/config/extensions/consent.test.ts b/packages/cli/src/config/extensions/consent.test.ts index 76d7227ab4..8de884cdd5 100644 --- a/packages/cli/src/config/extensions/consent.test.ts +++ b/packages/cli/src/config/extensions/consent.test.ts @@ -59,8 +59,9 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { }); async function expectConsentSnapshot(consentString: string) { - const renderResult = render(React.createElement(Text, null, consentString)); - await renderResult.waitUntilReady(); + const renderResult = await render( + React.createElement(Text, null, consentString), + ); await expect(renderResult).toMatchSvgSnapshot(); } diff --git a/packages/cli/src/test-utils/render.test.tsx b/packages/cli/src/test-utils/render.test.tsx index 7172a99119..3c3f4102a4 100644 --- a/packages/cli/src/test-utils/render.test.tsx +++ b/packages/cli/src/test-utils/render.test.tsx @@ -12,24 +12,18 @@ import { waitFor } from './async.js'; describe('render', () => { it('should render a component', async () => { - const { lastFrame, waitUntilReady, unmount } = render( - Hello World, - ); - await waitUntilReady(); + const { lastFrame, unmount } = await render(Hello World); expect(lastFrame()).toBe('Hello World\n'); unmount(); }); it('should support rerender', async () => { - const { lastFrame, rerender, waitUntilReady, unmount } = render( + const { lastFrame, rerender, waitUntilReady, unmount } = await render( Hello, ); - await waitUntilReady(); expect(lastFrame()).toBe('Hello\n'); - await act(async () => { - rerender(World); - }); + await act(async () => rerender(World)); await waitUntilReady(); expect(lastFrame()).toBe('World\n'); unmount(); @@ -42,10 +36,8 @@ describe('render', () => { return Hello; } - const { unmount, waitUntilReady } = render(); - await waitUntilReady(); + const { unmount } = await render(); unmount(); - expect(cleanupMock).toHaveBeenCalled(); }); }); @@ -54,36 +46,27 @@ describe('renderHook', () => { it('should rerender with previous props when called without arguments', async () => { const useTestHook = ({ value }: { value: number }) => { const [count, setCount] = useState(0); - useEffect(() => { - setCount((c) => c + 1); - }, [value]); + useEffect(() => setCount((c) => c + 1), [value]); return { count, value }; }; - const { result, rerender, waitUntilReady, unmount } = renderHook( + const { result, rerender, waitUntilReady, unmount } = await renderHook( useTestHook, - { - initialProps: { value: 1 }, - }, + { initialProps: { value: 1 } }, ); - await waitUntilReady(); expect(result.current.value).toBe(1); await waitFor(() => expect(result.current.count).toBe(1)); // Rerender with new props - await act(async () => { - rerender({ value: 2 }); - }); + await act(async () => rerender({ value: 2 })); await waitUntilReady(); expect(result.current.value).toBe(2); await waitFor(() => expect(result.current.count).toBe(2)); // Rerender without arguments should use previous props (value: 2) // This would previously crash or pass undefined if not fixed - await act(async () => { - rerender(); - }); + await act(async () => rerender()); await waitUntilReady(); expect(result.current.value).toBe(2); // Count should not increase because value didn't change @@ -98,14 +81,11 @@ describe('renderHook', () => { }; const { result, rerender, waitUntilReady, unmount } = - renderHook(useTestHook); - await waitUntilReady(); + await renderHook(useTestHook); expect(result.current.count).toBe(0); - await act(async () => { - rerender(); - }); + await act(async () => rerender()); await waitUntilReady(); expect(result.current.count).toBe(0); unmount(); @@ -113,19 +93,14 @@ describe('renderHook', () => { it('should update props if undefined is passed explicitly', async () => { const useTestHook = (val: string | undefined) => val; - const { result, rerender, waitUntilReady, unmount } = renderHook( + const { result, rerender, waitUntilReady, unmount } = await renderHook( useTestHook, - { - initialProps: 'initial' as string | undefined, - }, + { initialProps: 'initial' }, ); - await waitUntilReady(); expect(result.current).toBe('initial'); - await act(async () => { - rerender(undefined); - }); + await act(async () => rerender(undefined)); await waitUntilReady(); expect(result.current).toBeUndefined(); unmount(); diff --git a/packages/cli/src/test-utils/render.tsx b/packages/cli/src/test-utils/render.tsx index 7d298b120d..ea889181c6 100644 --- a/packages/cli/src/test-utils/render.tsx +++ b/packages/cli/src/test-utils/render.tsx @@ -257,13 +257,9 @@ class XtermStdout extends EventEmitter { return currentFrame !== ''; } - // If both are empty, it's a match. - // We consider undefined lastRenderOutput as effectively empty for this check - // to support hook testing where Ink may skip rendering completely. - if ( - (this.lastRenderOutput === undefined || expectedFrame === '') && - currentFrame === '' - ) { + // If Ink expects nothing (no new static content and no dynamic output), + // we consider it a match because the terminal buffer will just hold the historical static content. + if (expectedFrame === '') { return true; } @@ -271,8 +267,8 @@ class XtermStdout extends EventEmitter { return false; } - // If Ink expects nothing but terminal has content, or vice-versa, it's NOT a match. - if (expectedFrame === '' || currentFrame === '') { + // If the terminal is empty but Ink expects something, it's not a match. + if (currentFrame === '') { return false; } @@ -382,13 +378,11 @@ export type RenderInstance = { const instances: InkInstance[] = []; -// Wrapper around ink's render that ensures act() is called and uses Xterm for output -export const render = ( +export const render = async ( tree: React.ReactElement, terminalWidth?: number, -): Omit< - RenderInstance, - 'capturedOverflowState' | 'capturedOverflowActions' +): Promise< + Omit > => { const cols = terminalWidth ?? 100; // We use 1000 rows to avoid windows with incorrect snapshots if a correct @@ -437,6 +431,8 @@ export const render = ( instances.push(instance); + await stdout.waitUntilReady(); + return { rerender: (newTree: React.ReactElement) => { act(() => { @@ -751,7 +747,10 @@ export const renderWithProviders = async ( ); - const renderResult = render(wrapWithProviders(component), terminalWidth); + const renderResult = await render( + wrapWithProviders(component), + terminalWidth, + ); return { ...renderResult, @@ -765,19 +764,19 @@ export const renderWithProviders = async ( }; }; -export function renderHook( +export async function renderHook( renderCallback: (props: Props) => Result, options?: { initialProps?: Props; wrapper?: React.ComponentType<{ children: React.ReactNode }>; }, -): { +): Promise<{ result: { current: Result }; rerender: (props?: Props) => void; unmount: () => void; waitUntilReady: () => Promise; generateSvg: () => string; -} { +}> { const result = { current: undefined as unknown as Result }; let currentProps = options?.initialProps as Props; @@ -800,17 +799,15 @@ export function renderHook( let waitUntilReady: () => Promise = async () => {}; let generateSvg: () => string = () => ''; - act(() => { - const renderResult = render( - - - , - ); - inkRerender = renderResult.rerender; - unmount = renderResult.unmount; - waitUntilReady = renderResult.waitUntilReady; - generateSvg = renderResult.generateSvg; - }); + const renderResult = await render( + + + , + ); + inkRerender = renderResult.rerender; + unmount = renderResult.unmount; + waitUntilReady = renderResult.waitUntilReady; + generateSvg = renderResult.generateSvg; function rerender(props?: Props) { if (arguments.length > 0) { @@ -864,7 +861,13 @@ export async function renderHookWithProviders( const Wrapper = options.wrapper || (({ children }) => <>{children}); - let renderResult: ReturnType; + let renderResult: RenderInstance & { + simulateClick: ( + col: number, + row: number, + button?: 0 | 1 | 2, + ) => Promise; + }; await act(async () => { renderResult = await renderWithProviders( diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx index 7f5e55c022..950363f6a8 100644 --- a/packages/cli/src/ui/App.test.tsx +++ b/packages/cli/src/ui/App.test.tsx @@ -94,14 +94,10 @@ describe('App', () => { }; it('should render main content and composer when not quitting', async () => { - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( - , - { - uiState: mockUIState, - settings: createMockSettings({ ui: { useAlternateBuffer: false } }), - }, - ); - await waitUntilReady(); + const { lastFrame, unmount } = await renderWithProviders(, { + uiState: mockUIState, + settings: createMockSettings({ ui: { useAlternateBuffer: false } }), + }); expect(lastFrame()).toContain('Tips for getting started'); expect(lastFrame()).toContain('Notifications'); @@ -115,14 +111,10 @@ describe('App', () => { quittingMessages: [{ id: 1, type: 'user', text: 'test' }], } as UIState; - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( - , - { - uiState: quittingUIState, - settings: createMockSettings({ ui: { useAlternateBuffer: false } }), - }, - ); - await waitUntilReady(); + const { lastFrame, unmount } = await renderWithProviders(, { + uiState: quittingUIState, + settings: createMockSettings({ ui: { useAlternateBuffer: false } }), + }); expect(lastFrame()).toContain('Quitting...'); unmount(); @@ -136,14 +128,10 @@ describe('App', () => { pendingHistoryItems: [{ type: 'user', text: 'pending item' }], } as UIState; - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( - , - { - uiState: quittingUIState, - settings: createMockSettings({ ui: { useAlternateBuffer: true } }), - }, - ); - await waitUntilReady(); + const { lastFrame, unmount } = await renderWithProviders(, { + uiState: quittingUIState, + settings: createMockSettings({ ui: { useAlternateBuffer: true } }), + }); expect(lastFrame()).toContain('HistoryItemDisplay'); expect(lastFrame()).toContain('Quitting...'); @@ -156,14 +144,10 @@ describe('App', () => { dialogsVisible: true, } as UIState; - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( - , - { - uiState: dialogUIState, - settings: createMockSettings({ ui: { useAlternateBuffer: true } }), - }, - ); - await waitUntilReady(); + const { lastFrame, unmount } = await renderWithProviders(, { + uiState: dialogUIState, + settings: createMockSettings({ ui: { useAlternateBuffer: true } }), + }); expect(lastFrame()).toContain('Tips for getting started'); expect(lastFrame()).toContain('Notifications'); @@ -183,14 +167,10 @@ describe('App', () => { [stateKey]: true, } as UIState; - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( - , - { - uiState, - settings: createMockSettings({ ui: { useAlternateBuffer: true } }), - }, - ); - await waitUntilReady(); + const { lastFrame, unmount } = await renderWithProviders(, { + uiState, + settings: createMockSettings({ ui: { useAlternateBuffer: true } }), + }); expect(lastFrame()).toContain(`Press Ctrl+${key} again to exit.`); unmount(); @@ -200,14 +180,10 @@ describe('App', () => { it('should render ScreenReaderAppLayout when screen reader is enabled', async () => { (useIsScreenReaderEnabled as Mock).mockReturnValue(true); - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( - , - { - uiState: mockUIState, - settings: createMockSettings({ ui: { useAlternateBuffer: true } }), - }, - ); - await waitUntilReady(); + const { lastFrame, unmount } = await renderWithProviders(, { + uiState: mockUIState, + settings: createMockSettings({ ui: { useAlternateBuffer: true } }), + }); expect(lastFrame()).toContain('Notifications'); expect(lastFrame()).toContain('Footer'); @@ -219,14 +195,10 @@ describe('App', () => { it('should render DefaultAppLayout when screen reader is not enabled', async () => { (useIsScreenReaderEnabled as Mock).mockReturnValue(false); - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( - , - { - uiState: mockUIState, - settings: createMockSettings({ ui: { useAlternateBuffer: true } }), - }, - ); - await waitUntilReady(); + const { lastFrame, unmount } = await renderWithProviders(, { + uiState: mockUIState, + settings: createMockSettings({ ui: { useAlternateBuffer: true } }), + }); expect(lastFrame()).toContain('Tips for getting started'); expect(lastFrame()).toContain('Notifications'); @@ -274,15 +246,11 @@ describe('App', () => { vi.spyOn(configWithExperiment, 'isTrustedFolder').mockReturnValue(true); vi.spyOn(configWithExperiment, 'getIdeMode').mockReturnValue(false); - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( - , - { - uiState: stateWithConfirmingTool, - config: configWithExperiment, - settings: createMockSettings({ ui: { useAlternateBuffer: true } }), - }, - ); - await waitUntilReady(); + const { lastFrame, unmount } = await renderWithProviders(, { + uiState: stateWithConfirmingTool, + config: configWithExperiment, + settings: createMockSettings({ ui: { useAlternateBuffer: true } }), + }); expect(lastFrame()).toContain('Tips for getting started'); expect(lastFrame()).toContain('Notifications'); @@ -295,28 +263,20 @@ describe('App', () => { describe('Snapshots', () => { it('renders default layout correctly', async () => { (useIsScreenReaderEnabled as Mock).mockReturnValue(false); - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( - , - { - uiState: mockUIState, - settings: createMockSettings({ ui: { useAlternateBuffer: true } }), - }, - ); - await waitUntilReady(); + const { lastFrame, unmount } = await renderWithProviders(, { + uiState: mockUIState, + settings: createMockSettings({ ui: { useAlternateBuffer: true } }), + }); expect(lastFrame()).toMatchSnapshot(); unmount(); }); it('renders screen reader layout correctly', async () => { (useIsScreenReaderEnabled as Mock).mockReturnValue(true); - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( - , - { - uiState: mockUIState, - settings: createMockSettings({ ui: { useAlternateBuffer: true } }), - }, - ); - await waitUntilReady(); + const { lastFrame, unmount } = await renderWithProviders(, { + uiState: mockUIState, + settings: createMockSettings({ ui: { useAlternateBuffer: true } }), + }); expect(lastFrame()).toMatchSnapshot(); unmount(); }); @@ -326,14 +286,10 @@ describe('App', () => { ...mockUIState, dialogsVisible: true, } as UIState; - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( - , - { - uiState: dialogUIState, - settings: createMockSettings({ ui: { useAlternateBuffer: true } }), - }, - ); - await waitUntilReady(); + const { lastFrame, unmount } = await renderWithProviders(, { + uiState: dialogUIState, + settings: createMockSettings({ ui: { useAlternateBuffer: true } }), + }); expect(lastFrame()).toMatchSnapshot(); unmount(); }); diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx index 650804025b..313573a573 100644 --- a/packages/cli/src/ui/AppContainer.test.tsx +++ b/packages/cli/src/ui/AppContainer.test.tsx @@ -16,7 +16,7 @@ import { } from 'vitest'; import { render, cleanup, persistentStateMock } from '../test-utils/render.js'; import { waitFor } from '../test-utils/async.js'; -import { act, useContext, type ReactElement } from 'react'; +import { act, useContext } from 'react'; import { AppContainer } from './AppContainer.js'; import { SettingsContext } from './contexts/SettingsContext.js'; import { type TrackedToolCall } from './hooks/useToolScheduler.js'; @@ -250,6 +250,15 @@ describe('AppContainer State Management', () => { let mockInitResult: InitializationResult; let mockExtensionManager: MockedObject; + type AppContainerProps = { + settings?: LoadedSettings; + config?: Config; + version?: string; + initResult?: InitializationResult; + startupWarnings?: StartupWarning[]; + resumedSessionData?: ResumedSessionData; + }; + // Helper to generate the AppContainer JSX for render and rerender const getAppContainer = ({ settings = mockSettings, @@ -258,14 +267,7 @@ describe('AppContainer State Management', () => { initResult = mockInitResult, startupWarnings, resumedSessionData, - }: { - settings?: LoadedSettings; - config?: Config; - version?: string; - initResult?: InitializationResult; - startupWarnings?: StartupWarning[]; - resumedSessionData?: ResumedSessionData; - } = {}) => ( + }: AppContainerProps = {}) => ( @@ -282,7 +284,7 @@ describe('AppContainer State Management', () => { ); // Helper to render the AppContainer - const renderAppContainer = (props?: Parameters[0]) => + const renderAppContainer = async (props?: AppContainerProps) => render(getAppContainer(props)); // Create typed mocks for all hooks @@ -514,13 +516,9 @@ describe('AppContainer State Management', () => { describe('Basic Rendering', () => { it('renders without crashing with minimal props', async () => { - let unmount: () => void; - await act(async () => { - const result = renderAppContainer(); - unmount = result.unmount; - }); - await waitFor(() => expect(capturedUIState).toBeTruthy()); - unmount!(); + const { unmount } = await act(async () => renderAppContainer()); + expect(capturedUIState).toBeTruthy(); + unmount(); }); it('renders with startup warnings', async () => { @@ -537,44 +535,32 @@ describe('AppContainer State Management', () => { }, ]; - let unmount: () => void; - await act(async () => { - const result = renderAppContainer({ startupWarnings }); - unmount = result.unmount; - }); - await waitFor(() => expect(capturedUIState).toBeTruthy()); - unmount!(); + const { unmount } = await act(async () => + renderAppContainer({ startupWarnings }), + ); + expect(capturedUIState).toBeTruthy(); + unmount(); }); it('shows full UI details by default', async () => { - let unmount: () => void; - await act(async () => { - const result = renderAppContainer(); - unmount = result.unmount; - }); + const { unmount } = await act(async () => renderAppContainer()); - await waitFor(() => { - expect(capturedUIState.cleanUiDetailsVisible).toBe(true); - }); - unmount!(); + expect(capturedUIState.cleanUiDetailsVisible).toBe(true); + unmount(); }); it('starts in minimal UI mode when Focus UI preference is persisted', async () => { persistentStateMock.get.mockReturnValueOnce(true); - let unmount: () => void; - await act(async () => { - const result = renderAppContainer({ + const { unmount } = await act(async () => + renderAppContainer({ settings: mockSettings, - }); - unmount = result.unmount; - }); + }), + ); - await waitFor(() => { - expect(capturedUIState.cleanUiDetailsVisible).toBe(false); - }); + expect(capturedUIState.cleanUiDetailsVisible).toBe(false); expect(persistentStateMock.get).toHaveBeenCalledWith('focusUiEnabled'); - unmount!(); + unmount(); }); }); @@ -609,15 +595,9 @@ describe('AppContainer State Management', () => { ], }); - let unmount: (() => void) | undefined; - await act(async () => { - const rendered = renderAppContainer(); - unmount = rendered.unmount; - }); + const { unmount } = await act(async () => renderAppContainer()); - await waitFor(() => - expect(terminalNotificationsMocks.notifyViaTerminal).toHaveBeenCalled(), - ); + expect(terminalNotificationsMocks.notifyViaTerminal).toHaveBeenCalled(); expect( terminalNotificationsMocks.buildRunEventNotificationContent, ).toHaveBeenCalledWith( @@ -626,9 +606,7 @@ describe('AppContainer State Management', () => { }), ); - await act(async () => { - unmount?.(); - }); + unmount(); }); it('does not send attention notification when terminal is focused', async () => { @@ -661,19 +639,13 @@ describe('AppContainer State Management', () => { ], }); - let unmount: (() => void) | undefined; - await act(async () => { - const rendered = renderAppContainer(); - unmount = rendered.unmount; - }); + const { unmount } = await act(async () => renderAppContainer()); expect( terminalNotificationsMocks.notifyViaTerminal, ).not.toHaveBeenCalled(); - await act(async () => { - unmount?.(); - }); + unmount(); }); it('sends attention notification when focus reporting is unavailable', async () => { @@ -706,19 +678,11 @@ describe('AppContainer State Management', () => { ], }); - let unmount: (() => void) | undefined; - await act(async () => { - const rendered = renderAppContainer(); - unmount = rendered.unmount; - }); + const { unmount } = await act(async () => renderAppContainer()); - await waitFor(() => - expect(terminalNotificationsMocks.notifyViaTerminal).toHaveBeenCalled(), - ); + expect(terminalNotificationsMocks.notifyViaTerminal).toHaveBeenCalled(); - await act(async () => { - unmount?.(); - }); + unmount(); }); it('sends a macOS notification when a response completes while unfocused', async () => { @@ -732,35 +696,24 @@ describe('AppContainer State Management', () => { streamingState: currentStreamingState, })); - let unmount: (() => void) | undefined; - let rerender: ((tree: ReactElement) => void) | undefined; - - await act(async () => { - const rendered = renderAppContainer(); - unmount = rendered.unmount; - rerender = rendered.rerender; - }); + const { unmount, rerender } = await act(async () => renderAppContainer()); currentStreamingState = 'idle'; await act(async () => { - rerender?.(getAppContainer()); + rerender(getAppContainer()); }); - await waitFor(() => - expect( - terminalNotificationsMocks.buildRunEventNotificationContent, - ).toHaveBeenCalledWith( - expect.objectContaining({ - type: 'session_complete', - detail: 'Gemini CLI finished responding.', - }), - ), + expect( + terminalNotificationsMocks.buildRunEventNotificationContent, + ).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'session_complete', + detail: 'Gemini CLI finished responding.', + }), ); expect(terminalNotificationsMocks.notifyViaTerminal).toHaveBeenCalled(); - await act(async () => { - unmount?.(); - }); + unmount(); }); it('sends completion notification when focus reporting is unavailable', async () => { @@ -774,34 +727,23 @@ describe('AppContainer State Management', () => { streamingState: currentStreamingState, })); - let unmount: (() => void) | undefined; - let rerender: ((tree: ReactElement) => void) | undefined; - - await act(async () => { - const rendered = renderAppContainer(); - unmount = rendered.unmount; - rerender = rendered.rerender; - }); + const { unmount, rerender } = await act(async () => renderAppContainer()); currentStreamingState = 'idle'; await act(async () => { - rerender?.(getAppContainer()); + rerender(getAppContainer()); }); - await waitFor(() => - expect( - terminalNotificationsMocks.buildRunEventNotificationContent, - ).toHaveBeenCalledWith( - expect.objectContaining({ - type: 'session_complete', - detail: 'Gemini CLI finished responding.', - }), - ), + expect( + terminalNotificationsMocks.buildRunEventNotificationContent, + ).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'session_complete', + detail: 'Gemini CLI finished responding.', + }), ); - await act(async () => { - unmount?.(); - }); + unmount(); }); it('does not send completion notification when another action-required dialog is pending', async () => { @@ -819,27 +761,18 @@ describe('AppContainer State Management', () => { streamingState: currentStreamingState, })); - let unmount: (() => void) | undefined; - let rerender: ((tree: ReactElement) => void) | undefined; - - await act(async () => { - const rendered = renderAppContainer(); - unmount = rendered.unmount; - rerender = rendered.rerender; - }); + const { unmount, rerender } = await act(async () => renderAppContainer()); currentStreamingState = 'idle'; await act(async () => { - rerender?.(getAppContainer()); + rerender(getAppContainer()); }); expect( terminalNotificationsMocks.notifyViaTerminal, ).not.toHaveBeenCalled(); - await act(async () => { - unmount?.(); - }); + unmount(); }); it('can send repeated attention notifications for the same key after pending state clears', async () => { @@ -875,24 +808,15 @@ describe('AppContainer State Management', () => { pendingHistoryItems, })); - let unmount: (() => void) | undefined; - let rerender: ((tree: ReactElement) => void) | undefined; + const { unmount, rerender } = await act(async () => renderAppContainer()); - await act(async () => { - const rendered = renderAppContainer(); - unmount = rendered.unmount; - rerender = rendered.rerender; - }); - - await waitFor(() => - expect( - terminalNotificationsMocks.notifyViaTerminal, - ).toHaveBeenCalledTimes(1), - ); + expect( + terminalNotificationsMocks.notifyViaTerminal, + ).toHaveBeenCalledTimes(1); pendingHistoryItems = []; await act(async () => { - rerender?.(getAppContainer()); + rerender(getAppContainer()); }); pendingHistoryItems = [ @@ -917,18 +841,14 @@ describe('AppContainer State Management', () => { }, ]; await act(async () => { - rerender?.(getAppContainer()); + rerender(getAppContainer()); }); - await waitFor(() => - expect( - terminalNotificationsMocks.notifyViaTerminal, - ).toHaveBeenCalledTimes(2), - ); + expect( + terminalNotificationsMocks.notifyViaTerminal, + ).toHaveBeenCalledTimes(2); - await act(async () => { - unmount?.(); - }); + unmount(); }); it('initializes with theme error from initialization result', async () => { @@ -937,68 +857,53 @@ describe('AppContainer State Management', () => { themeError: 'Failed to load theme', }; - let unmount: () => void; - await act(async () => { - const result = renderAppContainer({ + const { unmount } = await act(async () => + renderAppContainer({ initResult: initResultWithError, - }); - unmount = result.unmount; - }); - await waitFor(() => expect(capturedUIState).toBeTruthy()); - unmount!(); + }), + ); + expect(capturedUIState).toBeTruthy(); + unmount(); }); - it('handles debug mode state', () => { + it('handles debug mode state', async () => { const debugConfig = makeFakeConfig(); vi.spyOn(debugConfig, 'getDebugMode').mockReturnValue(true); - expect(() => { - renderAppContainer({ config: debugConfig }); - }).not.toThrow(); + const { unmount } = await act(async () => + renderAppContainer({ config: debugConfig }), + ); + unmount(); }); }); describe('Context Providers', () => { it('provides AppContext with correct values', async () => { - let unmount: () => void; - await act(async () => { - const result = renderAppContainer({ version: '2.0.0' }); - unmount = result.unmount; - }); - await waitFor(() => expect(capturedUIState).toBeTruthy()); + const { unmount } = await act(async () => + renderAppContainer({ version: '2.0.0' }), + ); + expect(capturedUIState).toBeTruthy(); // Should render and unmount cleanly - expect(() => unmount!()).not.toThrow(); + unmount(); }); it('provides UIStateContext with state management', async () => { - let unmount: () => void; - await act(async () => { - const result = renderAppContainer(); - unmount = result.unmount; - }); - await waitFor(() => expect(capturedUIState).toBeTruthy()); - unmount!(); + const { unmount } = await act(async () => renderAppContainer()); + expect(capturedUIState).toBeTruthy(); + unmount(); }); it('provides UIActionsContext with action handlers', async () => { - let unmount: () => void; - await act(async () => { - const result = renderAppContainer(); - unmount = result.unmount; - }); - await waitFor(() => expect(capturedUIState).toBeTruthy()); - unmount!(); + const { unmount } = await act(async () => renderAppContainer()); + expect(capturedUIState).toBeTruthy(); + unmount(); }); it('provides ConfigContext with config object', async () => { - let unmount: () => void; - await act(async () => { - const result = renderAppContainer(); - unmount = result.unmount; - }); - await waitFor(() => expect(capturedUIState).toBeTruthy()); - unmount!(); + const { unmount } = await act(async () => renderAppContainer()); + expect(capturedUIState).toBeTruthy(); + unmount(); }); }); @@ -1011,13 +916,11 @@ describe('AppContainer State Management', () => { showMemoryUsage: false, }); - let unmount: () => void; - await act(async () => { - const result = renderAppContainer({ settings: settingsAllHidden }); - unmount = result.unmount; - }); - await waitFor(() => expect(capturedUIState).toBeTruthy()); - unmount!(); + const { unmount } = await act(async () => + renderAppContainer({ settings: settingsAllHidden }), + ); + expect(capturedUIState).toBeTruthy(); + unmount(); }); it('handles settings with memory usage enabled', async () => { @@ -1025,13 +928,11 @@ describe('AppContainer State Management', () => { showMemoryUsage: true, }); - let unmount: () => void; - await act(async () => { - const result = renderAppContainer({ settings: settingsWithMemory }); - unmount = result.unmount; - }); - await waitFor(() => expect(capturedUIState).toBeTruthy()); - unmount!(); + const { unmount } = await act(async () => + renderAppContainer({ settings: settingsWithMemory }), + ); + expect(capturedUIState).toBeTruthy(); + unmount(); }); }); @@ -1039,13 +940,11 @@ describe('AppContainer State Management', () => { it.each(['1.0.0', '2.1.3-beta', '3.0.0-nightly'])( 'handles version format: %s', async (version) => { - let unmount: () => void; - await act(async () => { - const result = renderAppContainer({ version }); - unmount = result.unmount; - }); - await waitFor(() => expect(capturedUIState).toBeTruthy()); - unmount!(); + const { unmount } = await act(async () => + renderAppContainer({ version }), + ); + expect(capturedUIState).toBeTruthy(); + unmount(); }, ); }); @@ -1058,30 +957,30 @@ describe('AppContainer State Management', () => { }); // Should still render without crashing - errors should be handled internally - const { unmount } = renderAppContainer({ config: errorConfig }); + const { unmount } = await act(async () => + renderAppContainer({ config: errorConfig }), + ); unmount(); }); it('handles undefined settings gracefully', async () => { const undefinedSettings = createMockSettings(); - let unmount: () => void; - await act(async () => { - const result = renderAppContainer({ settings: undefinedSettings }); - unmount = result.unmount; - }); - await waitFor(() => expect(capturedUIState).toBeTruthy()); - unmount!(); + const { unmount } = await act(async () => + renderAppContainer({ settings: undefinedSettings }), + ); + expect(capturedUIState).toBeTruthy(); + unmount(); }); }); describe('Provider Hierarchy', () => { - it('establishes correct provider nesting order', () => { + it('establishes correct provider nesting order', async () => { // This tests that all the context providers are properly nested // and that the component tree can be built without circular dependencies - const { unmount } = renderAppContainer(); + const { unmount } = await act(async () => renderAppContainer()); - expect(() => unmount()).not.toThrow(); + unmount(); }); }); @@ -1113,40 +1012,32 @@ describe('AppContainer State Management', () => { filePath: '/tmp/test-session.json', }; - let unmount: () => void; - await act(async () => { - const result = renderAppContainer({ + const { unmount } = await act(async () => + renderAppContainer({ config: mockConfig, settings: mockSettings, version: '1.0.0', initResult: mockInitResult, resumedSessionData: mockResumedSessionData, - }); - unmount = result.unmount; - }); - await act(async () => { - unmount(); - }); + }), + ); + unmount(); }); it('renders without resumed session data', async () => { - let unmount: () => void; - await act(async () => { - const result = renderAppContainer({ + const { unmount } = await act(async () => + renderAppContainer({ config: mockConfig, settings: mockSettings, version: '1.0.0', initResult: mockInitResult, resumedSessionData: undefined, - }); - unmount = result.unmount; - }); - await act(async () => { - unmount(); - }); + }), + ); + unmount(); }); - it('initializes chat recording service when config has it', () => { + it('initializes chat recording service when config has it', async () => { const mockChatRecordingService = { initialize: vi.fn(), recordMessage: vi.fn(), @@ -1166,18 +1057,19 @@ describe('AppContainer State Management', () => { mockGeminiClient as unknown as ReturnType, ); - expect(() => { + const { unmount } = await act(async () => renderAppContainer({ config: configWithRecording, settings: mockSettings, version: '1.0.0', initResult: mockInitResult, - }); - }).not.toThrow(); + }), + ); + unmount(); }); }); describe('Session Recording Integration', () => { - it('provides chat recording service configuration', () => { + it('provides chat recording service configuration', async () => { const mockChatRecordingService = { initialize: vi.fn(), recordMessage: vi.fn(), @@ -1203,23 +1095,24 @@ describe('AppContainer State Management', () => { 'test-session-123', ); - expect(() => { + const { unmount } = await act(async () => renderAppContainer({ config: configWithRecording, settings: mockSettings, version: '1.0.0', initResult: mockInitResult, - }); - }).not.toThrow(); + }), + ); // Verify the recording service structure is correct expect(configWithRecording.getGeminiClient).toBeDefined(); expect(mockGeminiClient.getChatRecordingService).toBeDefined(); expect(mockChatRecordingService.initialize).toBeDefined(); expect(mockChatRecordingService.recordMessage).toBeDefined(); + unmount(); }); - it('handles session recording when messages are added', () => { + it('handles session recording when messages are added', async () => { const mockRecordMessage = vi.fn(); const mockRecordMessageTokens = vi.fn(); @@ -1242,22 +1135,25 @@ describe('AppContainer State Management', () => { mockGeminiClient as unknown as ReturnType, ); - renderAppContainer({ - config: configWithRecording, - settings: mockSettings, - version: '1.0.0', - initResult: mockInitResult, - }); + const { unmount } = await act(async () => + renderAppContainer({ + config: configWithRecording, + settings: mockSettings, + version: '1.0.0', + initResult: mockInitResult, + }), + ); // The actual recording happens through the useHistory hook // which would be triggered by user interactions expect(mockChatRecordingService.initialize).toBeDefined(); expect(mockChatRecordingService.recordMessage).toBeDefined(); + unmount(); }); }); describe('Session Resume Flow', () => { - it('accepts resumed session data', () => { + it('accepts resumed session data', async () => { const mockResumeChat = vi.fn(); const mockGeminiClient = { isInitialized: vi.fn(() => true), @@ -1303,22 +1199,23 @@ describe('AppContainer State Management', () => { filePath: '/tmp/resumed-session.json', }; - expect(() => { + const { unmount } = await act(async () => renderAppContainer({ config: configWithClient, settings: mockSettings, version: '1.0.0', initResult: mockInitResult, resumedSessionData: resumedData, - }); - }).not.toThrow(); + }), + ); // Verify the resume functionality structure is in place expect(mockGeminiClient.resumeChat).toBeDefined(); expect(resumedData.conversation.messages).toHaveLength(2); + unmount(); }); - it('does not attempt resume when client is not initialized', () => { + it('does not attempt resume when client is not initialized', async () => { const mockResumeChat = vi.fn(); const mockGeminiClient = { isInitialized: vi.fn(() => false), // Not initialized @@ -1343,21 +1240,24 @@ describe('AppContainer State Management', () => { filePath: '/tmp/session.json', }; - renderAppContainer({ - config: configWithClient, - settings: mockSettings, - version: '1.0.0', - initResult: mockInitResult, - resumedSessionData: resumedData, - }); + const { unmount } = await act(async () => + renderAppContainer({ + config: configWithClient, + settings: mockSettings, + version: '1.0.0', + initResult: mockInitResult, + resumedSessionData: resumedData, + }), + ); // Should not call resumeChat when client is not initialized expect(mockResumeChat).not.toHaveBeenCalled(); + unmount(); }); }); describe('Token Counting from Session Stats', () => { - it('tracks token counts from session messages', () => { + it('tracks token counts from session messages', async () => { // Session stats are provided through the SessionStatsProvider context // in the real app, not through the config directly const mockChatRecordingService = { @@ -1385,33 +1285,30 @@ describe('AppContainer State Management', () => { mockGeminiClient as unknown as ReturnType, ); - renderAppContainer({ - config: configWithRecording, - settings: mockSettings, - version: '1.0.0', - initResult: mockInitResult, - }); + const { unmount } = await act(async () => + renderAppContainer({ + config: configWithRecording, + settings: mockSettings, + version: '1.0.0', + initResult: mockInitResult, + }), + ); // In the actual app, these stats would be displayed in components // and updated as messages are processed through the recording service expect(mockChatRecordingService.recordMessageTokens).toBeDefined(); expect(mockChatRecordingService.getCurrentConversation).toBeDefined(); + unmount(); }); }); describe('Quota and Fallback Integration', () => { it('passes a null proQuotaRequest to UIStateContext by default', async () => { // The default mock from beforeEach already sets proQuotaRequest to null - let unmount: () => void; - await act(async () => { - const result = renderAppContainer(); - unmount = result.unmount; - }); - await waitFor(() => { - // Assert that the context value is as expected - expect(capturedUIState.quota.proQuotaRequest).toBeNull(); - }); - unmount!(); + const { unmount } = await act(async () => renderAppContainer()); + // Assert that the context value is as expected + expect(capturedUIState.quota.proQuotaRequest).toBeNull(); + unmount(); }); it('passes a valid proQuotaRequest to UIStateContext when provided by the hook', async () => { @@ -1427,16 +1324,10 @@ describe('AppContainer State Management', () => { }); // Act: Render the container - let unmount: () => void; - await act(async () => { - const result = renderAppContainer(); - unmount = result.unmount; - }); - await waitFor(() => { - // Assert: The mock request is correctly passed through the context - expect(capturedUIState.quota.proQuotaRequest).toEqual(mockRequest); - }); - unmount!(); + const { unmount } = await act(async () => renderAppContainer()); + // Assert: The mock request is correctly passed through the context + expect(capturedUIState.quota.proQuotaRequest).toEqual(mockRequest); + unmount(); }); it('passes the handleProQuotaChoice function to UIActionsContext', async () => { @@ -1448,22 +1339,16 @@ describe('AppContainer State Management', () => { }); // Act: Render the container - let unmount: () => void; - await act(async () => { - const result = renderAppContainer(); - unmount = result.unmount; - }); - await waitFor(() => { - // Assert: The action in the context is the mock handler we provided - expect(capturedUIActions.handleProQuotaChoice).toBe(mockHandler); - }); + const { unmount } = await act(async () => renderAppContainer()); + // Assert: The action in the context is the mock handler we provided + expect(capturedUIActions.handleProQuotaChoice).toBe(mockHandler); // You can even verify that the plumbed function is callable act(() => { capturedUIActions.handleProQuotaChoice('retry_later'); }); expect(mockHandler).toHaveBeenCalledWith('retry_later'); - unmount!(); + unmount(); }); }); @@ -1479,7 +1364,7 @@ describe('AppContainer State Management', () => { expect(stdout).toBe(mocks.mockStdout); }); - it('should update terminal title with Working… when showStatusInTitle is false', () => { + it('should update terminal title with Working… when showStatusInTitle is false', async () => { // Arrange: Set up mock settings with showStatusInTitle disabled const mockSettingsWithShowStatusFalse = createMockSettings({ ui: { @@ -1496,9 +1381,11 @@ describe('AppContainer State Management', () => { }); // Act: Render the container - const { unmount } = renderAppContainer({ - settings: mockSettingsWithShowStatusFalse, - }); + const { unmount } = await act(async () => + renderAppContainer({ + settings: mockSettingsWithShowStatusFalse, + }), + ); // Assert: Check that title was updated with "Working…" const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) => @@ -1512,7 +1399,7 @@ describe('AppContainer State Management', () => { unmount(); }); - it('should use legacy terminal title when dynamicWindowTitle is false', () => { + it('should use legacy terminal title when dynamicWindowTitle is false', async () => { // Arrange: Set up mock settings with dynamicWindowTitle disabled const mockSettingsWithDynamicTitleFalse = createMockSettings({ ui: { @@ -1529,9 +1416,11 @@ describe('AppContainer State Management', () => { }); // Act: Render the container - const { unmount } = renderAppContainer({ - settings: mockSettingsWithDynamicTitleFalse, - }); + const { unmount } = await act(async () => + renderAppContainer({ + settings: mockSettingsWithDynamicTitleFalse, + }), + ); // Assert: Check that legacy title was used const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) => @@ -1545,7 +1434,7 @@ describe('AppContainer State Management', () => { unmount(); }); - it('should not update terminal title when hideWindowTitle is true', () => { + it('should not update terminal title when hideWindowTitle is true', async () => { // Arrange: Set up mock settings with hideWindowTitle enabled const mockSettingsWithHideTitleTrue = createMockSettings({ ui: { @@ -1555,9 +1444,11 @@ describe('AppContainer State Management', () => { }); // Act: Render the container - const { unmount } = renderAppContainer({ - settings: mockSettingsWithHideTitleTrue, - }); + const { unmount } = await act(async () => + renderAppContainer({ + settings: mockSettingsWithHideTitleTrue, + }), + ); // Assert: Check that no title-related writes occurred const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) => @@ -1568,7 +1459,7 @@ describe('AppContainer State Management', () => { unmount(); }); - it('should update terminal title with thought subject when in active state', () => { + it('should update terminal title with thought subject when in active state', async () => { // Arrange: Set up mock settings with showStatusInTitle enabled const mockSettingsWithTitleEnabled = createMockSettings({ ui: { @@ -1586,9 +1477,11 @@ describe('AppContainer State Management', () => { }); // Act: Render the container - const { unmount } = renderAppContainer({ - settings: mockSettingsWithTitleEnabled, - }); + const { unmount } = await act(async () => + renderAppContainer({ + settings: mockSettingsWithTitleEnabled, + }), + ); // Assert: Check that title was updated with thought subject and suffix const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) => @@ -1602,7 +1495,7 @@ describe('AppContainer State Management', () => { unmount(); }); - it('should update terminal title with default text when in Idle state and no thought subject', () => { + it('should update terminal title with default text when in Idle state and no thought subject', async () => { // Arrange: Set up mock settings with showStatusInTitle enabled const mockSettingsWithTitleEnabled = createMockSettings({ ui: { @@ -1615,9 +1508,11 @@ describe('AppContainer State Management', () => { mockedUseGeminiStream.mockReturnValue(DEFAULT_GEMINI_STREAM_MOCK); // Act: Render the container - const { unmount } = renderAppContainer({ - settings: mockSettingsWithTitleEnabled, - }); + const { unmount } = await act(async () => + renderAppContainer({ + settings: mockSettingsWithTitleEnabled, + }), + ); // Assert: Check that title was updated with default Idle text const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) => @@ -1649,13 +1544,11 @@ describe('AppContainer State Management', () => { }); // Act: Render the container - let unmount: () => void; - await act(async () => { - const result = renderAppContainer({ + const { unmount } = await act(async () => + renderAppContainer({ settings: mockSettingsWithTitleEnabled, - }); - unmount = result.unmount; - }); + }), + ); // Assert: Check that title was updated with confirmation text const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) => @@ -1666,7 +1559,7 @@ describe('AppContainer State Management', () => { expect(titleWrites[0][0]).toBe( `\x1b]0;${'✋ Action Required (workspace)'.padEnd(80, ' ')}\x07`, ); - unmount!(); + unmount(); }); describe('Shell Focus Action Required', () => { @@ -1712,9 +1605,11 @@ describe('AppContainer State Management', () => { vi.spyOn(mockConfig, 'isInteractiveShellEnabled').mockReturnValue(true); // Act: Render the container (embeddedShellFocused is false by default in state) - const { unmount } = renderAppContainer({ - settings: mockSettingsWithTitleEnabled, - }); + const { unmount } = await act(async () => + renderAppContainer({ + settings: mockSettingsWithTitleEnabled, + }), + ); // Initially it should show the working status const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) => @@ -1773,9 +1668,11 @@ describe('AppContainer State Management', () => { vi.spyOn(mockConfig, 'isInteractive').mockReturnValue(true); vi.spyOn(mockConfig, 'isInteractiveShellEnabled').mockReturnValue(true); - const { unmount } = renderAppContainer({ - settings: mockSettingsWithTitleEnabled, - }); + const { unmount } = await act(async () => + renderAppContainer({ + settings: mockSettingsWithTitleEnabled, + }), + ); // Fast-forward time by 65 seconds - should still NOT be Action Required await act(async () => { @@ -1830,9 +1727,11 @@ describe('AppContainer State Management', () => { vi.spyOn(mockConfig, 'isInteractive').mockReturnValue(true); vi.spyOn(mockConfig, 'isInteractiveShellEnabled').mockReturnValue(true); - const { unmount } = renderAppContainer({ - settings: mockSettingsWithTitleEnabled, - }); + const { unmount } = await act(async () => + renderAppContainer({ + settings: mockSettingsWithTitleEnabled, + }), + ); // Fast-forward time by 65 seconds await act(async () => { @@ -1875,9 +1774,11 @@ describe('AppContainer State Management', () => { vi.spyOn(mockConfig, 'isInteractiveShellEnabled').mockReturnValue(true); // Act: Render the container - const { unmount, rerender } = renderAppContainer({ - settings: mockSettingsWithTitleEnabled, - }); + const { unmount, rerender } = await act(async () => + renderAppContainer({ + settings: mockSettingsWithTitleEnabled, + }), + ); // Fast-forward time by 20 seconds await act(async () => { @@ -1931,7 +1832,7 @@ describe('AppContainer State Management', () => { }); }); - it('should pad title to exactly 80 characters', () => { + it('should pad title to exactly 80 characters', async () => { // Arrange: Set up mock settings with showStatusInTitle enabled const mockSettingsWithTitleEnabled = createMockSettings({ ui: { @@ -1949,9 +1850,11 @@ describe('AppContainer State Management', () => { }); // Act: Render the container - const { unmount } = renderAppContainer({ - settings: mockSettingsWithTitleEnabled, - }); + const { unmount } = await act(async () => + renderAppContainer({ + settings: mockSettingsWithTitleEnabled, + }), + ); // Assert: Check that title is padded to exactly 80 characters const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) => @@ -1966,7 +1869,7 @@ describe('AppContainer State Management', () => { unmount(); }); - it('should use correct ANSI escape code format', () => { + it('should use correct ANSI escape code format', async () => { // Arrange: Set up mock settings with showStatusInTitle enabled const mockSettingsWithTitleEnabled = createMockSettings({ ui: { @@ -1984,9 +1887,11 @@ describe('AppContainer State Management', () => { }); // Act: Render the container - const { unmount } = renderAppContainer({ - settings: mockSettingsWithTitleEnabled, - }); + const { unmount } = await act(async () => + renderAppContainer({ + settings: mockSettingsWithTitleEnabled, + }), + ); // Assert: Check that the correct ANSI escape sequence is used const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) => @@ -1999,7 +1904,7 @@ describe('AppContainer State Management', () => { unmount(); }); - it('should use CLI_TITLE environment variable when set', () => { + it('should use CLI_TITLE environment variable when set', async () => { // Arrange: Set up mock settings with showStatusInTitle disabled (so it shows suffix) const mockSettingsWithTitleDisabled = createMockSettings({ ui: { @@ -2018,9 +1923,11 @@ describe('AppContainer State Management', () => { }); // Act: Render the container - const { unmount } = renderAppContainer({ - settings: mockSettingsWithTitleDisabled, - }); + const { unmount } = await act(async () => + renderAppContainer({ + settings: mockSettingsWithTitleDisabled, + }), + ); // Assert: Check that title was updated with CLI_TITLE value const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) => @@ -2046,7 +1953,7 @@ describe('AppContainer State Management', () => { }); it('should set and clear the queue error message after a timeout', async () => { - const { rerender, unmount } = renderAppContainer(); + const { rerender, unmount } = await act(async () => renderAppContainer()); await act(async () => { vi.advanceTimersByTime(0); }); @@ -2068,7 +1975,7 @@ describe('AppContainer State Management', () => { }); it('should reset the timer if a new error message is set', async () => { - const { rerender, unmount } = renderAppContainer(); + const { rerender, unmount } = await act(async () => renderAppContainer()); await act(async () => { vi.advanceTimersByTime(0); }); @@ -2110,11 +2017,11 @@ describe('AppContainer State Management', () => { let mockCancelOngoingRequest: Mock; let rerender: () => void; let unmount: () => void; - let stdin: ReturnType['stdin']; + let stdin: Awaited>['stdin']; // Helper function to reduce boilerplate in tests const setupKeypressTest = async () => { - const renderResult = renderAppContainer(); + const renderResult = await act(async () => renderAppContainer()); stdin = renderResult.stdin; await act(async () => { vi.advanceTimersByTime(0); @@ -2328,7 +2235,7 @@ describe('AppContainer State Management', () => { activePtyId: 1, }); - const renderResult = render(getAppContainer()); + const renderResult = await act(async () => render(getAppContainer())); await act(async () => { vi.advanceTimersByTime(0); }); @@ -2446,7 +2353,7 @@ describe('AppContainer State Management', () => { let unmount: () => void; const setupShortcutsVisibilityTest = async () => { - const renderResult = renderAppContainer(); + const renderResult = await act(async () => renderAppContainer()); await act(async () => { vi.advanceTimersByTime(0); }); @@ -2522,9 +2429,7 @@ describe('AppContainer State Management', () => { await act(async () => { rerender(); }); - await waitFor(() => { - expect(capturedUIState.shortcutsHelpVisible).toBe(false); - }); + expect(capturedUIState.shortcutsHelpVisible).toBe(false); unmount(); }); @@ -2553,9 +2458,7 @@ describe('AppContainer State Management', () => { await act(async () => { rerender(); }); - await waitFor(() => { - expect(capturedUIState.shortcutsHelpVisible).toBe(false); - }); + expect(capturedUIState.shortcutsHelpVisible).toBe(false); unmount(); }); @@ -2564,7 +2467,7 @@ describe('AppContainer State Management', () => { describe('Copy Mode (CTRL+S)', () => { let rerender: () => void; let unmount: () => void; - let stdin: ReturnType['stdin']; + let stdin: Awaited>['stdin']; const setupCopyModeTest = async ( isAlternateMode = false, @@ -2602,7 +2505,7 @@ describe('AppContainer State Management', () => { ); - const renderResult = render(getTree(testSettings)); + const renderResult = await act(async () => render(getTree(testSettings))); stdin = renderResult.stdin; await act(async () => { vi.advanceTimersByTime(0); @@ -2792,15 +2695,10 @@ describe('AppContainer State Management', () => { closeModelDialog: vi.fn(), }); - let unmount: () => void; - await act(async () => { - const result = renderAppContainer(); - unmount = result.unmount; - }); - await waitFor(() => expect(capturedUIState).toBeTruthy()); + const { unmount } = await act(async () => renderAppContainer()); expect(capturedUIState.isModelDialogOpen).toBe(true); - unmount!(); + unmount(); }); it('should provide model dialog actions in the UIActionsContext', async () => { @@ -2812,45 +2710,29 @@ describe('AppContainer State Management', () => { closeModelDialog: mockCloseModelDialog, }); - let unmount: () => void; - await act(async () => { - const result = renderAppContainer(); - unmount = result.unmount; - }); - await waitFor(() => expect(capturedUIState).toBeTruthy()); + const { unmount } = await act(async () => renderAppContainer()); // Verify that the actions are correctly passed through context act(() => { capturedUIActions.closeModelDialog(); }); expect(mockCloseModelDialog).toHaveBeenCalled(); - unmount!(); + unmount(); }); }); describe('Agent Configuration Dialog Integration', () => { it('should initialize with dialog closed and no agent selected', async () => { - let unmount: () => void; - await act(async () => { - const result = renderAppContainer(); - unmount = result.unmount; - }); - await waitFor(() => expect(capturedUIState).toBeTruthy()); - + const { unmount } = await act(async () => renderAppContainer()); expect(capturedUIState.isAgentConfigDialogOpen).toBe(false); expect(capturedUIState.selectedAgentName).toBeUndefined(); expect(capturedUIState.selectedAgentDisplayName).toBeUndefined(); expect(capturedUIState.selectedAgentDefinition).toBeUndefined(); - unmount!(); + unmount(); }); it('should update state when openAgentConfigDialog is called', async () => { - let unmount: () => void; - await act(async () => { - const result = renderAppContainer(); - unmount = result.unmount; - }); - await waitFor(() => expect(capturedUIState).toBeTruthy()); + const { unmount } = await act(async () => renderAppContainer()); const agentDefinition = { name: 'test-agent' }; act(() => { @@ -2865,16 +2747,11 @@ describe('AppContainer State Management', () => { expect(capturedUIState.selectedAgentName).toBe('test-agent'); expect(capturedUIState.selectedAgentDisplayName).toBe('Test Agent'); expect(capturedUIState.selectedAgentDefinition).toEqual(agentDefinition); - unmount!(); + unmount(); }); it('should clear state when closeAgentConfigDialog is called', async () => { - let unmount: () => void; - await act(async () => { - const result = renderAppContainer(); - unmount = result.unmount; - }); - await waitFor(() => expect(capturedUIState).toBeTruthy()); + const { unmount } = await act(async () => renderAppContainer()); const agentDefinition = { name: 'test-agent' }; act(() => { @@ -2895,31 +2772,26 @@ describe('AppContainer State Management', () => { expect(capturedUIState.selectedAgentName).toBeUndefined(); expect(capturedUIState.selectedAgentDisplayName).toBeUndefined(); expect(capturedUIState.selectedAgentDefinition).toBeUndefined(); - unmount!(); + unmount(); }); }); describe('CoreEvents Integration', () => { it('subscribes to UserFeedback and drains backlog on mount', async () => { - let unmount: () => void; - await act(async () => { - const result = renderAppContainer(); - unmount = result.unmount; - }); - await waitFor(() => expect(capturedUIState).toBeTruthy()); + const { unmount } = await act(async () => renderAppContainer()); expect(mockCoreEvents.on).toHaveBeenCalledWith( CoreEvent.UserFeedback, expect.any(Function), ); expect(mockCoreEvents.drainBacklogs).toHaveBeenCalledTimes(1); - unmount!(); + unmount(); }); it('unsubscribes from UserFeedback on unmount', async () => { let unmount: () => void; await act(async () => { - const result = renderAppContainer(); + const result = await renderAppContainer(); unmount = result.unmount; }); await waitFor(() => expect(capturedUIState).toBeTruthy()); @@ -2935,7 +2807,7 @@ describe('AppContainer State Management', () => { it('adds history item when UserFeedback event is received', async () => { let unmount: () => void; await act(async () => { - const result = renderAppContainer(); + const result = await renderAppContainer(); unmount = result.unmount; }); await waitFor(() => expect(capturedUIState).toBeTruthy()); @@ -2971,7 +2843,7 @@ describe('AppContainer State Management', () => { let unmount: () => void; await act(async () => { - const result = renderAppContainer(); + const result = await renderAppContainer(); unmount = result.unmount; }); await waitFor(() => { @@ -3004,7 +2876,7 @@ describe('AppContainer State Management', () => { let unmount: () => void; await act(async () => { - const result = renderAppContainer(); + const result = await renderAppContainer(); unmount = result.unmount; }); await waitFor(() => expect(capturedUIState).toBeTruthy()); @@ -3016,7 +2888,7 @@ describe('AppContainer State Management', () => { it('handles consent request events', async () => { let unmount: () => void; await act(async () => { - const result = renderAppContainer(); + const result = await renderAppContainer(); unmount = result.unmount; }); await waitFor(() => expect(capturedUIState).toBeTruthy()); @@ -3053,7 +2925,7 @@ describe('AppContainer State Management', () => { it('unsubscribes from ConsentRequest on unmount', async () => { let unmount: () => void; await act(async () => { - const result = renderAppContainer(); + const result = await renderAppContainer(); unmount = result.unmount; }); await waitFor(() => expect(capturedUIState).toBeTruthy()); @@ -3076,7 +2948,7 @@ describe('AppContainer State Management', () => { }); let unmount: () => void; await act(async () => { - const result = renderAppContainer(); + const result = await renderAppContainer(); unmount = result.unmount; }); await waitFor(() => { @@ -3104,12 +2976,7 @@ describe('AppContainer State Management', () => { }); it('preserves buffer when cancelling, even if empty (user is in control)', async () => { - let unmount: () => void; - await act(async () => { - const result = renderAppContainer(); - unmount = result.unmount; - }); - await waitFor(() => expect(capturedUIState).toBeTruthy()); + const { unmount } = await act(async () => renderAppContainer()); const { onCancelSubmit } = extractUseGeminiStreamArgs( mockedUseGeminiStream.mock.lastCall!, @@ -3122,7 +2989,7 @@ describe('AppContainer State Management', () => { // Should NOT modify buffer when cancelling - user is in control expect(mockSetText).not.toHaveBeenCalled(); - unmount!(); + unmount(); }); it('preserves prompt text when cancelling streaming, even if same as last message (regression test for issue #13387)', async () => { @@ -3140,12 +3007,7 @@ describe('AppContainer State Management', () => { initializeFromLogger: vi.fn(), }); - let unmount: () => void; - await act(async () => { - const result = renderAppContainer(); - unmount = result.unmount; - }); - await waitFor(() => expect(capturedUIState).toBeTruthy()); + const { unmount } = await act(async () => renderAppContainer()); const { onCancelSubmit } = extractUseGeminiStreamArgs( mockedUseGeminiStream.mock.lastCall!, @@ -3159,7 +3021,7 @@ describe('AppContainer State Management', () => { // Should NOT call setText - prompt should be preserved regardless of content expect(mockSetText).not.toHaveBeenCalled(); - unmount!(); + unmount(); }); it('restores the prompt when onCancelSubmit is called with shouldRestorePrompt=true (or undefined)', async () => { @@ -3170,14 +3032,8 @@ describe('AppContainer State Management', () => { initializeFromLogger: vi.fn(), }); - let unmount: () => void; - await act(async () => { - const result = renderAppContainer(); - unmount = result.unmount; - }); - await waitFor(() => - expect(capturedUIState.userMessages).toContain('previous message'), - ); + const { unmount } = await act(async () => renderAppContainer()); + expect(capturedUIState.userMessages).toContain('previous message'); const { onCancelSubmit } = extractUseGeminiStreamArgs( mockedUseGeminiStream.mock.lastCall!, @@ -3187,11 +3043,9 @@ describe('AppContainer State Management', () => { onCancelSubmit(true); }); - await waitFor(() => { - expect(mockSetText).toHaveBeenCalledWith('previous message'); - }); + expect(mockSetText).toHaveBeenCalledWith('previous message'); - unmount!(); + unmount(); }); it('input history is independent from conversation history (survives /clear)', async () => { @@ -3204,18 +3058,10 @@ describe('AppContainer State Management', () => { initializeFromLogger: vi.fn(), }); - let rerender: (tree: ReactElement) => void; - let unmount; - await act(async () => { - const result = renderAppContainer(); - rerender = result.rerender; - unmount = result.unmount; - }); + const { rerender, unmount } = await act(async () => renderAppContainer()); // Verify userMessages is populated from inputHistory - await waitFor(() => - expect(capturedUIState.userMessages).toContain('first prompt'), - ); + expect(capturedUIState.userMessages).toContain('first prompt'); expect(capturedUIState.userMessages).toContain('second prompt'); // Clear the conversation history (simulating /clear command) @@ -3238,7 +3084,7 @@ describe('AppContainer State Management', () => { expect(capturedUIState.userMessages).toContain('first prompt'); expect(capturedUIState.userMessages).toContain('second prompt'); - unmount!(); + unmount(); }); }); @@ -3253,14 +3099,10 @@ describe('AppContainer State Management', () => { // Clear previous calls mocks.mockStdout.write.mockClear(); - let compUnmount: () => void = () => {}; - await act(async () => { - const { unmount } = renderAppContainer(); - compUnmount = unmount; - }); + const { unmount } = await act(async () => renderAppContainer()); // Allow async effects to run - await waitFor(() => expect(capturedUIState).toBeTruthy()); + expect(capturedUIState).toBeTruthy(); // Wait for fetchBannerTexts to complete await act(async () => { @@ -3273,7 +3115,7 @@ describe('AppContainer State Management', () => { ); expect(clearTerminalCalls).toHaveLength(0); - compUnmount(); + unmount(); }); }); @@ -3284,14 +3126,13 @@ describe('AppContainer State Management', () => { ); vi.mocked(checkPermissions).mockResolvedValue([]); - let unmount: () => void; - await act(async () => { - unmount = renderAppContainer({ + const { unmount } = await act(async () => + renderAppContainer({ settings: createMockSettings({ ui: { useAlternateBuffer: false } }), - }).unmount; - }); + }), + ); - await waitFor(() => expect(capturedUIActions).toBeTruthy()); + expect(capturedUIActions).toBeTruthy(); // Expand first act(() => capturedUIActions.setConstrainHeight(false)); @@ -3309,7 +3150,7 @@ describe('AppContainer State Management', () => { expect(mocks.mockStdout.write).toHaveBeenCalledWith( ansiEscapes.clearTerminal, ); - unmount!(); + unmount(); }); it('resets expansion state on submission when in alternate buffer without clearing terminal', async () => { @@ -3320,14 +3161,13 @@ describe('AppContainer State Management', () => { vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(true); - let unmount: () => void; - await act(async () => { - unmount = renderAppContainer({ + const { unmount } = await act(async () => + renderAppContainer({ settings: createMockSettings({ ui: { useAlternateBuffer: true } }), - }).unmount; - }); + }), + ); - await waitFor(() => expect(capturedUIActions).toBeTruthy()); + expect(capturedUIActions).toBeTruthy(); // Expand first act(() => capturedUIActions.setConstrainHeight(false)); @@ -3345,7 +3185,7 @@ describe('AppContainer State Management', () => { expect(mocks.mockStdout.write).not.toHaveBeenCalledWith( ansiEscapes.clearTerminal, ); - unmount!(); + unmount(); }); }); @@ -3358,13 +3198,9 @@ describe('AppContainer State Management', () => { vi.useRealTimers(); }); - it('sets showIsExpandableHint when overflow occurs in Standard Mode and hides after 10s', async () => { - let unmount: () => void; - await act(async () => { - const result = renderAppContainer(); - unmount = result.unmount; - }); - await waitFor(() => expect(capturedUIState).toBeTruthy()); + it('should set showIsExpandableHint when overflow occurs in Standard Mode and hides after 10s', async () => { + const { unmount } = await act(async () => renderAppContainer()); + await waitFor(() => expect(capturedOverflowActions).toBeTruthy()); // Trigger overflow act(() => { @@ -3390,16 +3226,12 @@ describe('AppContainer State Management', () => { expect(capturedUIState.showIsExpandableHint).toBe(false); }); - unmount!(); + unmount(); }); it('resets the hint timer when a new component overflows (overflowingIdsSize increases)', async () => { - let unmount: () => void; - await act(async () => { - const result = renderAppContainer(); - unmount = result.unmount; - }); - await waitFor(() => expect(capturedUIState).toBeTruthy()); + const { unmount } = await act(async () => renderAppContainer()); + await waitFor(() => expect(capturedOverflowActions).toBeTruthy()); // 1. Trigger first overflow act(() => { @@ -3447,18 +3279,12 @@ describe('AppContainer State Management', () => { expect(capturedUIState.showIsExpandableHint).toBe(false); }); - unmount!(); + unmount(); }); it('toggles expansion state and resets the hint timer when Ctrl+O is pressed in Standard Mode', async () => { - let unmount: () => void; - let stdin: ReturnType['stdin']; - await act(async () => { - const result = renderAppContainer(); - unmount = result.unmount; - stdin = result.stdin; - }); - await waitFor(() => expect(capturedUIState).toBeTruthy()); + const { stdin, unmount } = await act(async () => renderAppContainer()); + await waitFor(() => expect(capturedOverflowActions).toBeTruthy()); // Initial state is constrainHeight = true expect(capturedUIState.constrainHeight).toBe(true); @@ -3483,10 +3309,8 @@ describe('AppContainer State Management', () => { stdin.write('\x0f'); // \x0f is Ctrl+O }); - await waitFor(() => { - // constrainHeight should toggle - expect(capturedUIState.constrainHeight).toBe(false); - }); + // constrainHeight should toggle + expect(capturedUIState.constrainHeight).toBe(false); // Advance enough that the original timer would have expired if it hadn't reset act(() => { @@ -3505,18 +3329,12 @@ describe('AppContainer State Management', () => { expect(capturedUIState.showIsExpandableHint).toBe(false); }); - unmount!(); + unmount(); }); it('toggles Ctrl+O multiple times and verifies the hint disappears exactly after the last toggle', async () => { - let unmount: () => void; - let stdin: ReturnType['stdin']; - await act(async () => { - const result = renderAppContainer(); - unmount = result.unmount; - stdin = result.stdin; - }); - await waitFor(() => expect(capturedUIState).toBeTruthy()); + const { stdin, unmount } = await act(async () => renderAppContainer()); + await waitFor(() => expect(capturedOverflowActions).toBeTruthy()); // Initial state is constrainHeight = true expect(capturedUIState.constrainHeight).toBe(true); @@ -3540,9 +3358,7 @@ describe('AppContainer State Management', () => { act(() => { stdin.write('\x0f'); // Ctrl+O }); - await waitFor(() => { - expect(capturedUIState.constrainHeight).toBe(false); - }); + expect(capturedUIState.constrainHeight).toBe(false); // Wait 1 second act(() => { @@ -3554,9 +3370,7 @@ describe('AppContainer State Management', () => { act(() => { stdin.write('\x0f'); // Ctrl+O }); - await waitFor(() => { - expect(capturedUIState.constrainHeight).toBe(true); - }); + expect(capturedUIState.constrainHeight).toBe(true); // Wait 1 second act(() => { @@ -3568,9 +3382,7 @@ describe('AppContainer State Management', () => { act(() => { stdin.write('\x0f'); // Ctrl+O }); - await waitFor(() => { - expect(capturedUIState.constrainHeight).toBe(false); - }); + expect(capturedUIState.constrainHeight).toBe(false); // Now we wait just before the timeout from the LAST toggle. // It should still be true. @@ -3588,7 +3400,7 @@ describe('AppContainer State Management', () => { expect(capturedUIState.showIsExpandableHint).toBe(false); }); - unmount!(); + unmount(); }); it('DOES set showIsExpandableHint when overflow occurs in Alternate Buffer Mode', async () => { @@ -3598,14 +3410,12 @@ describe('AppContainer State Management', () => { vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(true); - let unmount: () => void; - await act(async () => { - const result = renderAppContainer({ + const { unmount } = await act(async () => + renderAppContainer({ settings: settingsWithAlternateBuffer, - }); - unmount = result.unmount; - }); - await waitFor(() => expect(capturedUIState).toBeTruthy()); + }), + ); + await waitFor(() => expect(capturedOverflowActions).toBeTruthy()); // Trigger overflow act(() => { @@ -3617,7 +3427,7 @@ describe('AppContainer State Management', () => { expect(capturedUIState.showIsExpandableHint).toBe(true); }); - unmount!(); + unmount(); }); }); @@ -3628,10 +3438,9 @@ describe('AppContainer State Management', () => { ); vi.mocked(checkPermissions).mockResolvedValue(['/test/file.txt']); - let unmount: () => void; - await act(async () => (unmount = renderAppContainer().unmount)); + const { unmount } = await act(async () => renderAppContainer()); - await waitFor(() => expect(capturedUIActions).toBeTruthy()); + expect(capturedUIActions).toBeTruthy(); await act(async () => capturedUIActions.handleFinalSubmit('read @file.txt'), @@ -3641,7 +3450,7 @@ describe('AppContainer State Management', () => { expect(capturedUIState.permissionConfirmationRequest?.files).toEqual([ '/test/file.txt', ]); - await act(async () => unmount!()); + unmount(); }); it.each([true, false])( @@ -3657,10 +3466,9 @@ describe('AppContainer State Management', () => { ); const { submitQuery } = mockedUseGeminiStream(); - let unmount: () => void; - await act(async () => (unmount = renderAppContainer().unmount)); + const { unmount } = await act(async () => renderAppContainer()); - await waitFor(() => expect(capturedUIActions).toBeTruthy()); + expect(capturedUIActions).toBeTruthy(); await act(async () => capturedUIActions.handleFinalSubmit('read @file.txt'), @@ -3679,7 +3487,7 @@ describe('AppContainer State Management', () => { } expect(submitQuery).toHaveBeenCalledWith('read @file.txt'); expect(capturedUIState.permissionConfirmationRequest).toBeNull(); - await act(async () => unmount!()); + unmount(); }, ); }); @@ -3692,17 +3500,11 @@ describe('AppContainer State Management', () => { pendingHistoryItems: [], }); - let unmount: () => void; - await act(async () => { - const result = renderAppContainer(); - unmount = result.unmount; - }); + const { unmount } = await act(async () => renderAppContainer()); - await waitFor(() => { - expect(capturedUIState).toBeTruthy(); - expect(capturedUIState.allowPlanMode).toBe(true); - }); - unmount!(); + expect(capturedUIState).toBeTruthy(); + expect(capturedUIState.allowPlanMode).toBe(true); + unmount(); }); it('should NOT allow plan mode when disabled in config', async () => { @@ -3712,17 +3514,11 @@ describe('AppContainer State Management', () => { pendingHistoryItems: [], }); - let unmount: () => void; - await act(async () => { - const result = renderAppContainer(); - unmount = result.unmount; - }); + const { unmount } = await act(async () => renderAppContainer()); - await waitFor(() => { - expect(capturedUIState).toBeTruthy(); - expect(capturedUIState.allowPlanMode).toBe(false); - }); - unmount!(); + expect(capturedUIState).toBeTruthy(); + expect(capturedUIState.allowPlanMode).toBe(false); + unmount(); }); it('should NOT allow plan mode when streaming', async () => { @@ -3733,17 +3529,11 @@ describe('AppContainer State Management', () => { pendingHistoryItems: [], }); - let unmount: () => void; - await act(async () => { - const result = renderAppContainer(); - unmount = result.unmount; - }); + const { unmount } = await act(async () => renderAppContainer()); - await waitFor(() => { - expect(capturedUIState).toBeTruthy(); - expect(capturedUIState.allowPlanMode).toBe(false); - }); - unmount!(); + expect(capturedUIState).toBeTruthy(); + expect(capturedUIState.allowPlanMode).toBe(false); + unmount(); }); it('should NOT allow plan mode when a tool is awaiting confirmation', async () => { @@ -3764,17 +3554,11 @@ describe('AppContainer State Management', () => { ], }); - let unmount: () => void; - await act(async () => { - const result = renderAppContainer(); - unmount = result.unmount; - }); + const { unmount } = await act(async () => renderAppContainer()); - await waitFor(() => { - expect(capturedUIState).toBeTruthy(); - expect(capturedUIState.allowPlanMode).toBe(false); - }); - unmount!(); + expect(capturedUIState).toBeTruthy(); + expect(capturedUIState.allowPlanMode).toBe(false); + unmount(); }); }); }); diff --git a/packages/cli/src/ui/IdeIntegrationNudge.test.tsx b/packages/cli/src/ui/IdeIntegrationNudge.test.tsx index 5df3534f12..eb3e6a3e4c 100644 --- a/packages/cli/src/ui/IdeIntegrationNudge.test.tsx +++ b/packages/cli/src/ui/IdeIntegrationNudge.test.tsx @@ -53,10 +53,9 @@ describe('IdeIntegrationNudge', () => { }); it('renders correctly with default options', async () => { - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); const frame = lastFrame(); expect(frame).toContain('Do you want to connect VS Code to Gemini CLI?'); @@ -72,8 +71,6 @@ describe('IdeIntegrationNudge', () => { , ); - await waitUntilReady(); - // "Yes" is the first option and selected by default usually. await act(async () => { stdin.write('\r'); @@ -93,8 +90,6 @@ describe('IdeIntegrationNudge', () => { , ); - await waitUntilReady(); - // Navigate down to "No (esc)" await act(async () => { stdin.write('\u001B[B'); // Down arrow @@ -119,8 +114,6 @@ describe('IdeIntegrationNudge', () => { , ); - await waitUntilReady(); - // Navigate down to "No, don't ask again" await act(async () => { stdin.write('\u001B[B'); // Down arrow @@ -150,8 +143,6 @@ describe('IdeIntegrationNudge', () => { , ); - await waitUntilReady(); - // Press Escape await act(async () => { stdin.write('\u001B'); @@ -178,8 +169,6 @@ describe('IdeIntegrationNudge', () => { , ); - await waitUntilReady(); - const frame = lastFrame(); expect(frame).toContain( diff --git a/packages/cli/src/ui/auth/ApiAuthDialog.test.tsx b/packages/cli/src/ui/auth/ApiAuthDialog.test.tsx index b8de6adb0b..d46e0295a1 100644 --- a/packages/cli/src/ui/auth/ApiAuthDialog.test.tsx +++ b/packages/cli/src/ui/auth/ApiAuthDialog.test.tsx @@ -73,23 +73,21 @@ describe('ApiAuthDialog', () => { }); it('renders correctly', async () => { - const { lastFrame, waitUntilReady, unmount } = render( + const { lastFrame, unmount } = await render( , ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); unmount(); }); it('renders with a defaultValue', async () => { - const { waitUntilReady, unmount } = render( + const { unmount } = await render( , ); - await waitUntilReady(); expect(mockedUseTextBuffer).toHaveBeenCalledWith( expect.objectContaining({ initialText: 'test-key', @@ -113,10 +111,9 @@ describe('ApiAuthDialog', () => { 'calls $expectedCall.name when $keyName is pressed', async ({ keyName, sequence, expectedCall, args }) => { mockBuffer.text = 'submitted-key'; // Set for the onSubmit case - const { waitUntilReady, unmount } = render( + const { unmount } = await render( , ); - await waitUntilReady(); // calls[0] is the ApiAuthDialog's useKeypress (Ctrl+C handler) // calls[1] is the TextInput's useKeypress (typing handler) const keypressHandler = mockedUseKeypress.mock.calls[1][0]; @@ -136,24 +133,22 @@ describe('ApiAuthDialog', () => { ); it('displays an error message', async () => { - const { lastFrame, waitUntilReady, unmount } = render( + const { lastFrame, unmount } = await render( , ); - await waitUntilReady(); expect(lastFrame()).toContain('Invalid API Key'); unmount(); }); it('calls clearApiKey and clears buffer when Ctrl+C is pressed', async () => { - const { waitUntilReady, unmount } = render( + const { unmount } = await render( , ); - await waitUntilReady(); // Call 0 is ApiAuthDialog (isActive: true) // Call 1 is TextInput (isActive: true, priority: true) const keypressHandler = mockedUseKeypress.mock.calls[0][0]; diff --git a/packages/cli/src/ui/auth/AuthDialog.test.tsx b/packages/cli/src/ui/auth/AuthDialog.test.tsx index 878b2a8ee0..4837a71490 100644 --- a/packages/cli/src/ui/auth/AuthDialog.test.tsx +++ b/packages/cli/src/ui/auth/AuthDialog.test.tsx @@ -143,10 +143,9 @@ describe('AuthDialog', () => { for (const [key, value] of Object.entries(env)) { vi.stubEnv(key, value as string); } - const { waitUntilReady, unmount } = await renderWithProviders( + const { unmount } = await renderWithProviders( , ); - await waitUntilReady(); const items = mockedRadioButtonSelect.mock.calls[0][0].items; for (const item of shouldContain) { expect(items).toContainEqual(item); @@ -161,10 +160,7 @@ describe('AuthDialog', () => { it('filters auth types when enforcedType is set', async () => { props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI; - const { waitUntilReady, unmount } = await renderWithProviders( - , - ); - await waitUntilReady(); + const { unmount } = await renderWithProviders(); const items = mockedRadioButtonSelect.mock.calls[0][0].items; expect(items).toHaveLength(1); expect(items[0].value).toBe(AuthType.USE_GEMINI); @@ -173,10 +169,7 @@ describe('AuthDialog', () => { it('sets initial index to 0 when enforcedType is set', async () => { props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI; - const { waitUntilReady, unmount } = await renderWithProviders( - , - ); - await waitUntilReady(); + const { unmount } = await renderWithProviders(); const { initialIndex } = mockedRadioButtonSelect.mock.calls[0][0]; expect(initialIndex).toBe(0); unmount(); @@ -213,10 +206,7 @@ describe('AuthDialog', () => { }, ])('selects initial auth type $desc', async ({ setup, expected }) => { setup(); - const { waitUntilReady, unmount } = await renderWithProviders( - , - ); - await waitUntilReady(); + const { unmount } = await renderWithProviders(); const { items, initialIndex } = mockedRadioButtonSelect.mock.calls[0][0]; expect(items[initialIndex].value).toBe(expected); unmount(); @@ -226,10 +216,7 @@ describe('AuthDialog', () => { describe('handleAuthSelect', () => { it('calls onAuthError if validation fails', async () => { mockedValidateAuthMethod.mockReturnValue('Invalid method'); - const { waitUntilReady, unmount } = await renderWithProviders( - , - ); - await waitUntilReady(); + const { unmount } = await renderWithProviders(); const { onSelect: handleAuthSelect } = mockedRadioButtonSelect.mock.calls[0][0]; handleAuthSelect(AuthType.USE_GEMINI); @@ -245,10 +232,7 @@ describe('AuthDialog', () => { it('sets auth context with requiresRestart: true for LOGIN_WITH_GOOGLE', async () => { mockedValidateAuthMethod.mockReturnValue(null); - const { waitUntilReady, unmount } = await renderWithProviders( - , - ); - await waitUntilReady(); + const { unmount } = await renderWithProviders(); const { onSelect: handleAuthSelect } = mockedRadioButtonSelect.mock.calls[0][0]; await handleAuthSelect(AuthType.LOGIN_WITH_GOOGLE); @@ -261,10 +245,7 @@ describe('AuthDialog', () => { it('sets auth context with empty object for other auth types', async () => { mockedValidateAuthMethod.mockReturnValue(null); - const { waitUntilReady, unmount } = await renderWithProviders( - , - ); - await waitUntilReady(); + const { unmount } = await renderWithProviders(); const { onSelect: handleAuthSelect } = mockedRadioButtonSelect.mock.calls[0][0]; await handleAuthSelect(AuthType.USE_GEMINI); @@ -278,10 +259,7 @@ describe('AuthDialog', () => { vi.stubEnv('GEMINI_API_KEY', 'test-key-from-env'); // props.settings.merged.security.auth.selectedType is undefined here, simulating initial setup - const { waitUntilReady, unmount } = await renderWithProviders( - , - ); - await waitUntilReady(); + const { unmount } = await renderWithProviders(); const { onSelect: handleAuthSelect } = mockedRadioButtonSelect.mock.calls[0][0]; await handleAuthSelect(AuthType.USE_GEMINI); @@ -297,10 +275,7 @@ describe('AuthDialog', () => { vi.stubEnv('GEMINI_API_KEY', ''); // Empty string // props.settings.merged.security.auth.selectedType is undefined here - const { waitUntilReady, unmount } = await renderWithProviders( - , - ); - await waitUntilReady(); + const { unmount } = await renderWithProviders(); const { onSelect: handleAuthSelect } = mockedRadioButtonSelect.mock.calls[0][0]; await handleAuthSelect(AuthType.USE_GEMINI); @@ -316,10 +291,7 @@ describe('AuthDialog', () => { // process.env['GEMINI_API_KEY'] is not set // props.settings.merged.security.auth.selectedType is undefined here, simulating initial setup - const { waitUntilReady, unmount } = await renderWithProviders( - , - ); - await waitUntilReady(); + const { unmount } = await renderWithProviders(); const { onSelect: handleAuthSelect } = mockedRadioButtonSelect.mock.calls[0][0]; await handleAuthSelect(AuthType.USE_GEMINI); @@ -337,10 +309,7 @@ describe('AuthDialog', () => { props.settings.merged.security.auth.selectedType = AuthType.LOGIN_WITH_GOOGLE; - const { waitUntilReady, unmount } = await renderWithProviders( - , - ); - await waitUntilReady(); + const { unmount } = await renderWithProviders(); const { onSelect: handleAuthSelect } = mockedRadioButtonSelect.mock.calls[0][0]; await handleAuthSelect(AuthType.USE_GEMINI); @@ -360,10 +329,7 @@ describe('AuthDialog', () => { vi.mocked(props.config.isBrowserLaunchSuppressed).mockReturnValue(true); mockedValidateAuthMethod.mockReturnValue(null); - const { waitUntilReady, unmount } = await renderWithProviders( - , - ); - await waitUntilReady(); + const { unmount } = await renderWithProviders(); const { onSelect: handleAuthSelect } = mockedRadioButtonSelect.mock.calls[0][0]; await act(async () => { @@ -383,10 +349,9 @@ describe('AuthDialog', () => { it('displays authError when provided', async () => { props.authError = 'Something went wrong'; - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); expect(lastFrame()).toContain('Something went wrong'); unmount(); }); @@ -429,10 +394,7 @@ describe('AuthDialog', () => { }, ])('$desc', async ({ setup, expectations }) => { setup(); - const { waitUntilReady, unmount } = await renderWithProviders( - , - ); - await waitUntilReady(); + const { unmount } = await renderWithProviders(); const keypressHandler = mockedUseKeypress.mock.calls[0][0]; keypressHandler({ name: 'escape' }); expectations(props); @@ -442,30 +404,27 @@ describe('AuthDialog', () => { describe('Snapshots', () => { it('renders correctly with default props', async () => { - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); unmount(); }); it('renders correctly with auth error', async () => { props.authError = 'Something went wrong'; - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); unmount(); }); it('renders correctly with enforced auth type', async () => { props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI; - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); unmount(); }); diff --git a/packages/cli/src/ui/auth/AuthInProgress.test.tsx b/packages/cli/src/ui/auth/AuthInProgress.test.tsx index bd6a3cb126..1c392be28d 100644 --- a/packages/cli/src/ui/auth/AuthInProgress.test.tsx +++ b/packages/cli/src/ui/auth/AuthInProgress.test.tsx @@ -55,20 +55,18 @@ describe('AuthInProgress', () => { }); it('renders initial state with spinner', async () => { - const { lastFrame, waitUntilReady, unmount } = render( + const { lastFrame, unmount } = await render( , ); - 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 } = render( + const { waitUntilReady, unmount } = await render( , ); - await waitUntilReady(); const keypressHandler = vi.mocked(useKeypress).mock.calls[0][0]; await act(async () => { @@ -84,10 +82,9 @@ describe('AuthInProgress', () => { }); it('calls onTimeout when Ctrl+C is pressed', async () => { - const { waitUntilReady, unmount } = render( + const { waitUntilReady, unmount } = await render( , ); - await waitUntilReady(); const keypressHandler = vi.mocked(useKeypress).mock.calls[0][0]; await act(async () => { @@ -100,10 +97,9 @@ describe('AuthInProgress', () => { }); it('calls onTimeout and shows timeout message after 3 minutes', async () => { - const { lastFrame, waitUntilReady, unmount } = render( + const { lastFrame, waitUntilReady, unmount } = await render( , ); - await waitUntilReady(); await act(async () => { vi.advanceTimersByTime(180000); @@ -116,10 +112,7 @@ describe('AuthInProgress', () => { }); it('clears timer on unmount', async () => { - const { waitUntilReady, unmount } = render( - , - ); - await waitUntilReady(); + const { unmount } = await render(); await act(async () => { unmount(); diff --git a/packages/cli/src/ui/auth/BannedAccountDialog.test.tsx b/packages/cli/src/ui/auth/BannedAccountDialog.test.tsx index 0670c81bc9..4b5d44e6d5 100644 --- a/packages/cli/src/ui/auth/BannedAccountDialog.test.tsx +++ b/packages/cli/src/ui/auth/BannedAccountDialog.test.tsx @@ -73,14 +73,13 @@ describe('BannedAccountDialog', () => { }); it('renders the suspension message from accountSuspensionInfo', async () => { - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); const frame = lastFrame(); expect(frame).toContain('Account Suspended'); expect(frame).toContain('violation of Terms of Service'); @@ -89,14 +88,13 @@ describe('BannedAccountDialog', () => { }); it('renders menu options with appeal link text from response', async () => { - const { waitUntilReady, unmount } = await renderWithProviders( + const { unmount } = await renderWithProviders( , ); - await waitUntilReady(); const items = mockedRadioButtonSelect.mock.calls[0][0].items; expect(items).toHaveLength(3); expect(items[0].label).toBe('Appeal Here'); @@ -109,14 +107,13 @@ describe('BannedAccountDialog', () => { const infoWithoutUrl: AccountSuspensionInfo = { message: 'Account suspended.', }; - const { waitUntilReady, unmount } = await renderWithProviders( + const { unmount } = await renderWithProviders( , ); - await waitUntilReady(); const items = mockedRadioButtonSelect.mock.calls[0][0].items; expect(items).toHaveLength(2); expect(items[0].label).toBe('Change authentication'); @@ -129,28 +126,26 @@ describe('BannedAccountDialog', () => { message: 'Account suspended.', appealUrl: 'https://example.com/appeal', }; - const { waitUntilReady, unmount } = await renderWithProviders( + const { unmount } = await renderWithProviders( , ); - await waitUntilReady(); const items = mockedRadioButtonSelect.mock.calls[0][0].items; expect(items[0].label).toBe('Open the Google Form'); unmount(); }); it('opens browser when appeal option is selected', async () => { - const { waitUntilReady, unmount } = await renderWithProviders( + const { unmount } = await renderWithProviders( , ); - await waitUntilReady(); const { onSelect } = mockedRadioButtonSelect.mock.calls[0][0]; await onSelect('open_form'); expect(mockedOpenBrowser).toHaveBeenCalledWith( @@ -162,14 +157,13 @@ describe('BannedAccountDialog', () => { it('shows URL when browser cannot be launched', async () => { mockedShouldLaunchBrowser.mockReturnValue(false); - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); const { onSelect } = mockedRadioButtonSelect.mock.calls[0][0]; onSelect('open_form'); await waitFor(() => { @@ -180,14 +174,13 @@ describe('BannedAccountDialog', () => { }); it('calls onExit when "Exit" is selected', async () => { - const { waitUntilReady, unmount } = await renderWithProviders( + const { unmount } = await renderWithProviders( , ); - await waitUntilReady(); const { onSelect } = mockedRadioButtonSelect.mock.calls[0][0]; await onSelect('exit'); expect(mockedRunExitCleanup).toHaveBeenCalled(); @@ -196,14 +189,13 @@ describe('BannedAccountDialog', () => { }); it('calls onChangeAuth when "Change authentication" is selected', async () => { - const { waitUntilReady, unmount } = await renderWithProviders( + const { unmount } = await renderWithProviders( , ); - await waitUntilReady(); const { onSelect } = mockedRadioButtonSelect.mock.calls[0][0]; onSelect('change_auth'); expect(onChangeAuth).toHaveBeenCalled(); @@ -212,14 +204,13 @@ describe('BannedAccountDialog', () => { }); it('exits on escape key', async () => { - const { waitUntilReady, unmount } = await renderWithProviders( + const { unmount } = await renderWithProviders( , ); - await waitUntilReady(); const keypressHandler = mockedUseKeypress.mock.calls[0][0]; const result = keypressHandler({ name: 'escape' }); expect(result).toBe(true); @@ -227,14 +218,13 @@ describe('BannedAccountDialog', () => { }); it('renders snapshot correctly', async () => { - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); unmount(); }); diff --git a/packages/cli/src/ui/auth/LoginWithGoogleRestartDialog.test.tsx b/packages/cli/src/ui/auth/LoginWithGoogleRestartDialog.test.tsx index 77310e3069..4dd13a3334 100644 --- a/packages/cli/src/ui/auth/LoginWithGoogleRestartDialog.test.tsx +++ b/packages/cli/src/ui/auth/LoginWithGoogleRestartDialog.test.tsx @@ -45,25 +45,23 @@ describe('LoginWithGoogleRestartDialog', () => { }); it('renders correctly', async () => { - const { lastFrame, waitUntilReady, unmount } = render( + const { lastFrame, unmount } = await render( , ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); unmount(); }); it('calls onDismiss when escape is pressed', async () => { - const { waitUntilReady, unmount } = render( + const { unmount } = await render( , ); - await waitUntilReady(); const keypressHandler = mockedUseKeypress.mock.calls[0][0]; keypressHandler({ @@ -83,13 +81,12 @@ describe('LoginWithGoogleRestartDialog', () => { async (keyName) => { vi.useFakeTimers(); - const { waitUntilReady, unmount } = render( + const { unmount } = await render( , ); - await waitUntilReady(); const keypressHandler = mockedUseKeypress.mock.calls[0][0]; keypressHandler({ diff --git a/packages/cli/src/ui/auth/useAuth.test.tsx b/packages/cli/src/ui/auth/useAuth.test.tsx index f236428ff1..8d51e46a64 100644 --- a/packages/cli/src/ui/auth/useAuth.test.tsx +++ b/packages/cli/src/ui/auth/useAuth.test.tsx @@ -4,15 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { - describe, - it, - expect, - vi, - beforeEach, - afterEach, - type Mock, -} from 'vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { act } from 'react'; import { renderHook } from '../../test-utils/render.js'; import { useAuthCommand, validateAuthMethodWithSettings } from './useAuth.js'; import { @@ -22,7 +15,6 @@ import { } from '@google/gemini-cli-core'; import { AuthState } from '../types.js'; import type { LoadedSettings } from '../../config/settings.js'; -import { waitFor } from '../../test-utils/async.js'; // Mock dependencies const mockLoadApiKey = vi.fn(); @@ -142,171 +134,202 @@ describe('useAuth', () => { }, }) as LoadedSettings; + let deferredRefreshAuth: { + resolve: () => void; + reject: (e: Error) => void; + }; + + beforeEach(() => { + vi.mocked(mockConfig.refreshAuth).mockImplementation( + () => + new Promise((resolve, reject) => { + deferredRefreshAuth = { resolve, reject }; + }), + ); + }); + it('should initialize with Unauthenticated state', async () => { - const { result } = renderHook(() => + const { result } = await renderHook(() => useAuthCommand(createSettings(AuthType.LOGIN_WITH_GOOGLE), mockConfig), ); + // Because we defer refreshAuth, the initial state is safely caught here expect(result.current.authState).toBe(AuthState.Unauthenticated); - await waitFor(() => { - expect(result.current.authState).toBe(AuthState.Authenticated); + await act(async () => { + deferredRefreshAuth.resolve(); }); + + expect(result.current.authState).toBe(AuthState.Authenticated); }); it('should set error if no auth type is selected and no env key', async () => { - const { result } = renderHook(() => + const { result } = await renderHook(() => useAuthCommand(createSettings(undefined), mockConfig), ); - await waitFor(() => { - expect(result.current.authError).toBe( - 'No authentication method selected.', - ); - expect(result.current.authState).toBe(AuthState.Updating); - }); + // This happens synchronously, no deferred promise + expect(result.current.authError).toBe( + 'No authentication method selected.', + ); + expect(result.current.authState).toBe(AuthState.Updating); }); it('should set error if no auth type is selected but env key exists', async () => { process.env['GEMINI_API_KEY'] = 'env-key'; - const { result } = renderHook(() => + const { result } = await renderHook(() => useAuthCommand(createSettings(undefined), mockConfig), ); - await waitFor(() => { - expect(result.current.authError).toContain( - 'Existing API key detected (GEMINI_API_KEY)', - ); - expect(result.current.authState).toBe(AuthState.Updating); - }); + expect(result.current.authError).toContain( + 'Existing API key detected (GEMINI_API_KEY)', + ); + expect(result.current.authState).toBe(AuthState.Updating); }); it('should transition to AwaitingApiKeyInput if USE_GEMINI and no key found', async () => { - mockLoadApiKey.mockResolvedValue(null); - const { result } = renderHook(() => + let deferredLoadKey: { resolve: (k: string | null) => void }; + mockLoadApiKey.mockImplementation( + () => + new Promise((resolve) => { + deferredLoadKey = { resolve }; + }), + ); + + const { result } = await renderHook(() => useAuthCommand(createSettings(AuthType.USE_GEMINI), mockConfig), ); - await waitFor(() => { - expect(result.current.authState).toBe(AuthState.AwaitingApiKeyInput); + await act(async () => { + deferredLoadKey.resolve(null); }); + + expect(result.current.authState).toBe(AuthState.AwaitingApiKeyInput); }); it('should authenticate if USE_GEMINI and key is found', async () => { - mockLoadApiKey.mockResolvedValue('stored-key'); - const { result } = renderHook(() => + let deferredLoadKey: { resolve: (k: string | null) => void }; + mockLoadApiKey.mockImplementation( + () => + new Promise((resolve) => { + deferredLoadKey = { resolve }; + }), + ); + + const { result } = await renderHook(() => useAuthCommand(createSettings(AuthType.USE_GEMINI), mockConfig), ); - await waitFor(() => { - expect(mockConfig.refreshAuth).toHaveBeenCalledWith( - AuthType.USE_GEMINI, - ); - expect(result.current.authState).toBe(AuthState.Authenticated); - expect(result.current.apiKeyDefaultValue).toBe('stored-key'); + await act(async () => { + deferredLoadKey.resolve('stored-key'); }); + + await act(async () => { + deferredRefreshAuth.resolve(); + }); + + expect(mockConfig.refreshAuth).toHaveBeenCalledWith(AuthType.USE_GEMINI); + expect(result.current.authState).toBe(AuthState.Authenticated); + expect(result.current.apiKeyDefaultValue).toBe('stored-key'); }); it('should authenticate if USE_GEMINI and env key is found', async () => { - mockLoadApiKey.mockResolvedValue(null); process.env['GEMINI_API_KEY'] = 'env-key'; - const { result } = renderHook(() => + + const { result } = await renderHook(() => useAuthCommand(createSettings(AuthType.USE_GEMINI), mockConfig), ); - await waitFor(() => { - expect(mockConfig.refreshAuth).toHaveBeenCalledWith( - AuthType.USE_GEMINI, - ); - expect(result.current.authState).toBe(AuthState.Authenticated); - expect(result.current.apiKeyDefaultValue).toBe('env-key'); + await act(async () => { + deferredRefreshAuth.resolve(); }); + + expect(mockConfig.refreshAuth).toHaveBeenCalledWith(AuthType.USE_GEMINI); + expect(result.current.authState).toBe(AuthState.Authenticated); + expect(result.current.apiKeyDefaultValue).toBe('env-key'); }); it('should prioritize env key over stored key when both are present', async () => { - mockLoadApiKey.mockResolvedValue('stored-key'); process.env['GEMINI_API_KEY'] = 'env-key'; - const { result } = renderHook(() => + + const { result } = await renderHook(() => useAuthCommand(createSettings(AuthType.USE_GEMINI), mockConfig), ); - await waitFor(() => { - expect(mockConfig.refreshAuth).toHaveBeenCalledWith( - AuthType.USE_GEMINI, - ); - expect(result.current.authState).toBe(AuthState.Authenticated); - // The environment key should take precedence - expect(result.current.apiKeyDefaultValue).toBe('env-key'); + await act(async () => { + deferredRefreshAuth.resolve(); }); + + expect(mockConfig.refreshAuth).toHaveBeenCalledWith(AuthType.USE_GEMINI); + expect(result.current.authState).toBe(AuthState.Authenticated); + expect(result.current.apiKeyDefaultValue).toBe('env-key'); }); it('should set error if validation fails', async () => { mockValidateAuthMethod.mockReturnValue('Validation Failed'); - const { result } = renderHook(() => + const { result } = await renderHook(() => useAuthCommand(createSettings(AuthType.LOGIN_WITH_GOOGLE), mockConfig), ); - await waitFor(() => { - expect(result.current.authError).toBe('Validation Failed'); - expect(result.current.authState).toBe(AuthState.Updating); - }); + expect(result.current.authError).toBe('Validation Failed'); + expect(result.current.authState).toBe(AuthState.Updating); }); it('should set error if GEMINI_DEFAULT_AUTH_TYPE is invalid', async () => { process.env['GEMINI_DEFAULT_AUTH_TYPE'] = 'INVALID_TYPE'; - const { result } = renderHook(() => + const { result } = await renderHook(() => useAuthCommand(createSettings(AuthType.LOGIN_WITH_GOOGLE), mockConfig), ); - await waitFor(() => { - expect(result.current.authError).toContain( - 'Invalid value for GEMINI_DEFAULT_AUTH_TYPE', - ); - expect(result.current.authState).toBe(AuthState.Updating); - }); + expect(result.current.authError).toContain( + 'Invalid value for GEMINI_DEFAULT_AUTH_TYPE', + ); + expect(result.current.authState).toBe(AuthState.Updating); }); it('should authenticate successfully for valid auth type', async () => { - const { result } = renderHook(() => + const { result } = await renderHook(() => useAuthCommand(createSettings(AuthType.LOGIN_WITH_GOOGLE), mockConfig), ); - await waitFor(() => { - expect(mockConfig.refreshAuth).toHaveBeenCalledWith( - AuthType.LOGIN_WITH_GOOGLE, - ); - expect(result.current.authState).toBe(AuthState.Authenticated); - expect(result.current.authError).toBeNull(); + await act(async () => { + deferredRefreshAuth.resolve(); }); + + expect(mockConfig.refreshAuth).toHaveBeenCalledWith( + AuthType.LOGIN_WITH_GOOGLE, + ); + expect(result.current.authState).toBe(AuthState.Authenticated); + expect(result.current.authError).toBeNull(); }); it('should handle refreshAuth failure', async () => { - (mockConfig.refreshAuth as Mock).mockRejectedValue( - new Error('Auth Failed'), - ); - const { result } = renderHook(() => + const { result } = await renderHook(() => useAuthCommand(createSettings(AuthType.LOGIN_WITH_GOOGLE), mockConfig), ); - await waitFor(() => { - expect(result.current.authError).toContain('Failed to sign in'); - expect(result.current.authState).toBe(AuthState.Updating); + await act(async () => { + deferredRefreshAuth.reject(new Error('Auth Failed')); }); + + expect(result.current.authError).toContain('Failed to sign in'); + expect(result.current.authState).toBe(AuthState.Updating); }); it('should handle ProjectIdRequiredError without "Failed to login" prefix', async () => { const projectIdError = new ProjectIdRequiredError(); - (mockConfig.refreshAuth as Mock).mockRejectedValue(projectIdError); - const { result } = renderHook(() => + const { result } = await renderHook(() => useAuthCommand(createSettings(AuthType.LOGIN_WITH_GOOGLE), mockConfig), ); - await waitFor(() => { - expect(result.current.authError).toBe( - 'This account requires setting the GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_PROJECT_ID env var. See https://goo.gle/gemini-cli-auth-docs#workspace-gca', - ); - expect(result.current.authError).not.toContain('Failed to login'); - expect(result.current.authState).toBe(AuthState.Updating); + await act(async () => { + deferredRefreshAuth.reject(projectIdError); }); + + expect(result.current.authError).toBe( + 'This account requires setting the GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_PROJECT_ID env var. See https://goo.gle/gemini-cli-auth-docs#workspace-gca', + ); + expect(result.current.authError).not.toContain('Failed to login'); + expect(result.current.authState).toBe(AuthState.Updating); }); }); }); diff --git a/packages/cli/src/ui/components/AboutBox.test.tsx b/packages/cli/src/ui/components/AboutBox.test.tsx index 1db36b1f60..9115ca31c1 100644 --- a/packages/cli/src/ui/components/AboutBox.test.tsx +++ b/packages/cli/src/ui/components/AboutBox.test.tsx @@ -25,10 +25,9 @@ describe('AboutBox', () => { }; it('renders with required props', async () => { - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); const output = lastFrame(); expect(output).toContain('About Gemini CLI'); expect(output).toContain('1.0.0'); @@ -46,10 +45,9 @@ describe('AboutBox', () => { ['tier', 'Enterprise', 'Tier'], ])('renders optional prop %s', async (prop, value, label) => { const props = { ...defaultProps, [prop]: value }; - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); const output = lastFrame(); expect(output).toContain(label); expect(output).toContain(value); @@ -58,10 +56,9 @@ describe('AboutBox', () => { it('renders Auth Method with email when userEmail is provided', async () => { const props = { ...defaultProps, userEmail: 'test@example.com' }; - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); const output = lastFrame(); expect(output).toContain('Signed in with Google (test@example.com)'); unmount(); @@ -69,10 +66,9 @@ describe('AboutBox', () => { it('renders Auth Method correctly when not oauth', async () => { const props = { ...defaultProps, selectedAuthType: 'api-key' }; - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); const output = lastFrame(); expect(output).toContain('api-key'); unmount(); diff --git a/packages/cli/src/ui/components/AdminSettingsChangedDialog.test.tsx b/packages/cli/src/ui/components/AdminSettingsChangedDialog.test.tsx index 19db058b87..76a36fe4dc 100644 --- a/packages/cli/src/ui/components/AdminSettingsChangedDialog.test.tsx +++ b/packages/cli/src/ui/components/AdminSettingsChangedDialog.test.tsx @@ -17,15 +17,14 @@ describe('AdminSettingsChangedDialog', () => { }); it('renders correctly', async () => { - const { lastFrame, waitUntilReady } = await renderWithProviders( + const { lastFrame } = await renderWithProviders( , ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); }); it('restarts on "r" key press', async () => { - const { stdin, waitUntilReady } = await renderWithProviders( + const { stdin } = await renderWithProviders( , { uiActions: { @@ -33,7 +32,6 @@ describe('AdminSettingsChangedDialog', () => { }, }, ); - await waitUntilReady(); act(() => { stdin.write('r'); @@ -43,7 +41,7 @@ describe('AdminSettingsChangedDialog', () => { }); it.each(['r', 'R'])('restarts on "%s" key press', async (key) => { - const { stdin, waitUntilReady } = await renderWithProviders( + const { stdin } = await renderWithProviders( , { uiActions: { @@ -51,7 +49,6 @@ describe('AdminSettingsChangedDialog', () => { }, }, ); - await waitUntilReady(); act(() => { stdin.write(key); diff --git a/packages/cli/src/ui/components/AgentConfigDialog.test.tsx b/packages/cli/src/ui/components/AgentConfigDialog.test.tsx index a2bfe052bb..2c6ea454db 100644 --- a/packages/cli/src/ui/components/AgentConfigDialog.test.tsx +++ b/packages/cli/src/ui/components/AgentConfigDialog.test.tsx @@ -126,7 +126,6 @@ describe('AgentConfigDialog', () => { />, { settings, uiState: { mainAreaWidth: 100 } }, ); - await result.waitUntilReady(); return result; }; diff --git a/packages/cli/src/ui/components/AlternateBufferQuittingDisplay.test.tsx b/packages/cli/src/ui/components/AlternateBufferQuittingDisplay.test.tsx index da71895485..571e0d36d3 100644 --- a/packages/cli/src/ui/components/AlternateBufferQuittingDisplay.test.tsx +++ b/packages/cli/src/ui/components/AlternateBufferQuittingDisplay.test.tsx @@ -108,7 +108,7 @@ describe('AlternateBufferQuittingDisplay', () => { it('renders with active and pending tool messages', async () => { persistentStateMock.setData({ tipsShown: 0 }); - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , { uiState: { @@ -118,14 +118,13 @@ describe('AlternateBufferQuittingDisplay', () => { }, }, ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot('with_history_and_pending'); unmount(); }); it('renders with empty history and no pending items', async () => { persistentStateMock.setData({ tipsShown: 0 }); - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , { uiState: { @@ -135,14 +134,13 @@ describe('AlternateBufferQuittingDisplay', () => { }, }, ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot('empty'); unmount(); }); it('renders with history but no pending items', async () => { persistentStateMock.setData({ tipsShown: 0 }); - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , { uiState: { @@ -152,14 +150,13 @@ describe('AlternateBufferQuittingDisplay', () => { }, }, ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot('with_history_no_pending'); unmount(); }); it('renders with pending items but no history', async () => { persistentStateMock.setData({ tipsShown: 0 }); - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , { uiState: { @@ -169,7 +166,6 @@ describe('AlternateBufferQuittingDisplay', () => { }, }, ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot('with_pending_no_history'); unmount(); }); @@ -195,7 +191,7 @@ describe('AlternateBufferQuittingDisplay', () => { ], }, ]; - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , { uiState: { @@ -205,7 +201,6 @@ describe('AlternateBufferQuittingDisplay', () => { }, }, ); - await waitUntilReady(); const output = lastFrame(); expect(output).toContain('Action Required (was prompted):'); expect(output).toContain('confirming_tool'); @@ -220,7 +215,7 @@ describe('AlternateBufferQuittingDisplay', () => { { id: 1, type: 'user', text: 'Hello Gemini' }, { id: 2, type: 'gemini', text: 'Hello User!' }, ]; - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , { uiState: { @@ -230,7 +225,6 @@ describe('AlternateBufferQuittingDisplay', () => { }, }, ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot('with_user_gemini_messages'); unmount(); }); diff --git a/packages/cli/src/ui/components/AnsiOutput.test.tsx b/packages/cli/src/ui/components/AnsiOutput.test.tsx index ac824fefe6..758361be0a 100644 --- a/packages/cli/src/ui/components/AnsiOutput.test.tsx +++ b/packages/cli/src/ui/components/AnsiOutput.test.tsx @@ -29,10 +29,9 @@ describe('', () => { createAnsiToken({ text: 'world!' }), ], ]; - const { lastFrame, waitUntilReady, unmount } = render( + const { lastFrame, unmount } = await render( , ); - await waitUntilReady(); expect(lastFrame().trim()).toBe('Hello, world!'); unmount(); }); @@ -47,10 +46,9 @@ describe('', () => { { style: { inverse: true }, text: 'Inverse' }, ])('correctly applies style $text', async ({ style, text }) => { const data: AnsiOutput = [[createAnsiToken({ text, ...style })]]; - const { lastFrame, waitUntilReady, unmount } = render( + const { lastFrame, unmount } = await render( , ); - await waitUntilReady(); expect(lastFrame().trim()).toBe(text); unmount(); }); @@ -61,10 +59,9 @@ describe('', () => { { color: { fg: '#00ff00', bg: '#ff00ff' }, text: 'Green FG Magenta BG' }, ])('correctly applies color $text', async ({ color, text }) => { const data: AnsiOutput = [[createAnsiToken({ text, ...color })]]; - const { lastFrame, waitUntilReady, unmount } = render( + const { lastFrame, unmount } = await render( , ); - await waitUntilReady(); expect(lastFrame().trim()).toBe(text); unmount(); }); @@ -76,10 +73,9 @@ describe('', () => { [createAnsiToken({ text: 'Third line' })], [createAnsiToken({ text: '' })], ]; - const { lastFrame, waitUntilReady, unmount } = render( + const { lastFrame, unmount } = await render( , ); - await waitUntilReady(); const output = lastFrame(); expect(output).toBeDefined(); const lines = output.split('\n'); @@ -96,10 +92,9 @@ describe('', () => { [createAnsiToken({ text: 'Line 3' })], [createAnsiToken({ text: 'Line 4' })], ]; - const { lastFrame, waitUntilReady, unmount } = render( + const { lastFrame, unmount } = await render( , ); - await waitUntilReady(); const output = lastFrame(); expect(output).not.toContain('Line 1'); expect(output).not.toContain('Line 2'); @@ -115,10 +110,9 @@ describe('', () => { [createAnsiToken({ text: 'Line 3' })], [createAnsiToken({ text: 'Line 4' })], ]; - const { lastFrame, waitUntilReady, unmount } = render( + const { lastFrame, unmount } = await render( , ); - await waitUntilReady(); const output = lastFrame(); expect(output).not.toContain('Line 1'); expect(output).not.toContain('Line 2'); @@ -135,7 +129,7 @@ describe('', () => { [createAnsiToken({ text: 'Line 4' })], ]; // availableTerminalHeight=3, maxLines=2 => show 2 lines - const { lastFrame, waitUntilReady, unmount } = render( + const { lastFrame, unmount } = await render( ', () => { width={80} />, ); - await waitUntilReady(); const output = lastFrame(); expect(output).not.toContain('Line 2'); expect(output).toContain('Line 3'); @@ -156,10 +149,9 @@ describe('', () => { for (let i = 0; i < 1000; i++) { largeData.push([createAnsiToken({ text: `Line ${i}` })]); } - const { lastFrame, waitUntilReady, unmount } = render( + const { lastFrame, unmount } = await render( , ); - await waitUntilReady(); // We are just checking that it renders something without crashing. expect(lastFrame()).toBeDefined(); unmount(); diff --git a/packages/cli/src/ui/components/AppHeader.test.tsx b/packages/cli/src/ui/components/AppHeader.test.tsx index 0d7e2b3a7b..8ff4caaacf 100644 --- a/packages/cli/src/ui/components/AppHeader.test.tsx +++ b/packages/cli/src/ui/components/AppHeader.test.tsx @@ -27,13 +27,12 @@ describe('', () => { bannerVisible: true, }; - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , { uiState, }, ); - await waitUntilReady(); expect(lastFrame()).toContain('This is the default banner'); expect(lastFrame()).toMatchSnapshot(); @@ -50,13 +49,12 @@ describe('', () => { bannerVisible: true, }; - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , { uiState, }, ); - await waitUntilReady(); expect(lastFrame()).toContain('There are capacity issues'); expect(lastFrame()).toMatchSnapshot(); @@ -72,13 +70,12 @@ describe('', () => { }, }; - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , { uiState, }, ); - await waitUntilReady(); expect(lastFrame()).not.toContain('Banner'); expect(lastFrame()).toMatchSnapshot(); @@ -103,13 +100,12 @@ describe('', () => { }, }); - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , { uiState, }, ); - await waitUntilReady(); expect(lastFrame()).not.toContain('This is the default banner'); expect(lastFrame()).toMatchSnapshot(); @@ -129,13 +125,12 @@ describe('', () => { // and interfering with the expected persistentState.set call. persistentStateMock.setData({ tipsShown: 10 }); - const { waitUntilReady, unmount } = await renderWithProviders( + const { unmount } = await renderWithProviders( , { uiState, }, ); - await waitUntilReady(); expect(persistentStateMock.set).toHaveBeenCalledWith( 'defaultBannerShownCount', @@ -159,13 +154,12 @@ describe('', () => { bannerVisible: true, }; - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , { uiState, }, ); - await waitUntilReady(); expect(lastFrame()).not.toContain('First line\\nSecond line'); unmount(); @@ -183,13 +177,12 @@ describe('', () => { persistentStateMock.setData({ tipsShown: 5 }); - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , { uiState, }, ); - await waitUntilReady(); expect(lastFrame()).toContain('Tips'); expect(persistentStateMock.set).toHaveBeenCalledWith('tipsShown', 6); @@ -206,13 +199,12 @@ describe('', () => { persistentStateMock.setData({ tipsShown: 10 }); - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , { uiState, }, ); - await waitUntilReady(); expect(lastFrame()).not.toContain('Tips'); unmount(); @@ -234,7 +226,6 @@ describe('', () => { const session1 = await renderWithProviders(, { uiState, }); - await session1.waitUntilReady(); expect(session1.lastFrame()).toContain('Tips'); expect(persistentStateMock.get('tipsShown')).toBe(10); @@ -245,7 +236,6 @@ describe('', () => { , {}, ); - await session2.waitUntilReady(); expect(session2.lastFrame()).not.toContain('Tips'); session2.unmount(); diff --git a/packages/cli/src/ui/components/ApprovalModeIndicator.test.tsx b/packages/cli/src/ui/components/ApprovalModeIndicator.test.tsx index 4386891c7a..1b2decbe16 100644 --- a/packages/cli/src/ui/components/ApprovalModeIndicator.test.tsx +++ b/packages/cli/src/ui/components/ApprovalModeIndicator.test.tsx @@ -11,56 +11,50 @@ import { ApprovalMode } from '@google/gemini-cli-core'; describe('ApprovalModeIndicator', () => { it('renders correctly for AUTO_EDIT mode', async () => { - const { lastFrame, waitUntilReady } = render( + const { lastFrame } = await render( , ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); }); it('renders correctly for AUTO_EDIT mode with plan enabled', async () => { - const { lastFrame, waitUntilReady } = render( + const { lastFrame } = await render( , ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); }); it('renders correctly for PLAN mode', async () => { - const { lastFrame, waitUntilReady } = render( + const { lastFrame } = await render( , ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); }); it('renders correctly for YOLO mode', async () => { - const { lastFrame, waitUntilReady } = render( + const { lastFrame } = await render( , ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); }); it('renders correctly for DEFAULT mode', async () => { - const { lastFrame, waitUntilReady } = render( + const { lastFrame } = await render( , ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); }); it('renders correctly for DEFAULT mode with plan enabled', async () => { - const { lastFrame, waitUntilReady } = render( + const { lastFrame } = await render( , ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); }); }); diff --git a/packages/cli/src/ui/components/AskUserDialog.test.tsx b/packages/cli/src/ui/components/AskUserDialog.test.tsx index 8ed240389c..864800a061 100644 --- a/packages/cli/src/ui/components/AskUserDialog.test.tsx +++ b/packages/cli/src/ui/components/AskUserDialog.test.tsx @@ -48,7 +48,7 @@ describe('AskUserDialog', () => { ]; it('renders question and options', async () => { - const { lastFrame, waitUntilReady } = await renderWithProviders( + const { lastFrame } = await renderWithProviders( { { width: 120 }, ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); }); @@ -397,7 +396,7 @@ describe('AskUserDialog', () => { }, ]; - const { lastFrame, waitUntilReady } = await renderWithProviders( + const { lastFrame } = await renderWithProviders( { { width: 120 }, ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); }); it('hides progress header for single question', async () => { - const { lastFrame, waitUntilReady } = await renderWithProviders( + const { lastFrame } = await renderWithProviders( { { width: 120 }, ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); }); it('shows keyboard hints', async () => { - const { lastFrame, waitUntilReady } = await renderWithProviders( + const { lastFrame } = await renderWithProviders( { { width: 120 }, ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); }); @@ -471,7 +467,6 @@ describe('AskUserDialog', () => { { width: 120 }, ); - await waitUntilReady(); expect(lastFrame()).toContain('Which testing framework?'); writeKey(stdin, '\x1b[C'); // Right arrow @@ -582,7 +577,7 @@ describe('AskUserDialog', () => { }, ]; - const { lastFrame, waitUntilReady } = await renderWithProviders( + const { lastFrame } = await renderWithProviders( { { width: 120 }, ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); }); @@ -736,7 +730,7 @@ describe('AskUserDialog', () => { }, ]; - const { lastFrame, waitUntilReady } = await renderWithProviders( + const { lastFrame } = await renderWithProviders( { { width: 120 }, ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); }); @@ -759,7 +752,7 @@ describe('AskUserDialog', () => { }, ]; - const { lastFrame, waitUntilReady } = await renderWithProviders( + const { lastFrame } = await renderWithProviders( { { width: 120 }, ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); }); @@ -820,7 +812,7 @@ describe('AskUserDialog', () => { }, ]; - const { lastFrame, waitUntilReady } = await renderWithProviders( + const { lastFrame } = await renderWithProviders( { { width: 120 }, ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); }); diff --git a/packages/cli/src/ui/components/BackgroundShellDisplay.test.tsx b/packages/cli/src/ui/components/BackgroundShellDisplay.test.tsx index 847dcd9a87..c097028a0d 100644 --- a/packages/cli/src/ui/components/BackgroundShellDisplay.test.tsx +++ b/packages/cli/src/ui/components/BackgroundShellDisplay.test.tsx @@ -145,7 +145,7 @@ describe('', () => { it('renders the output of the active shell', async () => { const width = 80; - const { lastFrame, waitUntilReady, unmount } = render( + const { lastFrame, unmount } = await render( ', () => { , width, ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); unmount(); @@ -166,7 +165,7 @@ describe('', () => { it('renders tabs for multiple shells', async () => { const width = 100; - const { lastFrame, waitUntilReady, unmount } = render( + const { lastFrame, unmount } = await render( ', () => { , width, ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); unmount(); @@ -187,7 +185,7 @@ describe('', () => { it('highlights the focused state', async () => { const width = 80; - const { lastFrame, waitUntilReady, unmount } = render( + const { lastFrame, unmount } = await render( ', () => { , width, ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); unmount(); @@ -208,7 +205,7 @@ describe('', () => { it('resizes the PTY on mount and when dimensions change', async () => { const width = 80; - const { rerender, waitUntilReady, unmount } = render( + const { rerender, unmount } = await render( ', () => { , width, ); - await waitUntilReady(); expect(ShellExecutionService.resizePty).toHaveBeenCalledWith( shell1.pid, @@ -241,7 +237,6 @@ describe('', () => { /> , ); - await waitUntilReady(); expect(ShellExecutionService.resizePty).toHaveBeenCalledWith( shell1.pid, @@ -253,7 +248,7 @@ describe('', () => { it('renders the process list when isListOpenProp is true', async () => { const width = 80; - const { lastFrame, waitUntilReady, unmount } = render( + const { lastFrame, unmount } = await render( ', () => { , width, ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); unmount(); @@ -274,7 +268,7 @@ describe('', () => { it('selects the current process and closes the list when Ctrl+L is pressed in list view', async () => { const width = 80; - const { waitUntilReady, unmount } = render( + const { unmount } = await render( ', () => { , width, ); - await waitUntilReady(); // Simulate down arrow to select the second process (handled by RadioButtonSelect) await act(async () => { simulateKey({ name: 'down' }); }); - await waitUntilReady(); // Simulate Ctrl+L (handled by BackgroundShellDisplay) await act(async () => { simulateKey({ name: 'l', ctrl: true }); }); - await waitUntilReady(); expect(mockSetActiveBackgroundShellPid).toHaveBeenCalledWith(shell2.pid); expect(mockSetIsBackgroundShellListOpen).toHaveBeenCalledWith(false); @@ -308,7 +299,7 @@ describe('', () => { it('kills the highlighted process when Ctrl+K is pressed in list view', async () => { const width = 80; - const { waitUntilReady, unmount } = render( + const { unmount } = await render( ', () => { , width, ); - await waitUntilReady(); // Initial state: shell1 (active) is highlighted @@ -329,13 +319,11 @@ describe('', () => { await act(async () => { simulateKey({ name: 'down' }); }); - await waitUntilReady(); // Press Ctrl+K await act(async () => { simulateKey({ name: 'k', ctrl: true }); }); - await waitUntilReady(); expect(mockDismissBackgroundShell).toHaveBeenCalledWith(shell2.pid); unmount(); @@ -343,7 +331,7 @@ describe('', () => { it('kills the active process when Ctrl+K is pressed in output view', async () => { const width = 80; - const { waitUntilReady, unmount } = render( + const { unmount } = await render( ', () => { , width, ); - await waitUntilReady(); await act(async () => { simulateKey({ name: 'k', ctrl: true }); }); - await waitUntilReady(); expect(mockDismissBackgroundShell).toHaveBeenCalledWith(shell1.pid); unmount(); @@ -370,7 +356,7 @@ describe('', () => { it('scrolls to active shell when list opens', async () => { // shell2 is active const width = 80; - const { lastFrame, waitUntilReady, unmount } = render( + const { lastFrame, unmount } = await render( ', () => { , width, ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); unmount(); @@ -402,7 +387,7 @@ describe('', () => { mockShells.set(exitedShell.pid, exitedShell); const width = 80; - const { lastFrame, waitUntilReady, unmount } = render( + const { lastFrame, unmount } = await render( ', () => { , width, ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); unmount(); diff --git a/packages/cli/src/ui/components/Checklist.test.tsx b/packages/cli/src/ui/components/Checklist.test.tsx index 442ee0400f..329a560aec 100644 --- a/packages/cli/src/ui/components/Checklist.test.tsx +++ b/packages/cli/src/ui/components/Checklist.test.tsx @@ -18,10 +18,9 @@ describe('', () => { ]; it('renders nothing when list is empty', async () => { - const { lastFrame, waitUntilReady } = render( + const { lastFrame } = await render( , ); - await waitUntilReady(); expect(lastFrame({ allowEmpty: true })).toBe(''); }); @@ -30,15 +29,14 @@ describe('', () => { { status: 'completed', label: 'Task 1' }, { status: 'cancelled', label: 'Task 2' }, ]; - const { lastFrame, waitUntilReady } = render( + const { lastFrame } = await render( , ); - await waitUntilReady(); expect(lastFrame({ allowEmpty: true })).toBe(''); }); it('renders summary view correctly (collapsed)', async () => { - const { lastFrame, waitUntilReady } = render( + const { lastFrame } = await render( ', () => { toggleHint="toggle me" />, ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); }); it('renders expanded view correctly', async () => { - const { lastFrame, waitUntilReady } = render( + const { lastFrame } = await render( ', () => { toggleHint="toggle me" />, ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); }); @@ -68,10 +64,9 @@ describe('', () => { { status: 'completed', label: 'Task 1' }, { status: 'pending', label: 'Task 2' }, ]; - const { lastFrame, waitUntilReady } = render( + const { lastFrame } = await render( , ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); }); }); diff --git a/packages/cli/src/ui/components/ChecklistItem.test.tsx b/packages/cli/src/ui/components/ChecklistItem.test.tsx index 4176f7914b..c71af523e1 100644 --- a/packages/cli/src/ui/components/ChecklistItem.test.tsx +++ b/packages/cli/src/ui/components/ChecklistItem.test.tsx @@ -17,8 +17,7 @@ describe('', () => { { status: 'cancelled', label: 'Skipped this' }, { status: 'blocked', label: 'Blocked this' }, ] as ChecklistItemData[])('renders %s item correctly', async (item) => { - const { lastFrame, waitUntilReady } = render(); - await waitUntilReady(); + const { lastFrame } = await render(); expect(lastFrame()).toMatchSnapshot(); }); @@ -28,12 +27,11 @@ describe('', () => { label: 'This is a very long text that should be truncated because the wrap prop is set to truncate', }; - const { lastFrame, waitUntilReady } = render( + const { lastFrame } = await render( , ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); }); @@ -43,12 +41,11 @@ describe('', () => { label: 'This is a very long text that should wrap because the default behavior is wrapping', }; - const { lastFrame, waitUntilReady } = render( + const { lastFrame } = await render( , ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); }); }); diff --git a/packages/cli/src/ui/components/CliSpinner.test.tsx b/packages/cli/src/ui/components/CliSpinner.test.tsx index cca997f370..4da6abb199 100644 --- a/packages/cli/src/ui/components/CliSpinner.test.tsx +++ b/packages/cli/src/ui/components/CliSpinner.test.tsx @@ -17,10 +17,7 @@ describe('', () => { it('should increment debugNumAnimatedComponents on mount and decrement on unmount', async () => { expect(debugState.debugNumAnimatedComponents).toBe(0); - const { waitUntilReady, unmount } = await renderWithProviders( - , - ); - await waitUntilReady(); + const { unmount } = await renderWithProviders(); expect(debugState.debugNumAnimatedComponents).toBe(1); unmount(); expect(debugState.debugNumAnimatedComponents).toBe(0); @@ -28,11 +25,9 @@ describe('', () => { it('should not render when showSpinner is false', async () => { const settings = createMockSettings({ ui: { showSpinner: false } }); - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( - , - { settings }, - ); - await waitUntilReady(); + const { lastFrame, unmount } = await renderWithProviders(, { + settings, + }); expect(lastFrame({ allowEmpty: true })).toBe(''); unmount(); }); diff --git a/packages/cli/src/ui/components/ColorsDisplay.test.tsx b/packages/cli/src/ui/components/ColorsDisplay.test.tsx index fdd08fd653..d934831c0e 100644 --- a/packages/cli/src/ui/components/ColorsDisplay.test.tsx +++ b/packages/cli/src/ui/components/ColorsDisplay.test.tsx @@ -96,10 +96,9 @@ describe('ColorsDisplay', () => { it('renders correctly', async () => { const mockTheme = themeManager.getActiveTheme(); - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); const output = lastFrame(); // Check for title and description diff --git a/packages/cli/src/ui/components/Composer.test.tsx b/packages/cli/src/ui/components/Composer.test.tsx index 641fc24810..8df5f690e7 100644 --- a/packages/cli/src/ui/components/Composer.test.tsx +++ b/packages/cli/src/ui/components/Composer.test.tsx @@ -251,7 +251,7 @@ const renderComposer = async ( config = createMockConfig(), uiActions = createMockUIActions(), ) => { - const result = render( + const result = await render( @@ -262,7 +262,6 @@ const renderComposer = async ( , ); - await result.waitUntilReady(); // Wait for shortcuts hint debounce if using fake timers if (vi.isFakeTimers()) { diff --git a/packages/cli/src/ui/components/ConfigInitDisplay.test.tsx b/packages/cli/src/ui/components/ConfigInitDisplay.test.tsx index 45ead4862e..b4ae8b93b1 100644 --- a/packages/cli/src/ui/components/ConfigInitDisplay.test.tsx +++ b/packages/cli/src/ui/components/ConfigInitDisplay.test.tsx @@ -43,10 +43,7 @@ describe('ConfigInitDisplay', () => { }); it('renders initial state', async () => { - const { lastFrame, waitUntilReady } = await renderWithProviders( - , - ); - await waitUntilReady(); + const { lastFrame } = await renderWithProviders(); expect(lastFrame()).toMatchSnapshot(); }); diff --git a/packages/cli/src/ui/components/ConsentPrompt.test.tsx b/packages/cli/src/ui/components/ConsentPrompt.test.tsx index dd69c44dd5..09a2dde16e 100644 --- a/packages/cli/src/ui/components/ConsentPrompt.test.tsx +++ b/packages/cli/src/ui/components/ConsentPrompt.test.tsx @@ -33,14 +33,13 @@ describe('ConsentPrompt', () => { it('renders a string prompt with MarkdownDisplay', async () => { const prompt = 'Are you sure?'; - const { waitUntilReady, unmount } = render( + const { unmount } = await render( , ); - await waitUntilReady(); expect(MockedMarkdownDisplay).toHaveBeenCalledWith( { @@ -55,14 +54,13 @@ describe('ConsentPrompt', () => { it('renders a ReactNode prompt directly', async () => { const prompt = Are you sure?; - const { lastFrame, waitUntilReady, unmount } = render( + const { lastFrame, unmount } = await render( , ); - await waitUntilReady(); expect(MockedMarkdownDisplay).not.toHaveBeenCalled(); expect(lastFrame()).toContain('Are you sure?'); @@ -71,14 +69,13 @@ describe('ConsentPrompt', () => { it('calls onConfirm with true when "Yes" is selected', async () => { const prompt = 'Are you sure?'; - const { waitUntilReady, unmount } = render( + const { waitUntilReady, unmount } = await render( , ); - await waitUntilReady(); const onSelect = MockedRadioButtonSelect.mock.calls[0][0].onSelect; await act(async () => { @@ -92,14 +89,13 @@ describe('ConsentPrompt', () => { it('calls onConfirm with false when "No" is selected', async () => { const prompt = 'Are you sure?'; - const { waitUntilReady, unmount } = render( + const { waitUntilReady, unmount } = await render( , ); - await waitUntilReady(); const onSelect = MockedRadioButtonSelect.mock.calls[0][0].onSelect; await act(async () => { @@ -113,14 +109,13 @@ describe('ConsentPrompt', () => { it('passes correct items to RadioButtonSelect', async () => { const prompt = 'Are you sure?'; - const { waitUntilReady, unmount } = render( + const { unmount } = await render( , ); - await waitUntilReady(); expect(MockedRadioButtonSelect).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/packages/cli/src/ui/components/ConsoleSummaryDisplay.test.tsx b/packages/cli/src/ui/components/ConsoleSummaryDisplay.test.tsx index cb8db1a895..b7662c3a26 100644 --- a/packages/cli/src/ui/components/ConsoleSummaryDisplay.test.tsx +++ b/packages/cli/src/ui/components/ConsoleSummaryDisplay.test.tsx @@ -10,10 +10,9 @@ import { describe, it, expect } from 'vitest'; describe('ConsoleSummaryDisplay', () => { it('renders nothing when errorCount is 0', async () => { - const { lastFrame, waitUntilReady, unmount } = render( + const { lastFrame, unmount } = await render( , ); - await waitUntilReady(); expect(lastFrame({ allowEmpty: true })).toBe(''); unmount(); }); @@ -22,10 +21,9 @@ describe('ConsoleSummaryDisplay', () => { [1, '1 error'], [5, '5 errors'], ])('renders correct message for %i errors', async (count, expectedText) => { - const { lastFrame, waitUntilReady, unmount } = render( + const { lastFrame, unmount } = await render( , ); - await waitUntilReady(); const output = lastFrame(); expect(output).toContain(expectedText); expect(output).toContain('✖'); diff --git a/packages/cli/src/ui/components/ContextSummaryDisplay.test.tsx b/packages/cli/src/ui/components/ContextSummaryDisplay.test.tsx index f48cfb2a31..1049e97912 100644 --- a/packages/cli/src/ui/components/ContextSummaryDisplay.test.tsx +++ b/packages/cli/src/ui/components/ContextSummaryDisplay.test.tsx @@ -26,8 +26,7 @@ const renderWithWidth = async ( props: React.ComponentProps, ) => { useTerminalSizeMock.mockReturnValue({ columns: width, rows: 24 }); - const result = render(); - await result.waitUntilReady(); + const result = await render(); return result; }; diff --git a/packages/cli/src/ui/components/ContextUsageDisplay.test.tsx b/packages/cli/src/ui/components/ContextUsageDisplay.test.tsx index 904e06635c..d8ec1650ee 100644 --- a/packages/cli/src/ui/components/ContextUsageDisplay.test.tsx +++ b/packages/cli/src/ui/components/ContextUsageDisplay.test.tsx @@ -19,35 +19,33 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { describe('ContextUsageDisplay', () => { it('renders correct percentage used', async () => { - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); const output = lastFrame(); expect(output).toContain('50% used'); unmount(); }); it('renders correctly when usage is 0%', async () => { - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); const output = lastFrame(); expect(output).toContain('0% used'); unmount(); }); it('renders abbreviated label when terminal width is small', async () => { - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( { />, { width: 80 }, ); - await waitUntilReady(); const output = lastFrame(); expect(output).toContain('20%'); expect(output).not.toContain('context used'); @@ -63,28 +60,26 @@ describe('ContextUsageDisplay', () => { }); it('renders 80% correctly', async () => { - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); const output = lastFrame(); expect(output).toContain('80% used'); unmount(); }); it('renders 100% when full', async () => { - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); const output = lastFrame(); expect(output).toContain('100% used'); unmount(); diff --git a/packages/cli/src/ui/components/CopyModeWarning.test.tsx b/packages/cli/src/ui/components/CopyModeWarning.test.tsx index 6f202ced4a..cc20a142dd 100644 --- a/packages/cli/src/ui/components/CopyModeWarning.test.tsx +++ b/packages/cli/src/ui/components/CopyModeWarning.test.tsx @@ -22,8 +22,7 @@ describe('CopyModeWarning', () => { mockUseUIState.mockReturnValue({ copyModeEnabled: false, } as unknown as UIState); - const { lastFrame, waitUntilReady, unmount } = render(); - await waitUntilReady(); + const { lastFrame, unmount } = await render(); expect(lastFrame({ allowEmpty: true })).toBe(''); unmount(); }); @@ -32,8 +31,7 @@ describe('CopyModeWarning', () => { mockUseUIState.mockReturnValue({ copyModeEnabled: true, } as unknown as UIState); - const { lastFrame, waitUntilReady, unmount } = render(); - await waitUntilReady(); + const { lastFrame, unmount } = await render(); expect(lastFrame()).toContain('In Copy Mode'); expect(lastFrame()).toContain('Use Page Up/Down to scroll'); expect(lastFrame()).toContain('Press Ctrl+S or any other key to exit'); diff --git a/packages/cli/src/ui/components/DebugProfiler.test.tsx b/packages/cli/src/ui/components/DebugProfiler.test.tsx index d4c0e28902..a014c740f0 100644 --- a/packages/cli/src/ui/components/DebugProfiler.test.tsx +++ b/packages/cli/src/ui/components/DebugProfiler.test.tsx @@ -242,8 +242,7 @@ describe('DebugProfiler Component', () => { showDebugProfiler: false, constrainHeight: false, } as unknown as UIState); - const { lastFrame, waitUntilReady, unmount } = render(); - await waitUntilReady(); + const { lastFrame, unmount } = await render(); expect(lastFrame({ allowEmpty: true })).toBe(''); unmount(); }); @@ -257,8 +256,7 @@ describe('DebugProfiler Component', () => { profiler.totalIdleFrames = 5; profiler.totalFlickerFrames = 2; - const { lastFrame, waitUntilReady, unmount } = render(); - await waitUntilReady(); + const { lastFrame, unmount } = await render(); const output = lastFrame(); expect(output).toContain('Renders: 10 (total)'); @@ -275,8 +273,7 @@ describe('DebugProfiler Component', () => { const reportActionSpy = vi.spyOn(profiler, 'reportAction'); - const { waitUntilReady, unmount } = render(); - await waitUntilReady(); + const { waitUntilReady, unmount } = await render(); await act(async () => { coreEvents.emitModelChanged('new-model'); @@ -295,8 +292,7 @@ describe('DebugProfiler Component', () => { const reportActionSpy = vi.spyOn(profiler, 'reportAction'); - const { waitUntilReady, unmount } = render(); - await waitUntilReady(); + const { waitUntilReady, unmount } = await render(); await act(async () => { appEvents.emit(AppEvent.SelectionWarning); diff --git a/packages/cli/src/ui/components/DetailedMessagesDisplay.test.tsx b/packages/cli/src/ui/components/DetailedMessagesDisplay.test.tsx index b2f4185842..30f98a6eda 100644 --- a/packages/cli/src/ui/components/DetailedMessagesDisplay.test.tsx +++ b/packages/cli/src/ui/components/DetailedMessagesDisplay.test.tsx @@ -41,13 +41,12 @@ describe('DetailedMessagesDisplay', () => { }); }); it('renders nothing when messages are empty', async () => { - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , { settings: createMockSettings({ ui: { errorVerbosity: 'full' } }), }, ); - await waitUntilReady(); expect(lastFrame({ allowEmpty: true })).toBe(''); unmount(); }); @@ -64,13 +63,12 @@ describe('DetailedMessagesDisplay', () => { clearConsoleMessages: vi.fn(), }); - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , { settings: createMockSettings({ ui: { errorVerbosity: 'full' } }), }, ); - await waitUntilReady(); const output = lastFrame(); expect(output).toMatchSnapshot(); @@ -86,13 +84,12 @@ describe('DetailedMessagesDisplay', () => { clearConsoleMessages: vi.fn(), }); - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , { settings: createMockSettings({ ui: { errorVerbosity: 'low' } }), }, ); - await waitUntilReady(); expect(lastFrame()).toContain('(F12 to close)'); unmount(); }); @@ -106,13 +103,12 @@ describe('DetailedMessagesDisplay', () => { clearConsoleMessages: vi.fn(), }); - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , { settings: createMockSettings({ ui: { errorVerbosity: 'full' } }), }, ); - await waitUntilReady(); expect(lastFrame()).toContain('(F12 to close)'); unmount(); }); @@ -126,13 +122,12 @@ describe('DetailedMessagesDisplay', () => { clearConsoleMessages: vi.fn(), }); - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , { settings: createMockSettings({ ui: { errorVerbosity: 'full' } }), }, ); - await waitUntilReady(); const output = lastFrame(); expect(output).toMatchSnapshot(); diff --git a/packages/cli/src/ui/components/DialogManager.test.tsx b/packages/cli/src/ui/components/DialogManager.test.tsx index 6f6dbb0289..31b28f5223 100644 --- a/packages/cli/src/ui/components/DialogManager.test.tsx +++ b/packages/cli/src/ui/components/DialogManager.test.tsx @@ -104,11 +104,10 @@ describe('DialogManager', () => { }; it('renders nothing by default', async () => { - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , { uiState: baseUiState as Partial as UIState }, ); - await waitUntilReady(); expect(lastFrame({ allowEmpty: true })).toBe(''); unmount(); }); @@ -197,7 +196,7 @@ describe('DialogManager', () => { it.each(testCases)( 'renders %s when state is %o', async (uiStateOverride, expectedComponent) => { - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , { uiState: { @@ -206,7 +205,6 @@ describe('DialogManager', () => { } as Partial as UIState, }, ); - await waitUntilReady(); expect(lastFrame()).toContain(expectedComponent); unmount(); }, diff --git a/packages/cli/src/ui/components/EditorSettingsDialog.test.tsx b/packages/cli/src/ui/components/EditorSettingsDialog.test.tsx index bd995652b1..18b47def7b 100644 --- a/packages/cli/src/ui/components/EditorSettingsDialog.test.tsx +++ b/packages/cli/src/ui/components/EditorSettingsDialog.test.tsx @@ -55,27 +55,25 @@ describe('EditorSettingsDialog', () => { renderWithProviders(ui); it('renders correctly', async () => { - const { lastFrame, waitUntilReady } = await renderWithProvider( + const { lastFrame } = await renderWithProvider( , ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); }); it('calls onSelect when an editor is selected', async () => { const onSelect = vi.fn(); - const { lastFrame, waitUntilReady } = await renderWithProvider( + const { lastFrame } = await renderWithProvider( , ); - await waitUntilReady(); expect(lastFrame()).toContain('VS Code'); }); @@ -88,7 +86,6 @@ describe('EditorSettingsDialog', () => { onExit={vi.fn()} />, ); - await waitUntilReady(); // Initial focus on editor expect(lastFrame()).toContain('> Select Editor'); @@ -134,7 +131,6 @@ describe('EditorSettingsDialog', () => { onExit={onExit} />, ); - await waitUntilReady(); await act(async () => { stdin.write('\u001B'); // Escape @@ -162,14 +158,13 @@ describe('EditorSettingsDialog', () => { }, } as unknown as LoadedSettings; - const { lastFrame, waitUntilReady } = await renderWithProvider( + const { lastFrame } = await renderWithProvider( , ); - await waitUntilReady(); const frame = lastFrame() || ''; if (!frame.includes('(Also modified')) { diff --git a/packages/cli/src/ui/components/EmptyWalletDialog.test.tsx b/packages/cli/src/ui/components/EmptyWalletDialog.test.tsx index 23a2038b10..74de1a8a41 100644 --- a/packages/cli/src/ui/components/EmptyWalletDialog.test.tsx +++ b/packages/cli/src/ui/components/EmptyWalletDialog.test.tsx @@ -30,7 +30,7 @@ describe('EmptyWalletDialog', () => { describe('rendering', () => { it('should match snapshot with fallback available', async () => { - const { lastFrame, unmount, waitUntilReady } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( { onChoice={mockOnChoice} />, ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); unmount(); }); it('should match snapshot without fallback', async () => { - const { lastFrame, unmount, waitUntilReady } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); unmount(); }); it('should display the model name and usage limit message', async () => { - const { lastFrame, unmount, waitUntilReady } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); const output = lastFrame() ?? ''; expect(output).toContain('gemini-2.5-pro'); @@ -73,13 +70,12 @@ describe('EmptyWalletDialog', () => { }); it('should display purchase prompt and credits update notice', async () => { - const { lastFrame, unmount, waitUntilReady } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); const output = lastFrame() ?? ''; expect(output).toContain('purchase more AI Credits'); @@ -90,14 +86,13 @@ describe('EmptyWalletDialog', () => { }); it('should display reset time when provided', async () => { - const { lastFrame, unmount, waitUntilReady } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); const output = lastFrame() ?? ''; expect(output).toContain('3:45 PM'); @@ -106,13 +101,12 @@ describe('EmptyWalletDialog', () => { }); it('should not display reset time when not provided', async () => { - const { lastFrame, unmount, waitUntilReady } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); const output = lastFrame() ?? ''; expect(output).not.toContain('Access resets at'); @@ -120,13 +114,12 @@ describe('EmptyWalletDialog', () => { }); it('should display slash command hints', async () => { - const { lastFrame, unmount, waitUntilReady } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); const output = lastFrame() ?? ''; expect(output).toContain('/stats'); @@ -139,14 +132,13 @@ describe('EmptyWalletDialog', () => { describe('onChoice handling', () => { it('should call onGetCredits and onChoice when get_credits is selected', async () => { // get_credits is the first item, so just press Enter - const { unmount, stdin, waitUntilReady } = await renderWithProviders( + const { unmount, stdin } = await renderWithProviders( , ); - await waitUntilReady(); writeKey(stdin, '\r'); @@ -158,13 +150,12 @@ describe('EmptyWalletDialog', () => { }); it('should call onChoice without onGetCredits when onGetCredits is not provided', async () => { - const { unmount, stdin, waitUntilReady } = await renderWithProviders( + const { unmount, stdin } = await renderWithProviders( , ); - await waitUntilReady(); writeKey(stdin, '\r'); @@ -177,14 +168,13 @@ describe('EmptyWalletDialog', () => { it('should call onChoice with use_fallback when selected', async () => { // With fallback: items are [get_credits, use_fallback, stop] // use_fallback is the second item: Down + Enter - const { unmount, stdin, waitUntilReady } = await renderWithProviders( + const { unmount, stdin } = await renderWithProviders( , ); - await waitUntilReady(); writeKey(stdin, '\x1b[B'); // Down arrow writeKey(stdin, '\r'); @@ -198,13 +188,12 @@ describe('EmptyWalletDialog', () => { it('should call onChoice with stop when selected', async () => { // Without fallback: items are [get_credits, stop] // stop is the second item: Down + Enter - const { unmount, stdin, waitUntilReady } = await renderWithProviders( + const { unmount, stdin } = await renderWithProviders( , ); - await waitUntilReady(); writeKey(stdin, '\x1b[B'); // Down arrow writeKey(stdin, '\r'); diff --git a/packages/cli/src/ui/components/ExitPlanModeDialog.test.tsx b/packages/cli/src/ui/components/ExitPlanModeDialog.test.tsx index f369e7ff8e..d6fc23dd70 100644 --- a/packages/cli/src/ui/components/ExitPlanModeDialog.test.tsx +++ b/packages/cli/src/ui/components/ExitPlanModeDialog.test.tsx @@ -440,36 +440,38 @@ Implement a comprehensive authentication system with multiple providers. return <>{children}; }; - const { stdin, lastFrame } = await renderWithProviders( - - - , - { - config: { - getTargetDir: () => mockTargetDir, - getIdeMode: () => false, - isTrustedFolder: () => true, - storage: { - getPlansDir: () => mockPlansDir, - }, - getFileSystemService: (): FileSystemService => ({ - readTextFile: vi.fn(), - writeTextFile: vi.fn(), + const { stdin, lastFrame } = await act(async () => + renderWithProviders( + + + , + { + config: { + getTargetDir: () => mockTargetDir, + getIdeMode: () => false, + isTrustedFolder: () => true, + storage: { + getPlansDir: () => mockPlansDir, + }, + getFileSystemService: (): FileSystemService => ({ + readTextFile: vi.fn(), + writeTextFile: vi.fn(), + }), + getUseAlternateBuffer: () => useAlternateBuffer ?? true, + } as unknown as import('@google/gemini-cli-core').Config, + settings: createMockSettings({ + ui: { useAlternateBuffer: useAlternateBuffer ?? true }, }), - getUseAlternateBuffer: () => useAlternateBuffer ?? true, - } as unknown as import('@google/gemini-cli-core').Config, - settings: createMockSettings({ - ui: { useAlternateBuffer: useAlternateBuffer ?? true }, - }), - }, + }, + ), ); await act(async () => { diff --git a/packages/cli/src/ui/components/ExitWarning.test.tsx b/packages/cli/src/ui/components/ExitWarning.test.tsx index 6d495a5e21..a504670d03 100644 --- a/packages/cli/src/ui/components/ExitWarning.test.tsx +++ b/packages/cli/src/ui/components/ExitWarning.test.tsx @@ -24,8 +24,7 @@ describe('ExitWarning', () => { ctrlCPressedOnce: false, ctrlDPressedOnce: false, } as unknown as UIState); - const { lastFrame, waitUntilReady, unmount } = render(); - await waitUntilReady(); + const { lastFrame, unmount } = await render(); expect(lastFrame({ allowEmpty: true })).toBe(''); unmount(); }); @@ -36,8 +35,7 @@ describe('ExitWarning', () => { ctrlCPressedOnce: true, ctrlDPressedOnce: false, } as unknown as UIState); - const { lastFrame, waitUntilReady, unmount } = render(); - await waitUntilReady(); + const { lastFrame, unmount } = await render(); expect(lastFrame()).toContain('Press Ctrl+C again to exit'); unmount(); }); @@ -48,8 +46,7 @@ describe('ExitWarning', () => { ctrlCPressedOnce: false, ctrlDPressedOnce: true, } as unknown as UIState); - const { lastFrame, waitUntilReady, unmount } = render(); - await waitUntilReady(); + const { lastFrame, unmount } = await render(); expect(lastFrame()).toContain('Press Ctrl+D again to exit'); unmount(); }); @@ -60,8 +57,7 @@ describe('ExitWarning', () => { ctrlCPressedOnce: true, ctrlDPressedOnce: true, } as unknown as UIState); - const { lastFrame, waitUntilReady, unmount } = render(); - await waitUntilReady(); + const { lastFrame, unmount } = await render(); expect(lastFrame({ allowEmpty: true })).toBe(''); unmount(); }); diff --git a/packages/cli/src/ui/components/FolderTrustDialog.test.tsx b/packages/cli/src/ui/components/FolderTrustDialog.test.tsx index c1d04b3ff9..de6e8096ec 100644 --- a/packages/cli/src/ui/components/FolderTrustDialog.test.tsx +++ b/packages/cli/src/ui/components/FolderTrustDialog.test.tsx @@ -48,10 +48,9 @@ describe('FolderTrustDialog', () => { }); it('should render the dialog with title and description', async () => { - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); expect(lastFrame()).toContain('Do you trust the files in this folder?'); expect(lastFrame()).toContain( @@ -72,7 +71,7 @@ describe('FolderTrustDialog', () => { discoveryErrors: [], securityWarnings: [], }; - const { lastFrame, unmount, waitUntilReady } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( { }, ); - await waitUntilReady(); expect(lastFrame()).toContain('This folder contains:'); expect(lastFrame()).toContain('hidden'); unmount(); @@ -103,7 +101,7 @@ describe('FolderTrustDialog', () => { discoveryErrors: [], securityWarnings: [], }; - const { lastFrame, unmount, waitUntilReady } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( { }, ); - await waitUntilReady(); // With maxHeight=4, the intro text (4 lines) will take most of the space. // The discovery results will likely be hidden. expect(lastFrame()).toContain('hidden'); @@ -135,7 +132,7 @@ describe('FolderTrustDialog', () => { discoveryErrors: [], securityWarnings: [], }; - const { lastFrame, unmount, waitUntilReady } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( { }, ); - await waitUntilReady(); expect(lastFrame()).toContain('hidden'); unmount(); }); @@ -182,9 +178,7 @@ describe('FolderTrustDialog', () => { // Initial state: truncated await waitFor(() => { expect(lastFrame()).toContain('Do you trust the files in this folder?'); - // In standard terminal mode, the expansion hint is handled globally by ToastDisplay - // via AppContainer, so it should not be present in the dialog's local frame. - expect(lastFrame()).not.toContain('Press Ctrl+O'); + expect(lastFrame()).toContain('Press Ctrl+O'); expect(lastFrame()).toContain('hidden'); }); @@ -221,7 +215,6 @@ describe('FolderTrustDialog', () => { await renderWithProviders( , ); - await waitUntilReady(); await act(async () => { stdin.write('\u001b[27u'); // Press kitty escape key @@ -246,10 +239,9 @@ describe('FolderTrustDialog', () => { }); it('should display restart message when isRestarting is true', async () => { - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); expect(lastFrame()).toContain('Gemini CLI is restarting'); unmount(); @@ -260,10 +252,9 @@ describe('FolderTrustDialog', () => { const relaunchApp = vi .spyOn(processUtils, 'relaunchApp') .mockResolvedValue(undefined); - const { waitUntilReady, unmount } = await renderWithProviders( + const { unmount } = await renderWithProviders( , ); - await waitUntilReady(); await vi.advanceTimersByTimeAsync(250); expect(relaunchApp).toHaveBeenCalled(); unmount(); @@ -275,10 +266,9 @@ describe('FolderTrustDialog', () => { const relaunchApp = vi .spyOn(processUtils, 'relaunchApp') .mockResolvedValue(undefined); - const { waitUntilReady, unmount } = await renderWithProviders( + const { unmount } = await renderWithProviders( , ); - await waitUntilReady(); // Unmount immediately (before 250ms) unmount(); @@ -292,7 +282,6 @@ describe('FolderTrustDialog', () => { const { stdin, waitUntilReady, unmount } = await renderWithProviders( , ); - await waitUntilReady(); await act(async () => { stdin.write('r'); @@ -308,30 +297,27 @@ describe('FolderTrustDialog', () => { describe('directory display', () => { it('should correctly display the folder name for a nested directory', async () => { mockedCwd.mockReturnValue('/home/user/project'); - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); expect(lastFrame()).toContain('Trust folder (project)'); unmount(); }); it('should correctly display the parent folder name for a nested directory', async () => { mockedCwd.mockReturnValue('/home/user/project'); - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); expect(lastFrame()).toContain('Trust parent folder (user)'); unmount(); }); it('should correctly display an empty parent folder name for a directory directly under root', async () => { mockedCwd.mockReturnValue('/project'); - const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); expect(lastFrame()).toContain('Trust parent folder ()'); unmount(); }); @@ -348,7 +334,7 @@ describe('FolderTrustDialog', () => { discoveryErrors: [], securityWarnings: [], }; - const { lastFrame, unmount, waitUntilReady } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( { { width: 80 }, ); - await waitUntilReady(); expect(lastFrame()).toContain('This folder contains:'); expect(lastFrame()).toContain('• Commands (2):'); expect(lastFrame()).toContain('- cmd1'); @@ -386,14 +371,13 @@ describe('FolderTrustDialog', () => { discoveryErrors: [], securityWarnings: ['Dangerous setting detected!'], }; - const { lastFrame, unmount, waitUntilReady } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); expect(lastFrame()).toContain('Security Warnings:'); expect(lastFrame()).toContain('Dangerous setting detected!'); unmount(); @@ -410,14 +394,13 @@ describe('FolderTrustDialog', () => { discoveryErrors: ['Failed to load custom commands'], securityWarnings: [], }; - const { lastFrame, unmount, waitUntilReady } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( , ); - await waitUntilReady(); expect(lastFrame()).toContain('Discovery Errors:'); expect(lastFrame()).toContain('Failed to load custom commands'); unmount(); @@ -434,7 +417,7 @@ describe('FolderTrustDialog', () => { discoveryErrors: [], securityWarnings: [], }; - const { lastFrame, unmount, waitUntilReady } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( { }, ); - await waitUntilReady(); // In alternate buffer + expanded, the title should be visible (StickyHeader) expect(lastFrame()).toContain('Do you trust the files in this folder?'); // And it should NOT use MaxSizedBox truncation @@ -470,7 +452,7 @@ describe('FolderTrustDialog', () => { securityWarnings: [`${ansiRed}warning-with-ansi${ansiReset}`], }; - const { lastFrame, unmount, waitUntilReady } = await renderWithProviders( + const { lastFrame, unmount } = await renderWithProviders( { { width: 100, uiState: { terminalHeight: 40 } }, ); - await waitUntilReady(); const output = lastFrame(); expect(output).toContain('cmd-with-ansi'); diff --git a/packages/cli/src/ui/components/Footer.test.tsx b/packages/cli/src/ui/components/Footer.test.tsx index 39f20e1c86..c0a52af868 100644 --- a/packages/cli/src/ui/components/Footer.test.tsx +++ b/packages/cli/src/ui/components/Footer.test.tsx @@ -138,33 +138,25 @@ describe('