From d7dfcf7f99af96197bcabecca49b3f8544aaf4f5 Mon Sep 17 00:00:00 2001 From: Tommaso Sciortino Date: Wed, 18 Mar 2026 16:38:56 +0000 Subject: [PATCH] refactor(cli): simplify keypress and mouse providers and update tests (#22853) --- packages/cli/src/interactiveCli.tsx | 14 +- packages/cli/src/test-utils/AppRig.tsx | 5 +- .../cli/src/test-utils/mockCommandContext.ts | 16 +- packages/cli/src/test-utils/render.tsx | 73 +--- packages/cli/src/test-utils/settings.ts | 10 +- packages/cli/src/ui/App.test.tsx | 49 ++- packages/cli/src/ui/AppContainer.test.tsx | 158 +++------ packages/cli/src/ui/AppContainer.tsx | 6 - .../cli/src/ui/IdeIntegrationNudge.test.tsx | 39 +- .../ui/components/AgentConfigDialog.test.tsx | 53 ++- .../src/ui/components/AskUserDialog.test.tsx | 23 +- .../components/EditorSettingsDialog.test.tsx | 7 +- .../ui/components/ExitPlanModeDialog.test.tsx | 18 +- .../ui/components/FolderTrustDialog.test.tsx | 33 +- .../ui/components/HistoryItemDisplay.test.tsx | 36 +- .../src/ui/components/InputPrompt.test.tsx | 11 +- .../src/ui/components/MainContent.test.tsx | 18 +- .../src/ui/components/SettingsDialog.test.tsx | 82 +++-- .../components/ToolConfirmationQueue.test.tsx | 14 +- .../components/messages/DiffRenderer.test.tsx | 79 ++++- .../messages/ShellToolMessage.test.tsx | 120 ++++--- .../messages/SubagentGroupDisplay.test.tsx | 54 +-- .../components/messages/ToolMessage.test.tsx | 17 +- .../messages/ToolMessageRawMarkdown.test.tsx | 10 +- .../ToolOverflowConsistencyChecks.test.tsx | 13 +- .../messages/ToolResultDisplay.test.tsx | 84 ++++- .../ToolResultDisplayOverflow.test.tsx | 18 +- .../shared/BaseSettingsDialog.test.tsx | 63 ++-- .../components/shared/ScrollableList.test.tsx | 333 ++++++++---------- .../components/shared/SearchableList.test.tsx | 15 +- .../views/ExtensionDetails.test.tsx | 19 +- .../views/ExtensionRegistryView.test.tsx | 45 +-- .../src/ui/contexts/KeypressContext.test.tsx | 119 +++---- .../cli/src/ui/contexts/KeypressContext.tsx | 19 +- .../cli/src/ui/contexts/MouseContext.test.tsx | 41 ++- packages/cli/src/ui/contexts/MouseContext.tsx | 14 +- packages/cli/src/ui/hooks/useFocus.test.tsx | 9 +- .../cli/src/ui/hooks/useKeypress.test.tsx | 16 +- packages/cli/src/ui/hooks/useMouse.test.ts | 20 +- .../cli/src/ui/utils/borderStyles.test.tsx | 13 +- 40 files changed, 923 insertions(+), 863 deletions(-) diff --git a/packages/cli/src/interactiveCli.tsx b/packages/cli/src/interactiveCli.tsx index a27cdbbb78..a6337ef29c 100644 --- a/packages/cli/src/interactiveCli.tsx +++ b/packages/cli/src/interactiveCli.tsx @@ -101,18 +101,8 @@ export async function startInteractiveUI( return ( - - + + diff --git a/packages/cli/src/test-utils/AppRig.tsx b/packages/cli/src/test-utils/AppRig.tsx index 6043c7f8cc..39a896a3f8 100644 --- a/packages/cli/src/test-utils/AppRig.tsx +++ b/packages/cli/src/test-utils/AppRig.tsx @@ -204,6 +204,7 @@ export class AppRig { enableEventDrivenScheduler: true, extensionLoader: new MockExtensionManager(), excludeTools: this.options.configOverrides?.excludeTools, + useAlternateBuffer: false, ...this.options.configOverrides, }; this.config = makeFakeConfig(configParams); @@ -275,6 +276,9 @@ export class AppRig { enabled: false, hasSeenNudge: true, }, + ui: { + useAlternateBuffer: false, + }, }, }); } @@ -410,7 +414,6 @@ export class AppRig { config: this.config!, settings: this.settings!, width: this.options.terminalWidth ?? 120, - useAlternateBuffer: false, uiState: { terminalHeight: this.options.terminalHeight ?? 40, }, diff --git a/packages/cli/src/test-utils/mockCommandContext.ts b/packages/cli/src/test-utils/mockCommandContext.ts index 47e56e1a44..b153aaf85e 100644 --- a/packages/cli/src/test-utils/mockCommandContext.ts +++ b/packages/cli/src/test-utils/mockCommandContext.ts @@ -37,14 +37,14 @@ export const createMockCommandContext = ( }, services: { config: null, - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + settings: { merged: defaultMergedSettings, setValue: vi.fn(), forScope: vi.fn().mockReturnValue({ settings: {} }), } as unknown as LoadedSettings, git: undefined as GitService | undefined, - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment + logger: { log: vi.fn(), logMessage: vi.fn(), @@ -53,7 +53,7 @@ export const createMockCommandContext = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any, // Cast because Logger is a class. }, - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment + ui: { addItem: vi.fn(), clear: vi.fn(), @@ -72,7 +72,7 @@ export const createMockCommandContext = ( } as any, session: { sessionShellAllowlist: new Set(), - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + stats: { sessionStartTime: new Date(), lastPromptTokenCount: 0, @@ -93,14 +93,12 @@ export const createMockCommandContext = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any const merge = (target: any, source: any): any => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const output = { ...target }; for (const key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const sourceValue = source[key]; - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const targetValue = output[key]; if ( @@ -108,11 +106,10 @@ export const createMockCommandContext = ( Object.prototype.toString.call(sourceValue) === '[object Object]' && Object.prototype.toString.call(targetValue) === '[object Object]' ) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment output[key] = merge(targetValue, sourceValue); } else { // If not, we do a direct assignment. This preserves Date objects and others. - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + output[key] = sourceValue; } } @@ -120,6 +117,5 @@ export const createMockCommandContext = ( return output; }; - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return merge(defaultMocks, overrides); }; diff --git a/packages/cli/src/test-utils/render.tsx b/packages/cli/src/test-utils/render.tsx index 74bac044c4..ede4fd6a5c 100644 --- a/packages/cli/src/test-utils/render.tsx +++ b/packages/cli/src/test-utils/render.tsx @@ -18,7 +18,7 @@ import type React from 'react'; import { act, useState } from 'react'; import os from 'node:os'; import path from 'node:path'; -import { LoadedSettings } from '../config/settings.js'; +import type { LoadedSettings } from '../config/settings.js'; import { KeypressProvider } from '../ui/contexts/KeypressContext.js'; import { SettingsContext } from '../ui/contexts/SettingsContext.js'; import { ShellFocusContext } from '../ui/contexts/ShellFocusContext.js'; @@ -416,11 +416,10 @@ export const render = ( stdout.clear(); act(() => { instance = inkRenderDirect(tree, { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion stdout: stdout as unknown as NodeJS.WriteStream, - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + stderr: stderr as unknown as NodeJS.WriteStream, - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + stdin: stdin as unknown as NodeJS.ReadStream, debug: false, exitOnCtrlC: false, @@ -499,7 +498,6 @@ const getMockConfigInternal = (): Config => { return mockConfigInternal; }; -// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const configProxy = new Proxy({} as Config, { get(_target, prop) { if (prop === 'getTargetDir') { @@ -526,21 +524,13 @@ const configProxy = new Proxy({} as Config, { } const internal = getMockConfigInternal(); if (prop in internal) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion return internal[prop as keyof typeof internal]; } throw new Error(`mockConfig does not have property ${String(prop)}`); }, }); -export const mockSettings = new LoadedSettings( - { path: '', settings: {}, originalSettings: {} }, - { path: '', settings: {}, originalSettings: {} }, - { path: '', settings: {}, originalSettings: {} }, - { path: '', settings: {}, originalSettings: {} }, - true, - [], -); +export const mockSettings = createMockSettings(); // A minimal mock UIState to satisfy the context provider. // Tests that need specific UIState values should provide their own. @@ -657,9 +647,8 @@ export const renderWithProviders = ( uiState: providedUiState, width, mouseEventsEnabled = false, - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + config = configProxy as unknown as Config, - useAlternateBuffer = true, uiActions, persistentState, appState = mockAppState, @@ -670,7 +659,6 @@ export const renderWithProviders = ( width?: number; mouseEventsEnabled?: boolean; config?: Config; - useAlternateBuffer?: boolean; uiActions?: Partial; persistentState?: { get?: typeof persistentStateMock.get; @@ -685,20 +673,17 @@ export const renderWithProviders = ( button?: 0 | 1 | 2, ) => Promise; } => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const baseState: UIState = new Proxy( { ...baseMockUiState, ...providedUiState }, { get(target, prop) { if (prop in target) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion return target[prop as keyof typeof target]; } // For properties not in the base mock or provided state, // we'll check the original proxy to see if it's a defined but // unprovided property, and if not, throw. if (prop in baseMockUiState) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion return baseMockUiState[prop as keyof typeof baseMockUiState]; } throw new Error(`mockUiState does not have property ${String(prop)}`); @@ -716,31 +701,8 @@ export const renderWithProviders = ( persistentStateMock.mockClear(); const terminalWidth = width ?? baseState.terminalWidth; - let finalSettings = settings; - if (useAlternateBuffer !== undefined) { - finalSettings = createMockSettings({ - ...settings.merged, - ui: { - ...settings.merged.ui, - useAlternateBuffer, - }, - }); - } - - // Wrap config in a Proxy so useAlternateBuffer hook (which reads from Config) gets the correct value, - // without replacing the entire config object and its other values. - let finalConfig = config; - if (useAlternateBuffer !== undefined) { - finalConfig = new Proxy(config, { - get(target, prop, receiver) { - if (prop === 'getUseAlternateBuffer') { - return () => useAlternateBuffer; - } - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return Reflect.get(target, prop, receiver); - }, - }); - } + const finalSettings = settings; + const finalConfig = config; const mainAreaWidth = terminalWidth; @@ -768,7 +730,7 @@ export const renderWithProviders = ( capturedOverflowState = undefined; capturedOverflowActions = undefined; - const renderResult = render( + const wrapWithProviders = (comp: React.ReactElement) => ( @@ -803,7 +765,7 @@ export const renderWithProviders = ( flexGrow={0} flexDirection="column" > - {component} + {comp} @@ -821,12 +783,16 @@ export const renderWithProviders = ( - , - terminalWidth, + ); + const renderResult = render(wrapWithProviders(component), terminalWidth); + return { ...renderResult, + rerender: (newComponent: React.ReactElement) => { + renderResult.rerender(wrapWithProviders(newComponent)); + }, capturedOverflowState, capturedOverflowActions, simulateClick: (col: number, row: number, button?: 0 | 1 | 2) => @@ -847,9 +813,8 @@ export function renderHook( waitUntilReady: () => Promise; generateSvg: () => string; } { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const result = { current: undefined as unknown as Result }; - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + let currentProps = options?.initialProps as Props; function TestComponent({ @@ -884,7 +849,6 @@ export function renderHook( function rerender(props?: Props) { if (arguments.length > 0) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion currentProps = props as Props; } act(() => { @@ -911,7 +875,6 @@ export function renderHookWithProviders( width?: number; mouseEventsEnabled?: boolean; config?: Config; - useAlternateBuffer?: boolean; } = {}, ): { result: { current: Result }; @@ -920,7 +883,6 @@ export function renderHookWithProviders( waitUntilReady: () => Promise; generateSvg: () => string; } { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const result = { current: undefined as unknown as Result }; let setPropsFn: ((props: Props) => void) | undefined; @@ -942,7 +904,7 @@ export function renderHookWithProviders( act(() => { renderResult = renderWithProviders( - {/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion */} + {} , options, @@ -952,7 +914,6 @@ export function renderHookWithProviders( function rerender(newProps?: Props) { act(() => { if (arguments.length > 0 && setPropsFn) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion setPropsFn(newProps as Props); } else if (forceUpdateFn) { forceUpdateFn(); diff --git a/packages/cli/src/test-utils/settings.ts b/packages/cli/src/test-utils/settings.ts index dd498b6625..ab2420849d 100644 --- a/packages/cli/src/test-utils/settings.ts +++ b/packages/cli/src/test-utils/settings.ts @@ -46,23 +46,22 @@ export const createMockSettings = ( workspace, isTrusted, errors, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + merged: mergedOverride, ...settingsOverrides } = overrides; const loaded = new LoadedSettings( - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion (system as any) || { path: '', settings: {}, originalSettings: {} }, - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + (systemDefaults as any) || { path: '', settings: {}, originalSettings: {} }, - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + (user as any) || { path: '', settings: settingsOverrides, originalSettings: settingsOverrides, }, - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + (workspace as any) || { path: '', settings: {}, originalSettings: {} }, isTrusted ?? true, errors || [], @@ -76,7 +75,6 @@ export const createMockSettings = ( // Assign any function overrides (e.g., vi.fn() for methods) for (const key in overrides) { if (typeof overrides[key] === 'function') { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment (loaded as any)[key] = overrides[key]; } } diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx index d96bfe3071..969e8b23aa 100644 --- a/packages/cli/src/ui/App.test.tsx +++ b/packages/cli/src/ui/App.test.tsx @@ -7,6 +7,7 @@ import { describe, it, expect, vi, type Mock, beforeEach } from 'vitest'; import type React from 'react'; import { renderWithProviders } from '../test-utils/render.js'; +import { createMockSettings } from '../test-utils/settings.js'; import { Text, useIsScreenReaderEnabled, type DOMElement } from 'ink'; import { App } from './App.js'; import { type UIState } from './contexts/UIStateContext.js'; @@ -97,7 +98,10 @@ describe('App', () => { , { uiState: mockUIState, - useAlternateBuffer: false, + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), }, ); await waitUntilReady(); @@ -118,7 +122,10 @@ describe('App', () => { , { uiState: quittingUIState, - useAlternateBuffer: false, + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), }, ); await waitUntilReady(); @@ -139,7 +146,10 @@ describe('App', () => { , { uiState: quittingUIState, - useAlternateBuffer: true, + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), }, ); await waitUntilReady(); @@ -159,6 +169,10 @@ describe('App', () => { , { uiState: dialogUIState, + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), }, ); await waitUntilReady(); @@ -185,6 +199,10 @@ describe('App', () => { , { uiState, + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), }, ); await waitUntilReady(); @@ -201,6 +219,10 @@ describe('App', () => { , { uiState: mockUIState, + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), }, ); await waitUntilReady(); @@ -219,6 +241,10 @@ describe('App', () => { , { uiState: mockUIState, + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), }, ); await waitUntilReady(); @@ -265,7 +291,7 @@ describe('App', () => { ], } as UIState; - const configWithExperiment = makeFakeConfig(); + const configWithExperiment = makeFakeConfig({ useAlternateBuffer: true }); vi.spyOn(configWithExperiment, 'isTrustedFolder').mockReturnValue(true); vi.spyOn(configWithExperiment, 'getIdeMode').mockReturnValue(false); @@ -274,6 +300,9 @@ describe('App', () => { { uiState: stateWithConfirmingTool, config: configWithExperiment, + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), }, ); await waitUntilReady(); @@ -293,6 +322,10 @@ describe('App', () => { , { uiState: mockUIState, + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), }, ); await waitUntilReady(); @@ -306,6 +339,10 @@ describe('App', () => { , { uiState: mockUIState, + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), }, ); await waitUntilReady(); @@ -322,6 +359,10 @@ describe('App', () => { , { uiState: dialogUIState, + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), }, ); await waitUntilReady(); diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx index 13550d3f42..26ee1a87c1 100644 --- a/packages/cli/src/ui/AppContainer.test.tsx +++ b/packages/cli/src/ui/AppContainer.test.tsx @@ -95,7 +95,8 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { }; }); import ansiEscapes from 'ansi-escapes'; -import { mergeSettings, type LoadedSettings } from '../config/settings.js'; +import { type LoadedSettings } from '../config/settings.js'; +import { createMockSettings } from '../test-utils/settings.js'; import type { InitializationResult } from '../core/initializer.js'; import { useQuotaAndFallback } from './hooks/useQuotaAndFallback.js'; import { StreamingState } from './types.js'; @@ -484,23 +485,20 @@ describe('AppContainer State Management', () => { ); // Mock LoadedSettings - const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true); - mockSettings = { + mockSettings = createMockSettings({ merged: { - ...defaultMergedSettings, hideBanner: false, hideFooter: false, hideTips: false, showMemoryUsage: false, theme: 'default', ui: { - ...defaultMergedSettings.ui, showStatusInTitle: false, hideWindowTitle: false, useAlternateBuffer: false, }, }, - } as unknown as LoadedSettings; + }); // Mock InitializationResult mockInitResult = { @@ -1008,16 +1006,14 @@ describe('AppContainer State Management', () => { describe('Settings Integration', () => { it('handles settings with all display options disabled', async () => { - const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true); - const settingsAllHidden = { + const settingsAllHidden = createMockSettings({ merged: { - ...defaultMergedSettings, hideBanner: true, hideFooter: true, hideTips: true, showMemoryUsage: false, }, - } as unknown as LoadedSettings; + }); let unmount: () => void; await act(async () => { @@ -1029,16 +1025,11 @@ describe('AppContainer State Management', () => { }); it('handles settings with memory usage enabled', async () => { - const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true); - const settingsWithMemory = { + const settingsWithMemory = createMockSettings({ merged: { - ...defaultMergedSettings, - hideBanner: false, - hideFooter: false, - hideTips: false, showMemoryUsage: true, }, - } as unknown as LoadedSettings; + }); let unmount: () => void; await act(async () => { @@ -1078,9 +1069,7 @@ describe('AppContainer State Management', () => { }); it('handles undefined settings gracefully', async () => { - const undefinedSettings = { - merged: mergeSettings({}, {}, {}, {}, true), - } as LoadedSettings; + const undefinedSettings = createMockSettings(); let unmount: () => void; await act(async () => { @@ -1498,18 +1487,14 @@ describe('AppContainer State Management', () => { it('should update terminal title with Working… when showStatusInTitle is false', () => { // Arrange: Set up mock settings with showStatusInTitle disabled - const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true); - const mockSettingsWithShowStatusFalse = { - ...mockSettings, + const mockSettingsWithShowStatusFalse = createMockSettings({ merged: { - ...defaultMergedSettings, ui: { - ...defaultMergedSettings.ui, showStatusInTitle: false, hideWindowTitle: false, }, }, - } as unknown as LoadedSettings; + }); // Mock the streaming state as Active mockedUseGeminiStream.mockReturnValue({ @@ -1537,17 +1522,14 @@ describe('AppContainer State Management', () => { it('should use legacy terminal title when dynamicWindowTitle is false', () => { // Arrange: Set up mock settings with dynamicWindowTitle disabled - const mockSettingsWithDynamicTitleFalse = { - ...mockSettings, + const mockSettingsWithDynamicTitleFalse = createMockSettings({ merged: { - ...mockSettings.merged, ui: { - ...mockSettings.merged.ui, dynamicWindowTitle: false, hideWindowTitle: false, }, }, - } as unknown as LoadedSettings; + }); // Mock the streaming state mockedUseGeminiStream.mockReturnValue({ @@ -1575,18 +1557,14 @@ describe('AppContainer State Management', () => { it('should not update terminal title when hideWindowTitle is true', () => { // Arrange: Set up mock settings with hideWindowTitle enabled - const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true); - const mockSettingsWithHideTitleTrue = { - ...mockSettings, + const mockSettingsWithHideTitleTrue = createMockSettings({ merged: { - ...defaultMergedSettings, ui: { - ...defaultMergedSettings.ui, showStatusInTitle: true, hideWindowTitle: true, }, }, - } as unknown as LoadedSettings; + }); // Act: Render the container const { unmount } = renderAppContainer({ @@ -1604,18 +1582,14 @@ describe('AppContainer State Management', () => { it('should update terminal title with thought subject when in active state', () => { // Arrange: Set up mock settings with showStatusInTitle enabled - const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true); - const mockSettingsWithTitleEnabled = { - ...mockSettings, + const mockSettingsWithTitleEnabled = createMockSettings({ merged: { - ...defaultMergedSettings, ui: { - ...defaultMergedSettings.ui, showStatusInTitle: true, hideWindowTitle: false, }, }, - } as unknown as LoadedSettings; + }); // Mock the streaming state and thought const thoughtSubject = 'Processing request'; @@ -1644,18 +1618,14 @@ describe('AppContainer State Management', () => { it('should update terminal title with default text when in Idle state and no thought subject', () => { // Arrange: Set up mock settings with showStatusInTitle enabled - const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true); - const mockSettingsWithTitleEnabled = { - ...mockSettings, + const mockSettingsWithTitleEnabled = createMockSettings({ merged: { - ...defaultMergedSettings, ui: { - ...defaultMergedSettings.ui, showStatusInTitle: true, hideWindowTitle: false, }, }, - } as unknown as LoadedSettings; + }); // Mock the streaming state as Idle with no thought mockedUseGeminiStream.mockReturnValue(DEFAULT_GEMINI_STREAM_MOCK); @@ -1679,18 +1649,14 @@ describe('AppContainer State Management', () => { it('should update terminal title when in WaitingForConfirmation state with thought subject', async () => { // Arrange: Set up mock settings with showStatusInTitle enabled - const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true); - const mockSettingsWithTitleEnabled = { - ...mockSettings, + const mockSettingsWithTitleEnabled = createMockSettings({ merged: { - ...defaultMergedSettings, ui: { - ...defaultMergedSettings.ui, showStatusInTitle: true, hideWindowTitle: false, }, }, - } as unknown as LoadedSettings; + }); // Mock the streaming state and thought const thoughtSubject = 'Confirm tool execution'; @@ -1742,17 +1708,14 @@ describe('AppContainer State Management', () => { vi.setSystemTime(startTime); // Arrange: Set up mock settings with showStatusInTitle enabled - const mockSettingsWithTitleEnabled = { - ...mockSettings, + const mockSettingsWithTitleEnabled = createMockSettings({ merged: { - ...mockSettings.merged, ui: { - ...mockSettings.merged.ui, showStatusInTitle: true, hideWindowTitle: false, }, }, - } as unknown as LoadedSettings; + }); // Mock an active shell pty but not focused mockedUseGeminiStream.mockReturnValue({ @@ -1801,17 +1764,14 @@ describe('AppContainer State Management', () => { vi.setSystemTime(startTime); // Arrange: Set up mock settings with showStatusInTitle enabled - const mockSettingsWithTitleEnabled = { - ...mockSettings, + const mockSettingsWithTitleEnabled = createMockSettings({ merged: { - ...mockSettings.merged, ui: { - ...mockSettings.merged.ui, showStatusInTitle: true, hideWindowTitle: false, }, }, - } as unknown as LoadedSettings; + }); // Mock an active shell pty with redirection active mockedUseGeminiStream.mockReturnValue({ @@ -1871,17 +1831,14 @@ describe('AppContainer State Management', () => { vi.setSystemTime(startTime); // Arrange: Set up mock settings with showStatusInTitle enabled - const mockSettingsWithTitleEnabled = { - ...mockSettings, + const mockSettingsWithTitleEnabled = createMockSettings({ merged: { - ...mockSettings.merged, ui: { - ...mockSettings.merged.ui, showStatusInTitle: true, hideWindowTitle: false, }, }, - } as unknown as LoadedSettings; + }); // Mock an active shell pty with NO output since operation started (silent) mockedUseGeminiStream.mockReturnValue({ @@ -1921,17 +1878,14 @@ describe('AppContainer State Management', () => { vi.setSystemTime(startTime); // Arrange: Set up mock settings with showStatusInTitle enabled - const mockSettingsWithTitleEnabled = { - ...mockSettings, + const mockSettingsWithTitleEnabled = createMockSettings({ merged: { - ...mockSettings.merged, ui: { - ...mockSettings.merged.ui, showStatusInTitle: true, hideWindowTitle: false, }, }, - } as unknown as LoadedSettings; + }); // Mock an active shell pty but not focused let lastOutputTime = startTime + 1000; @@ -2005,18 +1959,14 @@ describe('AppContainer State Management', () => { it('should pad title to exactly 80 characters', () => { // Arrange: Set up mock settings with showStatusInTitle enabled - const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true); - const mockSettingsWithTitleEnabled = { - ...mockSettings, + const mockSettingsWithTitleEnabled = createMockSettings({ merged: { - ...defaultMergedSettings, ui: { - ...defaultMergedSettings.ui, showStatusInTitle: true, hideWindowTitle: false, }, }, - } as unknown as LoadedSettings; + }); // Mock the streaming state and thought with a short subject const shortTitle = 'Short'; @@ -2046,18 +1996,14 @@ describe('AppContainer State Management', () => { it('should use correct ANSI escape code format', () => { // Arrange: Set up mock settings with showStatusInTitle enabled - const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true); - const mockSettingsWithTitleEnabled = { - ...mockSettings, + const mockSettingsWithTitleEnabled = createMockSettings({ merged: { - ...defaultMergedSettings, ui: { - ...defaultMergedSettings.ui, showStatusInTitle: true, hideWindowTitle: false, }, }, - } as unknown as LoadedSettings; + }); // Mock the streaming state and thought const title = 'Test Title'; @@ -2085,17 +2031,14 @@ describe('AppContainer State Management', () => { it('should use CLI_TITLE environment variable when set', () => { // Arrange: Set up mock settings with showStatusInTitle disabled (so it shows suffix) - const mockSettingsWithTitleDisabled = { - ...mockSettings, + const mockSettingsWithTitleDisabled = createMockSettings({ merged: { - ...mockSettings.merged, ui: { - ...mockSettings.merged.ui, showStatusInTitle: false, hideWindowTitle: false, }, }, - } as unknown as LoadedSettings; + }); // Mock CLI_TITLE environment variable vi.stubEnv('CLI_TITLE', 'Custom Gemini Title'); @@ -2664,17 +2607,13 @@ describe('AppContainer State Management', () => { ); // Update settings for this test run - const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true); - const testSettings = { - ...mockSettings, + const testSettings = createMockSettings({ merged: { - ...defaultMergedSettings, ui: { - ...defaultMergedSettings.ui, useAlternateBuffer: isAlternateMode, }, }, - } as unknown as LoadedSettings; + }); function TestChild() { useKeypress(childHandler || (() => {}), { @@ -3384,13 +3323,11 @@ describe('AppContainer State Management', () => { let unmount: () => void; await act(async () => { unmount = renderAppContainer({ - settings: { - ...mockSettings, + settings: createMockSettings({ merged: { - ...mockSettings.merged, - ui: { ...mockSettings.merged.ui, useAlternateBuffer: false }, + ui: { useAlternateBuffer: false }, }, - } as LoadedSettings, + }), }).unmount; }); @@ -3426,13 +3363,11 @@ describe('AppContainer State Management', () => { let unmount: () => void; await act(async () => { unmount = renderAppContainer({ - settings: { - ...mockSettings, + settings: createMockSettings({ merged: { - ...mockSettings.merged, - ui: { ...mockSettings.merged.ui, useAlternateBuffer: true }, + ui: { useAlternateBuffer: true }, }, - } as LoadedSettings, + }), }).unmount; }); @@ -3701,16 +3636,13 @@ describe('AppContainer State Management', () => { }); it('DOES set showIsExpandableHint when overflow occurs in Alternate Buffer Mode', async () => { - const alternateSettings = mergeSettings({}, {}, {}, {}, true); - const settingsWithAlternateBuffer = { + const settingsWithAlternateBuffer = createMockSettings({ merged: { - ...alternateSettings, ui: { - ...alternateSettings.ui, useAlternateBuffer: true, }, }, - } as unknown as LoadedSettings; + }); vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(true); diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index b0a936a81b..b2402f9fe9 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -1677,11 +1677,6 @@ Logging in with Google... Restarting Gemini CLI to continue. const handleGlobalKeypress = useCallback( (key: Key): boolean => { - // Debug log keystrokes if enabled - if (settings.merged.general.debugKeystrokeLogging) { - debugLogger.log('[DEBUG] Keystroke:', JSON.stringify(key)); - } - if (shortcutsHelpVisible && isHelpDismissKey(key)) { setShortcutsHelpVisible(false); } @@ -1860,7 +1855,6 @@ Logging in with Google... Restarting Gemini CLI to continue. activePtyId, handleSuspend, embeddedShellFocused, - settings.merged.general.debugKeystrokeLogging, refreshStatic, setCopyModeEnabled, tabFocusTimeoutRef, diff --git a/packages/cli/src/ui/IdeIntegrationNudge.test.tsx b/packages/cli/src/ui/IdeIntegrationNudge.test.tsx index 52d00550ea..1b30e0e0b2 100644 --- a/packages/cli/src/ui/IdeIntegrationNudge.test.tsx +++ b/packages/cli/src/ui/IdeIntegrationNudge.test.tsx @@ -5,10 +5,9 @@ */ import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'; -import { render } from '../test-utils/render.js'; +import { renderWithProviders } from '../test-utils/render.js'; import { act } from 'react'; import { IdeIntegrationNudge } from './IdeIntegrationNudge.js'; -import { KeypressProvider } from './contexts/KeypressContext.js'; import { debugLogger } from '@google/gemini-cli-core'; // Mock debugLogger @@ -54,10 +53,8 @@ describe('IdeIntegrationNudge', () => { }); it('renders correctly with default options', async () => { - const { lastFrame, waitUntilReady, unmount } = render( - - - , + const { lastFrame, waitUntilReady, unmount } = renderWithProviders( + , ); await waitUntilReady(); const frame = lastFrame(); @@ -71,10 +68,8 @@ describe('IdeIntegrationNudge', () => { it('handles "Yes" selection', async () => { const onComplete = vi.fn(); - const { stdin, waitUntilReady, unmount } = render( - - - , + const { stdin, waitUntilReady, unmount } = renderWithProviders( + , ); await waitUntilReady(); @@ -94,10 +89,8 @@ describe('IdeIntegrationNudge', () => { it('handles "No" selection', async () => { const onComplete = vi.fn(); - const { stdin, waitUntilReady, unmount } = render( - - - , + const { stdin, waitUntilReady, unmount } = renderWithProviders( + , ); await waitUntilReady(); @@ -122,10 +115,8 @@ describe('IdeIntegrationNudge', () => { it('handles "Dismiss" selection', async () => { const onComplete = vi.fn(); - const { stdin, waitUntilReady, unmount } = render( - - - , + const { stdin, waitUntilReady, unmount } = renderWithProviders( + , ); await waitUntilReady(); @@ -155,10 +146,8 @@ describe('IdeIntegrationNudge', () => { it('handles Escape key press', async () => { const onComplete = vi.fn(); - const { stdin, waitUntilReady, unmount } = render( - - - , + const { stdin, waitUntilReady, unmount } = renderWithProviders( + , ); await waitUntilReady(); @@ -184,10 +173,8 @@ describe('IdeIntegrationNudge', () => { vi.stubEnv('GEMINI_CLI_IDE_WORKSPACE_PATH', '/tmp'); const onComplete = vi.fn(); - const { lastFrame, stdin, waitUntilReady, unmount } = render( - - - , + const { lastFrame, stdin, waitUntilReady, unmount } = renderWithProviders( + , ); await waitUntilReady(); diff --git a/packages/cli/src/ui/components/AgentConfigDialog.test.tsx b/packages/cli/src/ui/components/AgentConfigDialog.test.tsx index 52cda094e0..2e5b6ecdb2 100644 --- a/packages/cli/src/ui/components/AgentConfigDialog.test.tsx +++ b/packages/cli/src/ui/components/AgentConfigDialog.test.tsx @@ -4,21 +4,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { render } from '../../test-utils/render.js'; +import { renderWithProviders } from '../../test-utils/render.js'; import { waitFor } from '../../test-utils/async.js'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { act } from 'react'; import { AgentConfigDialog } from './AgentConfigDialog.js'; import { LoadedSettings, SettingScope } from '../../config/settings.js'; -import { KeypressProvider } from '../contexts/KeypressContext.js'; import type { AgentDefinition } from '@google/gemini-cli-core'; -vi.mock('../contexts/UIStateContext.js', () => ({ - useUIState: () => ({ - mainAreaWidth: 100, - }), -})); - enum TerminalKeys { ENTER = '\u000D', TAB = '\t', @@ -122,17 +115,16 @@ describe('AgentConfigDialog', () => { settings: LoadedSettings, definition: AgentDefinition = createMockAgentDefinition(), ) => { - const result = render( - - - , + const result = renderWithProviders( + , + { settings, uiState: { mainAreaWidth: 100 } }, ); await result.waitUntilReady(); return result; @@ -331,18 +323,17 @@ describe('AgentConfigDialog', () => { const settings = createMockSettings(); // Agent config has about 6 base items + 2 per tool // Render with very small height (20) - const { lastFrame, unmount } = render( - - - , + const { lastFrame, unmount } = renderWithProviders( + , + { settings, uiState: { mainAreaWidth: 100 } }, ); await waitFor(() => expect(lastFrame()).toContain('Configure: Test Agent'), diff --git a/packages/cli/src/ui/components/AskUserDialog.test.tsx b/packages/cli/src/ui/components/AskUserDialog.test.tsx index 0469bec373..2f4f711e75 100644 --- a/packages/cli/src/ui/components/AskUserDialog.test.tsx +++ b/packages/cli/src/ui/components/AskUserDialog.test.tsx @@ -7,6 +7,8 @@ import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'; import { act } from 'react'; import { renderWithProviders } from '../../test-utils/render.js'; +import { createMockSettings } from '../../test-utils/settings.js'; +import { makeFakeConfig } from '@google/gemini-cli-core'; import { waitFor } from '../../test-utils/async.js'; import { AskUserDialog } from './AskUserDialog.js'; import { QuestionType, type Question } from '@google/gemini-cli-core'; @@ -313,7 +315,12 @@ describe('AskUserDialog', () => { width={80} availableHeight={10} // Small height to force scrolling />, - { useAlternateBuffer }, + { + config: makeFakeConfig({ useAlternateBuffer }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer } }, + }), + }, ); await waitFor(async () => { @@ -1291,7 +1298,12 @@ describe('AskUserDialog', () => { width={80} /> , - { useAlternateBuffer: false }, + { + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), + }, ); // With height 5 and alternate buffer disabled, it should show scroll arrows (▲) @@ -1327,7 +1339,12 @@ describe('AskUserDialog', () => { width={40} // Small width to force wrapping /> , - { useAlternateBuffer: true }, + { + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), + }, ); // Should NOT contain the truncation message diff --git a/packages/cli/src/ui/components/EditorSettingsDialog.test.tsx b/packages/cli/src/ui/components/EditorSettingsDialog.test.tsx index 6ebe22d982..d3b285c3a4 100644 --- a/packages/cli/src/ui/components/EditorSettingsDialog.test.tsx +++ b/packages/cli/src/ui/components/EditorSettingsDialog.test.tsx @@ -4,11 +4,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { render } from '../../test-utils/render.js'; +import { renderWithProviders } from '../../test-utils/render.js'; import { EditorSettingsDialog } from './EditorSettingsDialog.js'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { SettingScope, type LoadedSettings } from '../../config/settings.js'; -import { KeypressProvider } from '../contexts/KeypressContext.js'; import { act } from 'react'; import { waitFor } from '../../test-utils/async.js'; import { debugLogger } from '@google/gemini-cli-core'; @@ -52,8 +51,8 @@ describe('EditorSettingsDialog', () => { vi.clearAllMocks(); }); - const renderWithProvider = (ui: React.ReactNode) => - render({ui}); + const renderWithProvider = (ui: React.ReactElement) => + renderWithProviders(ui); it('renders correctly', async () => { const { lastFrame, waitUntilReady } = renderWithProvider( diff --git a/packages/cli/src/ui/components/ExitPlanModeDialog.test.tsx b/packages/cli/src/ui/components/ExitPlanModeDialog.test.tsx index 33daca1e33..272ccbdc27 100644 --- a/packages/cli/src/ui/components/ExitPlanModeDialog.test.tsx +++ b/packages/cli/src/ui/components/ExitPlanModeDialog.test.tsx @@ -7,6 +7,7 @@ import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'; import { act } from 'react'; import { renderWithProviders } from '../../test-utils/render.js'; +import { createMockSettings } from '../../test-utils/settings.js'; import { waitFor } from '../../test-utils/async.js'; import { ExitPlanModeDialog } from './ExitPlanModeDialog.js'; import { useKeypress } from '../hooks/useKeypress.js'; @@ -138,8 +139,9 @@ Implement a comprehensive authentication system with multiple providers. vi.restoreAllMocks(); }); - const renderDialog = (options?: { useAlternateBuffer?: boolean }) => - renderWithProviders( + const renderDialog = (options?: { useAlternateBuffer?: boolean }) => { + const useAlternateBuffer = options?.useAlternateBuffer ?? true; + return renderWithProviders( options?.useAlternateBuffer ?? true, + getUseAlternateBuffer: () => useAlternateBuffer, } as unknown as import('@google/gemini-cli-core').Config, + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer } }, + }), }, ); + }; describe.each([{ useAlternateBuffer: true }, { useAlternateBuffer: false }])( 'useAlternateBuffer: $useAlternateBuffer', @@ -429,7 +435,6 @@ Implement a comprehensive authentication system with multiple providers. /> , { - useAlternateBuffer, config: { getTargetDir: () => mockTargetDir, getIdeMode: () => false, @@ -443,6 +448,11 @@ Implement a comprehensive authentication system with multiple providers. }), getUseAlternateBuffer: () => useAlternateBuffer ?? true, } as unknown as import('@google/gemini-cli-core').Config, + settings: createMockSettings({ + merged: { + ui: { useAlternateBuffer: useAlternateBuffer ?? true }, + }, + }), }, ); diff --git a/packages/cli/src/ui/components/FolderTrustDialog.test.tsx b/packages/cli/src/ui/components/FolderTrustDialog.test.tsx index e68417fc55..0ff0e9b0df 100644 --- a/packages/cli/src/ui/components/FolderTrustDialog.test.tsx +++ b/packages/cli/src/ui/components/FolderTrustDialog.test.tsx @@ -5,11 +5,12 @@ */ import { renderWithProviders } from '../../test-utils/render.js'; +import { createMockSettings } from '../../test-utils/settings.js'; +import { makeFakeConfig, ExitCodes } from '@google/gemini-cli-core'; import { waitFor } from '../../test-utils/async.js'; import { act } from 'react'; import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; import { FolderTrustDialog } from './FolderTrustDialog.js'; -import { ExitCodes } from '@google/gemini-cli-core'; import * as processUtils from '../../utils/processUtils.js'; vi.mock('../../utils/processUtils.js', () => ({ @@ -78,7 +79,10 @@ describe('FolderTrustDialog', () => { />, { width: 80, - useAlternateBuffer: false, + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), uiState: { constrainHeight: true, terminalHeight: 24 }, }, ); @@ -108,7 +112,10 @@ describe('FolderTrustDialog', () => { />, { width: 80, - useAlternateBuffer: false, + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), uiState: { constrainHeight: true, terminalHeight: 14 }, }, ); @@ -139,7 +146,10 @@ describe('FolderTrustDialog', () => { />, { width: 80, - useAlternateBuffer: false, + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), uiState: { constrainHeight: true, terminalHeight: 10 }, }, ); @@ -168,7 +178,10 @@ describe('FolderTrustDialog', () => { />, { width: 80, - useAlternateBuffer: false, + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), // Initially constrained uiState: { constrainHeight: true, terminalHeight: 24 }, }, @@ -194,7 +207,10 @@ describe('FolderTrustDialog', () => { />, { width: 80, - useAlternateBuffer: false, + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), uiState: { constrainHeight: false, terminalHeight: 24 }, }, ); @@ -434,7 +450,10 @@ describe('FolderTrustDialog', () => { />, { width: 80, - useAlternateBuffer: true, + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), uiState: { constrainHeight: false, terminalHeight: 15 }, }, ); diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx index f049ffe15e..d258a8089d 100644 --- a/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx +++ b/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx @@ -16,6 +16,7 @@ import { import { ToolGroupMessage } from './messages/ToolGroupMessage.js'; import { renderWithProviders } from '../../test-utils/render.js'; import { createMockSettings } from '../../test-utils/settings.js'; +import { makeFakeConfig } from '@google/gemini-cli-core'; // Mock child components vi.mock('./messages/ToolGroupMessage.js', () => ({ @@ -84,7 +85,12 @@ describe('', () => { }; const { lastFrame, waitUntilReady, unmount } = renderWithProviders( , - { useAlternateBuffer }, + { + config: makeFakeConfig({ useAlternateBuffer }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer } }, + }), + }, ); await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); @@ -352,7 +358,12 @@ describe('', () => { terminalWidth={80} availableTerminalHeight={10} />, - { useAlternateBuffer }, + { + config: makeFakeConfig({ useAlternateBuffer }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer } }, + }), + }, ); await waitUntilReady(); @@ -374,7 +385,12 @@ describe('', () => { availableTerminalHeight={10} availableTerminalHeightGemini={Number.MAX_SAFE_INTEGER} />, - { useAlternateBuffer }, + { + config: makeFakeConfig({ useAlternateBuffer }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer } }, + }), + }, ); await waitUntilReady(); @@ -395,7 +411,12 @@ describe('', () => { terminalWidth={80} availableTerminalHeight={10} />, - { useAlternateBuffer }, + { + config: makeFakeConfig({ useAlternateBuffer }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer } }, + }), + }, ); await waitUntilReady(); @@ -417,7 +438,12 @@ describe('', () => { availableTerminalHeight={10} availableTerminalHeightGemini={Number.MAX_SAFE_INTEGER} />, - { useAlternateBuffer }, + { + config: makeFakeConfig({ useAlternateBuffer }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer } }, + }), + }, ); await waitUntilReady(); diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index c092e600b9..003f24c66b 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -6,6 +6,7 @@ import { renderWithProviders } from '../../test-utils/render.js'; import { createMockSettings } from '../../test-utils/settings.js'; +import { makeFakeConfig } from '@google/gemini-cli-core'; import { waitFor } from '../../test-utils/async.js'; import { act, useState } from 'react'; import { @@ -3512,7 +3513,10 @@ describe('InputPrompt', () => { , { mouseEventsEnabled: true, - useAlternateBuffer: true, + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), uiActions, }, ); @@ -3603,7 +3607,10 @@ describe('InputPrompt', () => { , { mouseEventsEnabled: true, - useAlternateBuffer: true, + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), uiActions, }, ); diff --git a/packages/cli/src/ui/components/MainContent.test.tsx b/packages/cli/src/ui/components/MainContent.test.tsx index e0880e624c..23218647f9 100644 --- a/packages/cli/src/ui/components/MainContent.test.tsx +++ b/packages/cli/src/ui/components/MainContent.test.tsx @@ -5,6 +5,8 @@ */ import { renderWithProviders } from '../../test-utils/render.js'; +import { createMockSettings } from '../../test-utils/settings.js'; +import { makeFakeConfig, CoreToolCallStatus } from '@google/gemini-cli-core'; import { waitFor } from '../../test-utils/async.js'; import { MainContent } from './MainContent.js'; import { getToolGroupBorderAppearance } from '../utils/borderStyles.js'; @@ -18,7 +20,6 @@ import { useUIState, type UIState, } from '../contexts/UIStateContext.js'; -import { CoreToolCallStatus } from '@google/gemini-cli-core'; import { type IndividualToolCallDisplay } from '../types.js'; // Mock dependencies @@ -482,7 +483,10 @@ describe('MainContent', () => { , { uiState: uiState as Partial, - useAlternateBuffer: true, + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), }, ); @@ -509,7 +513,10 @@ describe('MainContent', () => { , { uiState: uiState as unknown as Partial, - useAlternateBuffer: true, + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), }, ); @@ -733,7 +740,10 @@ describe('MainContent', () => { , { uiState: uiState as Partial, - useAlternateBuffer: isAlternateBuffer, + config: makeFakeConfig({ useAlternateBuffer: isAlternateBuffer }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: isAlternateBuffer } }, + }), }, ); await waitUntilReady(); diff --git a/packages/cli/src/ui/components/SettingsDialog.test.tsx b/packages/cli/src/ui/components/SettingsDialog.test.tsx index 4a2fd6a854..bc9249877c 100644 --- a/packages/cli/src/ui/components/SettingsDialog.test.tsx +++ b/packages/cli/src/ui/components/SettingsDialog.test.tsx @@ -20,16 +20,14 @@ * */ -import { render } from '../../test-utils/render.js'; +import { renderWithProviders } from '../../test-utils/render.js'; import { waitFor } from '../../test-utils/async.js'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { SettingsDialog } from './SettingsDialog.js'; import { SettingScope } from '../../config/settings.js'; import { createMockSettings } from '../../test-utils/settings.js'; -import { KeypressProvider } from '../contexts/KeypressContext.js'; import { act } from 'react'; import { TEST_ONLY } from '../../utils/settingsUtils.js'; -import { SettingsContext } from '../contexts/SettingsContext.js'; import { getSettingsSchema, type SettingDefinition, @@ -37,12 +35,6 @@ import { } from '../../config/settingsSchema.js'; import { terminalCapabilityManager } from '../utils/terminalCapabilityManager.js'; -vi.mock('../contexts/UIStateContext.js', () => ({ - useUIState: () => ({ - terminalWidth: 100, // Fixed width for consistent snapshots - }), -})); - enum TerminalKeys { ENTER = '\u000D', TAB = '\t', @@ -96,7 +88,25 @@ const ENUM_SETTING: SettingDefinition = { showInDialog: true, }; +// Minimal general schema for KeypressProvider +const MINIMAL_GENERAL_SCHEMA = { + general: { + showInDialog: false, + properties: { + debugKeystrokeLogging: { + type: 'boolean', + label: 'Debug Keystroke Logging', + category: 'General', + requiresRestart: false, + default: false, + showInDialog: false, + }, + }, + }, +}; + const ENUM_FAKE_SCHEMA: SettingsSchemaType = { + ...MINIMAL_GENERAL_SCHEMA, ui: { showInDialog: false, properties: { @@ -108,6 +118,7 @@ const ENUM_FAKE_SCHEMA: SettingsSchemaType = { } as unknown as SettingsSchemaType; const ARRAY_FAKE_SCHEMA: SettingsSchemaType = { + ...MINIMAL_GENERAL_SCHEMA, context: { type: 'object', label: 'Context', @@ -164,6 +175,7 @@ const ARRAY_FAKE_SCHEMA: SettingsSchemaType = { } as unknown as SettingsSchemaType; const TOOLS_SHELL_FAKE_SCHEMA: SettingsSchemaType = { + ...MINIMAL_GENERAL_SCHEMA, tools: { type: 'object', label: 'Tools', @@ -224,16 +236,16 @@ const renderDialog = ( availableTerminalHeight?: number; }, ) => - render( - - - - - , + renderWithProviders( + , + { + settings, + uiState: { terminalBackgroundColor: undefined }, + }, ); describe('SettingsDialog', () => { @@ -1344,17 +1356,14 @@ describe('SettingsDialog', () => { describe('String Settings Editing', () => { it('should allow editing and committing a string setting', async () => { - let settings = createMockSettings({ + const settings = createMockSettings({ 'general.sessionCleanup.maxAge': 'initial', }); const onSelect = vi.fn(); - const { stdin, unmount, rerender, waitUntilReady } = render( - - - - - , + const { stdin, unmount, waitUntilReady } = renderWithProviders( + , + { settings }, ); await waitUntilReady(); @@ -1384,20 +1393,15 @@ describe('SettingsDialog', () => { }); await waitUntilReady(); - settings = createMockSettings({ - user: { - settings: { 'general.sessionCleanup.maxAge': 'new value' }, - originalSettings: { 'general.sessionCleanup.maxAge': 'new value' }, - path: '', - }, + // Simulate the settings file being updated on disk + await act(async () => { + settings.setValue( + SettingScope.User, + 'general.sessionCleanup.maxAge', + 'new value', + ); }); - rerender( - - - - - , - ); + await waitUntilReady(); // Press Escape to exit await act(async () => { diff --git a/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx b/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx index 77d072b02e..05ec5d5591 100644 --- a/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx +++ b/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx @@ -9,6 +9,7 @@ import { Box } from 'ink'; import { ToolConfirmationQueue } from './ToolConfirmationQueue.js'; import { StreamingState } from '../types.js'; import { renderWithProviders } from '../../test-utils/render.js'; +import { createMockSettings } from '../../test-utils/settings.js'; import { waitFor } from '../../test-utils/async.js'; import { type Config, CoreToolCallStatus } from '@google/gemini-cli-core'; import type { ConfirmingToolState } from '../hooks/useConfirmingTool.js'; @@ -162,8 +163,13 @@ describe('ToolConfirmationQueue', () => { /> , { - config: mockConfig, - useAlternateBuffer: true, + config: { + ...mockConfig, + getUseAlternateBuffer: () => true, + } as unknown as Config, + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), uiState: { terminalWidth: 80, terminalHeight: 20, @@ -212,7 +218,9 @@ describe('ToolConfirmationQueue', () => { />, { config: mockConfig, - useAlternateBuffer: false, + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), uiState: { terminalWidth: 80, terminalHeight: 40, diff --git a/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx b/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx index 9063606146..5e88151715 100644 --- a/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx +++ b/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx @@ -6,6 +6,8 @@ import { OverflowProvider } from '../../contexts/OverflowContext.js'; import { renderWithProviders } from '../../../test-utils/render.js'; +import { createMockSettings } from '../../../test-utils/settings.js'; +import { makeFakeConfig } from '@google/gemini-cli-core'; import { waitFor } from '../../../test-utils/async.js'; import { DiffRenderer } from './DiffRenderer.js'; import * as CodeColorizer from '../../utils/CodeColorizer.js'; @@ -42,7 +44,12 @@ index 0000000..e69de29 terminalWidth={80} /> , - { useAlternateBuffer }, + { + config: makeFakeConfig({ useAlternateBuffer }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer } }, + }), + }, ); await waitFor(() => expect(mockColorizeCode).toHaveBeenCalledWith({ @@ -74,7 +81,12 @@ index 0000000..e69de29 terminalWidth={80} /> , - { useAlternateBuffer }, + { + config: makeFakeConfig({ useAlternateBuffer }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer } }, + }), + }, ); await waitFor(() => expect(mockColorizeCode).toHaveBeenCalledWith({ @@ -102,7 +114,12 @@ index 0000000..e69de29 , - { useAlternateBuffer }, + { + config: makeFakeConfig({ useAlternateBuffer }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer } }, + }), + }, ); await waitFor(() => expect(mockColorizeCode).toHaveBeenCalledWith({ @@ -135,7 +152,12 @@ index 0000001..0000002 100644 terminalWidth={80} /> , - { useAlternateBuffer }, + { + config: makeFakeConfig({ useAlternateBuffer }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer } }, + }), + }, ); // colorizeCode is used internally by the line-by-line rendering, not for the whole block await waitFor(() => expect(lastFrame()).toContain('new line')); @@ -166,7 +188,12 @@ index 1234567..1234567 100644 terminalWidth={80} /> , - { useAlternateBuffer }, + { + config: makeFakeConfig({ useAlternateBuffer }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer } }, + }), + }, ); await waitFor(() => expect(lastFrame()).toBeDefined()); expect(lastFrame()).toMatchSnapshot(); @@ -178,7 +205,12 @@ index 1234567..1234567 100644 , - { useAlternateBuffer }, + { + config: makeFakeConfig({ useAlternateBuffer }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer } }, + }), + }, ); await waitFor(() => expect(lastFrame()).toBeDefined()); expect(lastFrame()).toMatchSnapshot(); @@ -208,7 +240,12 @@ index 123..456 100644 terminalWidth={80} /> , - { useAlternateBuffer }, + { + config: makeFakeConfig({ useAlternateBuffer }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer } }, + }), + }, ); await waitFor(() => expect(lastFrame()).toContain('added line')); expect(lastFrame()).toMatchSnapshot(); @@ -242,7 +279,12 @@ index abc..def 100644 terminalWidth={80} /> , - { useAlternateBuffer }, + { + config: makeFakeConfig({ useAlternateBuffer }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer } }, + }), + }, ); await waitFor(() => expect(lastFrame()).toContain('context line 15')); expect(lastFrame()).toMatchSnapshot(); @@ -292,7 +334,12 @@ index 123..789 100644 availableTerminalHeight={height} /> , - { useAlternateBuffer }, + { + config: makeFakeConfig({ useAlternateBuffer }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer } }, + }), + }, ); await waitFor(() => expect(lastFrame()).toContain('anotherNew')); const output = lastFrame(); @@ -326,7 +373,12 @@ fileDiff Index: file.txt terminalWidth={80} /> , - { useAlternateBuffer }, + { + config: makeFakeConfig({ useAlternateBuffer }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer } }, + }), + }, ); await waitFor(() => expect(lastFrame()).toContain('newVar')); expect(lastFrame()).toMatchSnapshot(); @@ -353,7 +405,12 @@ fileDiff Index: Dockerfile terminalWidth={80} /> , - { useAlternateBuffer }, + { + config: makeFakeConfig({ useAlternateBuffer }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer } }, + }), + }, ); await waitFor(() => expect(lastFrame()).toContain('RUN npm run build')); expect(lastFrame()).toMatchSnapshot(); diff --git a/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx b/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx index b650ee4d9d..39fd44bcdf 100644 --- a/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx @@ -16,6 +16,8 @@ import { CoreToolCallStatus, } from '@google/gemini-cli-core'; import { renderWithProviders } from '../../../test-utils/render.js'; +import { createMockSettings } from '../../../test-utils/settings.js'; +import { makeFakeConfig } from '@google/gemini-cli-core'; import { waitFor } from '../../../test-utils/async.js'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { SHELL_COMMAND_NAME, ACTIVE_SHELL_MAX_LINES } from '../../constants.js'; @@ -48,14 +50,6 @@ describe('', () => { setEmbeddedShellFocused: mockSetEmbeddedShellFocused, }; - const renderShell = ( - props: Partial = {}, - options: Parameters[1] = {}, - ) => - renderWithProviders(, { - uiActions, - ...options, - }); beforeEach(() => { vi.clearAllMocks(); }); @@ -65,9 +59,9 @@ describe('', () => { ['SHELL_COMMAND_NAME', SHELL_COMMAND_NAME], ['SHELL_TOOL_NAME', SHELL_TOOL_NAME], ])('clicks inside the shell area sets focus for %s', async (_, name) => { - const { lastFrame, simulateClick, unmount } = renderShell( - { name }, - { mouseEventsEnabled: true }, + const { lastFrame, simulateClick, unmount } = renderWithProviders( + , + { uiActions, mouseEventsEnabled: true }, ); await waitFor(() => { @@ -152,7 +146,10 @@ describe('', () => { ptyId: 1, }, { - useAlternateBuffer: true, + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), uiState: { embeddedShellFocused: true, activePtyId: 1, @@ -166,7 +163,10 @@ describe('', () => { ptyId: 1, }, { - useAlternateBuffer: true, + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), uiState: { embeddedShellFocused: false, activePtyId: 1, @@ -174,9 +174,9 @@ describe('', () => { }, ], ])('%s', async (_, props, options) => { - const { lastFrame, waitUntilReady, unmount } = renderShell( - props, - options, + const { lastFrame, waitUntilReady, unmount } = renderWithProviders( + , + { uiActions, ...options }, ); await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); @@ -223,16 +223,21 @@ describe('', () => { focused, constrainHeight, ) => { - const { lastFrame, waitUntilReady, unmount } = renderShell( + const { lastFrame, waitUntilReady, unmount } = renderWithProviders( + , { - resultDisplay: LONG_OUTPUT, - renderOutputAsMarkdown: false, - availableTerminalHeight, - ptyId: 1, - status: CoreToolCallStatus.Executing, - }, - { - useAlternateBuffer: true, + uiActions, + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), uiState: { activePtyId: focused ? 1 : 2, embeddedShellFocused: focused, @@ -250,14 +255,21 @@ describe('', () => { ); it('fully expands in standard mode when availableTerminalHeight is undefined', async () => { - const { lastFrame, unmount } = renderShell( + const { lastFrame, unmount } = renderWithProviders( + , { - resultDisplay: LONG_OUTPUT, - renderOutputAsMarkdown: false, - availableTerminalHeight: undefined, - status: CoreToolCallStatus.Executing, + uiActions, + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), }, - { useAlternateBuffer: false }, ); await waitFor(() => { @@ -269,16 +281,21 @@ describe('', () => { }); it('fully expands in alternate buffer mode when constrainHeight is false and isExpandable is true', async () => { - const { lastFrame, waitUntilReady, unmount } = renderShell( + const { lastFrame, waitUntilReady, unmount } = renderWithProviders( + , { - resultDisplay: LONG_OUTPUT, - renderOutputAsMarkdown: false, - availableTerminalHeight: undefined, - status: CoreToolCallStatus.Success, - isExpandable: true, - }, - { - useAlternateBuffer: true, + uiActions, + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), uiState: { constrainHeight: false, }, @@ -296,16 +313,21 @@ describe('', () => { }); it('stays constrained in alternate buffer mode when isExpandable is false even if constrainHeight is false', async () => { - const { lastFrame, waitUntilReady, unmount } = renderShell( + const { lastFrame, waitUntilReady, unmount } = renderWithProviders( + , { - resultDisplay: LONG_OUTPUT, - renderOutputAsMarkdown: false, - availableTerminalHeight: undefined, - status: CoreToolCallStatus.Success, - isExpandable: false, - }, - { - useAlternateBuffer: true, + uiActions, + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), uiState: { constrainHeight: false, }, diff --git a/packages/cli/src/ui/components/messages/SubagentGroupDisplay.test.tsx b/packages/cli/src/ui/components/messages/SubagentGroupDisplay.test.tsx index 197b78e356..5af99541b5 100644 --- a/packages/cli/src/ui/components/messages/SubagentGroupDisplay.test.tsx +++ b/packages/cli/src/ui/components/messages/SubagentGroupDisplay.test.tsx @@ -4,12 +4,10 @@ * SPDX-License-Identifier: Apache-2.0 */ import { waitFor } from '../../../test-utils/async.js'; -import { render } from '../../../test-utils/render.js'; +import { renderWithProviders } from '../../../test-utils/render.js'; import { SubagentGroupDisplay } from './SubagentGroupDisplay.js'; import { Kind, CoreToolCallStatus } from '@google/gemini-cli-core'; import type { IndividualToolCallDisplay } from '../../types.js'; -import { KeypressProvider } from '../../contexts/KeypressContext.js'; -import { OverflowProvider } from '../../contexts/OverflowContext.js'; import { vi } from 'vitest'; import { Text } from 'ink'; @@ -69,36 +67,32 @@ describe('', () => { const renderSubagentGroup = ( toolCallsToRender: IndividualToolCallDisplay[], height?: number, - ) => ( - - - - - - ); + ) => + renderWithProviders( + , + ); it('renders nothing if there are no agent tool calls', async () => { - const { lastFrame } = render(renderSubagentGroup([], 40)); + const { lastFrame } = renderSubagentGroup([], 40); expect(lastFrame({ allowEmpty: true })).toBe(''); }); it('renders collapsed view by default with correct agent counts and states', async () => { - const { lastFrame, waitUntilReady } = render( - renderSubagentGroup(mockToolCalls, 40), + const { lastFrame, waitUntilReady } = renderSubagentGroup( + mockToolCalls, + 40, ); await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); }); it('expands when availableTerminalHeight is undefined', async () => { - const { lastFrame, rerender } = render( - renderSubagentGroup(mockToolCalls, 40), - ); + const { lastFrame, rerender } = renderSubagentGroup(mockToolCalls, 40); // Default collapsed view await waitFor(() => { @@ -106,13 +100,27 @@ describe('', () => { }); // Expand view - rerender(renderSubagentGroup(mockToolCalls, undefined)); + rerender( + , + ); await waitFor(() => { expect(lastFrame()).toContain('(ctrl+o to collapse)'); }); // Collapse view - rerender(renderSubagentGroup(mockToolCalls, 40)); + rerender( + , + ); await waitFor(() => { expect(lastFrame()).toContain('(ctrl+o to expand)'); }); diff --git a/packages/cli/src/ui/components/messages/ToolMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx index e3869b6e1b..c6142b2bf8 100644 --- a/packages/cli/src/ui/components/messages/ToolMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx @@ -13,8 +13,10 @@ import { type AnsiOutput, CoreToolCallStatus, Kind, + makeFakeConfig, } from '@google/gemini-cli-core'; import { renderWithProviders } from '../../../test-utils/render.js'; +import { createMockSettings } from '../../../test-utils/settings.js'; import { tryParseJSON } from '../../../utils/jsonoutput.js'; vi.mock('../GeminiRespondingSpinner.js', () => ({ @@ -462,7 +464,10 @@ describe('', () => { constrainHeight: true, }, width: 80, - useAlternateBuffer: false, + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), }, ); await waitUntilReady(); @@ -495,7 +500,10 @@ describe('', () => { uiActions, uiState: { streamingState: StreamingState.Idle }, width: 80, - useAlternateBuffer: false, + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), }, ); await waitUntilReady(); @@ -523,7 +531,10 @@ describe('', () => { uiActions, uiState: { streamingState: StreamingState.Idle }, width: 80, - useAlternateBuffer: false, + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), }, ); await waitUntilReady(); diff --git a/packages/cli/src/ui/components/messages/ToolMessageRawMarkdown.test.tsx b/packages/cli/src/ui/components/messages/ToolMessageRawMarkdown.test.tsx index 2375be7f0e..1300710ebe 100644 --- a/packages/cli/src/ui/components/messages/ToolMessageRawMarkdown.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolMessageRawMarkdown.test.tsx @@ -5,11 +5,12 @@ */ import { describe, it, expect } from 'vitest'; -import { ToolMessage, type ToolMessageProps } from './ToolMessage.js'; +import { type ToolMessageProps, ToolMessage } from './ToolMessage.js'; import { StreamingState } from '../../types.js'; import { StreamingContext } from '../../contexts/StreamingContext.js'; import { renderWithProviders } from '../../../test-utils/render.js'; -import { CoreToolCallStatus } from '@google/gemini-cli-core'; +import { createMockSettings } from '../../../test-utils/settings.js'; +import { CoreToolCallStatus, makeFakeConfig } from '@google/gemini-cli-core'; describe(' - Raw Markdown Display Snapshots', () => { const baseProps: ToolMessageProps = { @@ -72,7 +73,10 @@ describe(' - Raw Markdown Display Snapshots', () => { , { uiState: { renderMarkdown, streamingState: StreamingState.Idle }, - useAlternateBuffer, + config: makeFakeConfig({ useAlternateBuffer }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer } }, + }), }, ); await waitUntilReady(); diff --git a/packages/cli/src/ui/components/messages/ToolOverflowConsistencyChecks.test.tsx b/packages/cli/src/ui/components/messages/ToolOverflowConsistencyChecks.test.tsx index 20b8d13459..8b2da8b95e 100644 --- a/packages/cli/src/ui/components/messages/ToolOverflowConsistencyChecks.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolOverflowConsistencyChecks.test.tsx @@ -7,9 +7,10 @@ import { describe, it, expect } from 'vitest'; import { ToolGroupMessage } from './ToolGroupMessage.js'; import { renderWithProviders } from '../../../test-utils/render.js'; +import { createMockSettings } from '../../../test-utils/settings.js'; import { StreamingState, type IndividualToolCallDisplay } from '../../types.js'; import { waitFor } from '../../../test-utils/async.js'; -import { CoreToolCallStatus } from '@google/gemini-cli-core'; +import { CoreToolCallStatus, makeFakeConfig } from '@google/gemini-cli-core'; import { useOverflowState } from '../../contexts/OverflowContext.js'; describe('ToolOverflowConsistencyChecks: ToolGroupMessage and ToolResultDisplay synchronization', () => { @@ -56,7 +57,10 @@ describe('ToolOverflowConsistencyChecks: ToolGroupMessage and ToolResultDisplay streamingState: StreamingState.Idle, constrainHeight: true, }, - useAlternateBuffer: true, + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), }, ); @@ -106,7 +110,10 @@ describe('ToolOverflowConsistencyChecks: ToolGroupMessage and ToolResultDisplay streamingState: StreamingState.Idle, constrainHeight: true, }, - useAlternateBuffer: false, + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), }, ); diff --git a/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx b/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx index 02f466e72f..538a647744 100644 --- a/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx @@ -5,9 +5,10 @@ */ import { renderWithProviders } from '../../../test-utils/render.js'; +import { createMockSettings } from '../../../test-utils/settings.js'; import { ToolResultDisplay } from './ToolResultDisplay.js'; import { describe, it, expect, vi } from 'vitest'; -import type { AnsiOutput } from '@google/gemini-cli-core'; +import { makeFakeConfig, type AnsiOutput } from '@google/gemini-cli-core'; describe('ToolResultDisplay', () => { beforeEach(() => { @@ -36,7 +37,12 @@ describe('ToolResultDisplay', () => { terminalWidth={80} maxLines={10} />, - { useAlternateBuffer: true }, + { + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), + }, ); await waitUntilReady(); const output = lastFrame(); @@ -52,7 +58,12 @@ describe('ToolResultDisplay', () => { terminalWidth={80} maxLines={10} />, - { useAlternateBuffer: true }, + { + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), + }, ); await waitUntilReady(); const output = lastFrame(); @@ -69,7 +80,12 @@ describe('ToolResultDisplay', () => { terminalWidth={80} hasFocus={true} />, - { useAlternateBuffer: true }, + { + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), + }, ); await waitUntilReady(); @@ -80,7 +96,12 @@ describe('ToolResultDisplay', () => { it('renders string result as markdown by default', async () => { const { lastFrame, waitUntilReady, unmount } = renderWithProviders( , - { useAlternateBuffer: false }, + { + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), + }, ); await waitUntilReady(); const output = lastFrame(); @@ -98,7 +119,10 @@ describe('ToolResultDisplay', () => { renderOutputAsMarkdown={false} />, { - useAlternateBuffer: false, + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), uiState: { constrainHeight: true }, }, ); @@ -118,7 +142,10 @@ describe('ToolResultDisplay', () => { availableTerminalHeight={20} />, { - useAlternateBuffer: false, + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), uiState: { constrainHeight: true }, }, ); @@ -140,7 +167,12 @@ describe('ToolResultDisplay', () => { terminalWidth={80} availableTerminalHeight={20} />, - { useAlternateBuffer: false }, + { + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), + }, ); await waitUntilReady(); const output = lastFrame(); @@ -170,7 +202,12 @@ describe('ToolResultDisplay', () => { terminalWidth={80} availableTerminalHeight={20} />, - { useAlternateBuffer: false }, + { + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), + }, ); await waitUntilReady(); const output = lastFrame(); @@ -189,7 +226,12 @@ describe('ToolResultDisplay', () => { terminalWidth={80} availableTerminalHeight={20} />, - { useAlternateBuffer: false }, + { + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), + }, ); await waitUntilReady(); const output = lastFrame({ allowEmpty: true }); @@ -208,7 +250,10 @@ describe('ToolResultDisplay', () => { renderOutputAsMarkdown={true} />, { - useAlternateBuffer: false, + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), uiState: { constrainHeight: true }, }, ); @@ -226,7 +271,12 @@ describe('ToolResultDisplay', () => { availableTerminalHeight={20} renderOutputAsMarkdown={true} />, - { useAlternateBuffer: true }, + { + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), + }, ); await waitUntilReady(); const output = lastFrame(); @@ -306,7 +356,10 @@ describe('ToolResultDisplay', () => { maxLines={3} />, { - useAlternateBuffer: false, + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), uiState: { constrainHeight: true }, }, ); @@ -342,7 +395,10 @@ describe('ToolResultDisplay', () => { availableTerminalHeight={undefined} />, { - useAlternateBuffer: false, + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), uiState: { constrainHeight: true }, }, ); diff --git a/packages/cli/src/ui/components/messages/ToolResultDisplayOverflow.test.tsx b/packages/cli/src/ui/components/messages/ToolResultDisplayOverflow.test.tsx index b809e89748..3ee86cc06e 100644 --- a/packages/cli/src/ui/components/messages/ToolResultDisplayOverflow.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolResultDisplayOverflow.test.tsx @@ -5,9 +5,10 @@ */ import { renderWithProviders } from '../../../test-utils/render.js'; +import { createMockSettings } from '../../../test-utils/settings.js'; import { ToolResultDisplay } from './ToolResultDisplay.js'; import { describe, it, expect } from 'vitest'; -import { type AnsiOutput } from '@google/gemini-cli-core'; +import { makeFakeConfig, type AnsiOutput } from '@google/gemini-cli-core'; describe('ToolResultDisplay Overflow', () => { it('shows the head of the content when overflowDirection is bottom (string)', async () => { @@ -20,7 +21,10 @@ describe('ToolResultDisplay Overflow', () => { overflowDirection="bottom" />, { - useAlternateBuffer: false, + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), uiState: { constrainHeight: true }, }, ); @@ -46,7 +50,10 @@ describe('ToolResultDisplay Overflow', () => { overflowDirection="top" />, { - useAlternateBuffer: false, + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), uiState: { constrainHeight: true }, }, ); @@ -83,7 +90,10 @@ describe('ToolResultDisplay Overflow', () => { overflowDirection="bottom" />, { - useAlternateBuffer: false, + config: makeFakeConfig({ useAlternateBuffer: false }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: false } }, + }), uiState: { constrainHeight: true }, }, ); diff --git a/packages/cli/src/ui/components/shared/BaseSettingsDialog.test.tsx b/packages/cli/src/ui/components/shared/BaseSettingsDialog.test.tsx index 1ac701eff1..ebabe87133 100644 --- a/packages/cli/src/ui/components/shared/BaseSettingsDialog.test.tsx +++ b/packages/cli/src/ui/components/shared/BaseSettingsDialog.test.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { render } from '../../../test-utils/render.js'; +import { renderWithProviders } from '../../../test-utils/render.js'; import { waitFor } from '../../../test-utils/async.js'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { act } from 'react'; @@ -14,15 +14,8 @@ import { type BaseSettingsDialogProps, type SettingsDialogItem, } from './BaseSettingsDialog.js'; -import { KeypressProvider } from '../../contexts/KeypressContext.js'; import { SettingScope } from '../../../config/settings.js'; -vi.mock('../../contexts/UIStateContext.js', () => ({ - useUIState: () => ({ - mainAreaWidth: 100, - }), -})); - enum TerminalKeys { ENTER = '\u000D', TAB = '\t', @@ -115,10 +108,8 @@ describe('BaseSettingsDialog', () => { ...props, }; - const result = render( - - - , + const result = renderWithProviders( + , ); await result.waitUntilReady(); return result; @@ -331,22 +322,18 @@ describe('BaseSettingsDialog', () => { const filteredItems = [items[0], items[2], items[4]]; await act(async () => { rerender( - - - , + , ); }); - await waitUntilReady(); - // Verify the dialog hasn't crashed and the items are displayed await waitFor(() => { const frame = lastFrame(); @@ -391,22 +378,18 @@ describe('BaseSettingsDialog', () => { const filteredItems = [items[0], items[1]]; await act(async () => { rerender( - - - , + , ); }); - await waitUntilReady(); - await waitFor(() => { const frame = lastFrame(); expect(frame).toContain('Boolean Setting'); diff --git a/packages/cli/src/ui/components/shared/ScrollableList.test.tsx b/packages/cli/src/ui/components/shared/ScrollableList.test.tsx index 1dd72b89a2..2a1182a5f3 100644 --- a/packages/cli/src/ui/components/shared/ScrollableList.test.tsx +++ b/packages/cli/src/ui/components/shared/ScrollableList.test.tsx @@ -5,21 +5,12 @@ */ import { useState, useEffect, useRef, act } from 'react'; -import { render } from '../../../test-utils/render.js'; +import { renderWithProviders } from '../../../test-utils/render.js'; import { Box, Text } from 'ink'; import { ScrollableList, type ScrollableListRef } from './ScrollableList.js'; -import { ScrollProvider } from '../../contexts/ScrollProvider.js'; -import { KeypressProvider } from '../../contexts/KeypressContext.js'; -import { MouseProvider } from '../../contexts/MouseContext.js'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { waitFor } from '../../../test-utils/async.js'; -vi.mock('../../contexts/UIStateContext.js', () => ({ - useUIState: vi.fn(() => ({ - copyModeEnabled: false, - })), -})); - // Mock useStdout to provide a fixed size for testing vi.mock('ink', async (importOriginal) => { const actual = await importOriginal(); @@ -85,51 +76,45 @@ const TestComponent = ({ }, [onRef]); return ( - - - - - - ( - + + + ( + + + {item.title} - {item.title} - - - } - > - {item.title} - - {getLorem(index)} + borderStyle="single" + borderTop={true} + borderBottom={false} + borderLeft={false} + borderRight={false} + borderColor="gray" + /> - )} - estimatedItemHeight={() => 14} - keyExtractor={(item) => item.id} - hasFocus={true} - initialScrollIndex={Number.MAX_SAFE_INTEGER} - /> + } + > + {item.title} + + {getLorem(index)} - Count: {items.length} - - - - + )} + estimatedItemHeight={() => 14} + keyExtractor={(item) => item.id} + hasFocus={true} + initialScrollIndex={Number.MAX_SAFE_INTEGER} + /> + + Count: {items.length} + ); }; describe('ScrollableList Demo Behavior', () => { @@ -147,10 +132,10 @@ describe('ScrollableList Demo Behavior', () => { let lastFrame: (options?: { allowEmpty?: boolean }) => string | undefined; let waitUntilReady: () => Promise; - let result: ReturnType; + let result: ReturnType; await act(async () => { - result = render( + result = renderWithProviders( { addItem = add; @@ -230,45 +215,39 @@ describe('ScrollableList Demo Behavior', () => { }, []); return ( - - - - - ( - - {index === 0 ? ( - [STICKY] {item.title}} - > - [Normal] {item.title} - - ) : ( - [Normal] {item.title} - )} - Content for {item.title} - More content for {item.title} - - )} - estimatedItemHeight={() => 3} - keyExtractor={(item) => item.id} - hasFocus={true} - /> + + ( + + {index === 0 ? ( + [STICKY] {item.title}} + > + [Normal] {item.title} + + ) : ( + [Normal] {item.title} + )} + Content for {item.title} + More content for {item.title} - - - + )} + estimatedItemHeight={() => 3} + keyExtractor={(item) => item.id} + hasFocus={true} + /> + ); }; let lastFrame: () => string | undefined; let waitUntilReady: () => Promise; - let result: ReturnType; + let result: ReturnType; await act(async () => { - result = render(); + result = renderWithProviders(); lastFrame = result.lastFrame; waitUntilReady = result.waitUntilReady; }); @@ -334,27 +313,21 @@ describe('ScrollableList Demo Behavior', () => { title: `Item ${i}`, })); - let result: ReturnType; + let result: ReturnType; await act(async () => { - result = render( - - - - - { - listRef = ref; - }} - data={items} - renderItem={({ item }) => {item.title}} - estimatedItemHeight={() => 1} - keyExtractor={(item) => item.id} - hasFocus={true} - /> - - - - , + result = renderWithProviders( + + { + listRef = ref; + }} + data={items} + renderItem={({ item }) => {item.title}} + estimatedItemHeight={() => 1} + keyExtractor={(item) => item.id} + hasFocus={true} + /> + , ); lastFrame = result.lastFrame; stdin = result.stdin; @@ -444,25 +417,19 @@ describe('ScrollableList Demo Behavior', () => { let lastFrame: (options?: { allowEmpty?: boolean }) => string | undefined; let waitUntilReady: () => Promise; - let result: ReturnType; + let result: ReturnType; await act(async () => { - result = render( - - - - - {item.title}} - estimatedItemHeight={() => 1} - keyExtractor={(item) => item.id} - hasFocus={true} - width={50} - /> - - - - , + result = renderWithProviders( + + {item.title}} + estimatedItemHeight={() => 1} + keyExtractor={(item) => item.id} + hasFocus={true} + width={50} + /> + , ); lastFrame = result.lastFrame; waitUntilReady = result.waitUntilReady; @@ -497,31 +464,25 @@ describe('ScrollableList Demo Behavior', () => { }, []); return ( - - - - - { - listRef = ref; - }} - data={items} - renderItem={({ item }) => {item.title}} - estimatedItemHeight={() => 1} - keyExtractor={(item) => item.id} - hasFocus={true} - initialScrollIndex={Number.MAX_SAFE_INTEGER} - /> - - - - + + { + listRef = ref; + }} + data={items} + renderItem={({ item }) => {item.title}} + estimatedItemHeight={() => 1} + keyExtractor={(item) => item.id} + hasFocus={true} + initialScrollIndex={Number.MAX_SAFE_INTEGER} + /> + ); }; - let result: ReturnType; + let result: ReturnType; await act(async () => { - result = render(); + result = renderWithProviders(); }); await result!.waitUntilReady(); @@ -622,33 +583,27 @@ describe('ScrollableList Demo Behavior', () => { ); return ( - - - - - { - listRef = ref; - }} - data={items} - renderItem={({ item, index }) => ( - - )} - estimatedItemHeight={() => 1} - keyExtractor={(item) => item.id} - hasFocus={true} - initialScrollIndex={Number.MAX_SAFE_INTEGER} - /> - - - - + + { + listRef = ref; + }} + data={items} + renderItem={({ item, index }) => ( + + )} + estimatedItemHeight={() => 1} + keyExtractor={(item) => item.id} + hasFocus={true} + initialScrollIndex={Number.MAX_SAFE_INTEGER} + /> + ); }; - let result: ReturnType; + let result: ReturnType; await act(async () => { - result = render(); + result = renderWithProviders(); }); await result!.waitUntilReady(); @@ -696,35 +651,29 @@ describe('ScrollableList Demo Behavior', () => { }, []); return ( - - - - - { - listRef = ref; - }} - data={items} - renderItem={({ item }) => ( - - {item.title} - - )} - estimatedItemHeight={() => 2} - keyExtractor={(item) => item.id} - hasFocus={true} - initialScrollIndex={Number.MAX_SAFE_INTEGER} - /> + + { + listRef = ref; + }} + data={items} + renderItem={({ item }) => ( + + {item.title} - - - + )} + estimatedItemHeight={() => 2} + keyExtractor={(item) => item.id} + hasFocus={true} + initialScrollIndex={Number.MAX_SAFE_INTEGER} + /> + ); }; - let result: ReturnType; + let result: ReturnType; await act(async () => { - result = render(); + result = renderWithProviders(); }); await result!.waitUntilReady(); diff --git a/packages/cli/src/ui/components/shared/SearchableList.test.tsx b/packages/cli/src/ui/components/shared/SearchableList.test.tsx index e156c12695..127a5feef8 100644 --- a/packages/cli/src/ui/components/shared/SearchableList.test.tsx +++ b/packages/cli/src/ui/components/shared/SearchableList.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { render } from '../../../test-utils/render.js'; +import { renderWithProviders } from '../../../test-utils/render.js'; import { waitFor } from '../../../test-utils/async.js'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { @@ -14,7 +14,6 @@ import { type SearchListState, type GenericListItem, } from './SearchableList.js'; -import { KeypressProvider } from '../../contexts/KeypressContext.js'; import { useTextBuffer } from './text-buffer.js'; const useMockSearch = (props: { @@ -52,12 +51,6 @@ const useMockSearch = (props: { }; }; -vi.mock('../../contexts/UIStateContext.js', () => ({ - useUIState: () => ({ - mainAreaWidth: 100, - }), -})); - const mockItems: GenericListItem[] = [ { key: 'item-1', @@ -98,11 +91,7 @@ describe('SearchableList', () => { ...props, }; - return render( - - - , - ); + return renderWithProviders(); }; it('should render all items initially', async () => { diff --git a/packages/cli/src/ui/components/views/ExtensionDetails.test.tsx b/packages/cli/src/ui/components/views/ExtensionDetails.test.tsx index d7e4fb8ae4..d8df7012cc 100644 --- a/packages/cli/src/ui/components/views/ExtensionDetails.test.tsx +++ b/packages/cli/src/ui/components/views/ExtensionDetails.test.tsx @@ -5,11 +5,10 @@ */ import React from 'react'; -import { render } from '../../../test-utils/render.js'; +import { renderWithProviders } from '../../../test-utils/render.js'; import { waitFor } from '../../../test-utils/async.js'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { ExtensionDetails } from './ExtensionDetails.js'; -import { KeypressProvider } from '../../contexts/KeypressContext.js'; import { type RegistryExtension } from '../../../config/extensionRegistryClient.js'; const mockExtension: RegistryExtension = { @@ -43,15 +42,13 @@ describe('ExtensionDetails', () => { }); const renderDetails = (isInstalled = false) => - render( - - - , + renderWithProviders( + , ); it('should render extension details correctly', async () => { diff --git a/packages/cli/src/ui/components/views/ExtensionRegistryView.test.tsx b/packages/cli/src/ui/components/views/ExtensionRegistryView.test.tsx index b13b202b90..55e307ecfe 100644 --- a/packages/cli/src/ui/components/views/ExtensionRegistryView.test.tsx +++ b/packages/cli/src/ui/components/views/ExtensionRegistryView.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { render } from '../../../test-utils/render.js'; +import { renderWithProviders } from '../../../test-utils/render.js'; import { waitFor } from '../../../test-utils/async.js'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { ExtensionRegistryView } from './ExtensionRegistryView.js'; @@ -14,9 +14,7 @@ import { useExtensionRegistry } from '../../hooks/useExtensionRegistry.js'; import { useExtensionUpdates } from '../../hooks/useExtensionUpdates.js'; import { useRegistrySearch } from '../../hooks/useRegistrySearch.js'; import { type RegistryExtension } from '../../../config/extensionRegistryClient.js'; -import { useUIState } from '../../contexts/UIStateContext.js'; -import { useConfig } from '../../contexts/ConfigContext.js'; -import { KeypressProvider } from '../../contexts/KeypressContext.js'; +import { type UIState } from '../../contexts/UIStateContext.js'; import { type SearchListState, type GenericListItem, @@ -28,8 +26,6 @@ vi.mock('../../hooks/useExtensionRegistry.js'); vi.mock('../../hooks/useExtensionUpdates.js'); vi.mock('../../hooks/useRegistrySearch.js'); vi.mock('../../../config/extension-manager.js'); -vi.mock('../../contexts/UIStateContext.js'); -vi.mock('../../contexts/ConfigContext.js'); const mockExtensions: RegistryExtension[] = [ { @@ -123,34 +119,27 @@ describe('ExtensionRegistryView', () => { maxLabelWidth: 10, }) as unknown as SearchListState, ); - - vi.mocked(useUIState).mockReturnValue({ - mainAreaWidth: 100, - terminalHeight: 40, - staticExtraHeight: 5, - } as unknown as ReturnType); - - vi.mocked(useConfig).mockReturnValue({ - getEnableExtensionReloading: vi.fn().mockReturnValue(false), - getExtensionRegistryURI: vi - .fn() - .mockReturnValue('https://geminicli.com/extensions.json'), - } as unknown as ReturnType); }); const renderView = () => - render( - - - , + renderWithProviders( + , + { + uiState: { + staticExtraHeight: 5, + terminalHeight: 40, + } as Partial, + }, ); it('should render extensions', async () => { - const { lastFrame } = renderView(); + const { lastFrame, waitUntilReady } = renderView(); + await waitUntilReady(); + await waitFor(() => { expect(lastFrame()).toContain('Test Extension 1'); expect(lastFrame()).toContain('Test Extension 2'); diff --git a/packages/cli/src/ui/contexts/KeypressContext.test.tsx b/packages/cli/src/ui/contexts/KeypressContext.test.tsx index 31e43af575..8eb9c7c94f 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.test.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.test.tsx @@ -5,13 +5,12 @@ */ import { debugLogger } from '@google/gemini-cli-core'; -import type React from 'react'; import { act } from 'react'; -import { renderHook } from '../../test-utils/render.js'; +import { renderHookWithProviders } from '../../test-utils/render.js'; +import { createMockSettings } from '../../test-utils/settings.js'; import { waitFor } from '../../test-utils/async.js'; import { vi, afterAll, beforeAll, type Mock } from 'vitest'; import { - KeypressProvider, useKeypressContext, ESC_TIMEOUT, FAST_RETURN_TIMEOUT, @@ -52,11 +51,8 @@ class MockStdin extends EventEmitter { // Helper function to setup keypress test with standard configuration const setupKeypressTest = () => { const keyHandler = vi.fn(); - const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} - ); - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); return { result, keyHandler }; @@ -66,10 +62,6 @@ describe('KeypressContext', () => { let stdin: MockStdin; const mockSetRawMode = vi.fn(); - const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} - ); - beforeAll(() => vi.useFakeTimers()); afterAll(() => vi.useRealTimers()); @@ -269,10 +261,7 @@ describe('KeypressContext', () => { it('should handle double Escape', async () => { const keyHandler = vi.fn(); - const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} - ); - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); act(() => { @@ -306,10 +295,7 @@ describe('KeypressContext', () => { it('should handle lone Escape key (keycode 27) with timeout when kitty protocol is enabled', async () => { // Use real timers for this test to avoid issues with stream/buffer timing const keyHandler = vi.fn(); - const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} - ); - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); // Send just ESC @@ -432,7 +418,7 @@ describe('KeypressContext', () => { ])('should $name', async ({ pastedText, writeSequence }) => { const keyHandler = vi.fn(); - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); @@ -452,7 +438,7 @@ describe('KeypressContext', () => { it('should parse valid OSC 52 response', async () => { const keyHandler = vi.fn(); - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); @@ -473,7 +459,7 @@ describe('KeypressContext', () => { it('should handle split OSC 52 response', async () => { const keyHandler = vi.fn(); - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); @@ -499,7 +485,7 @@ describe('KeypressContext', () => { it('should handle OSC 52 response terminated by ESC \\', async () => { const keyHandler = vi.fn(); - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); @@ -520,7 +506,7 @@ describe('KeypressContext', () => { it('should ignore unknown OSC sequences', async () => { const keyHandler = vi.fn(); - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); @@ -537,7 +523,7 @@ describe('KeypressContext', () => { it('should ignore invalid OSC 52 format', async () => { const keyHandler = vi.fn(); - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); @@ -569,13 +555,11 @@ describe('KeypressContext', () => { it('should not log keystrokes when debugKeystrokeLogging is false', async () => { const keyHandler = vi.fn(); - const wrapper = ({ children }: { children: React.ReactNode }) => ( - - {children} - - ); - - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext(), { + settings: createMockSettings({ + general: { debugKeystrokeLogging: false }, + }), + }); act(() => result.current.subscribe(keyHandler)); @@ -593,13 +577,11 @@ describe('KeypressContext', () => { it('should log kitty buffer accumulation when debugKeystrokeLogging is true', async () => { const keyHandler = vi.fn(); - const wrapper = ({ children }: { children: React.ReactNode }) => ( - - {children} - - ); - - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext(), { + settings: createMockSettings({ + general: { debugKeystrokeLogging: true }, + }), + }); act(() => result.current.subscribe(keyHandler)); @@ -614,13 +596,11 @@ describe('KeypressContext', () => { it('should show char codes when debugKeystrokeLogging is true even without debug mode', async () => { const keyHandler = vi.fn(); - const wrapper = ({ children }: { children: React.ReactNode }) => ( - - {children} - - ); - - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext(), { + settings: createMockSettings({ + general: { debugKeystrokeLogging: true }, + }), + }); act(() => result.current.subscribe(keyHandler)); @@ -765,7 +745,7 @@ describe('KeypressContext', () => { 'should recognize sequence "$sequence" as $expected.name', ({ sequence, expected }) => { const keyHandler = vi.fn(); - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); act(() => stdin.write(sequence)); @@ -1000,12 +980,7 @@ describe('KeypressContext', () => { 'should handle Alt+$key in $terminal', ({ chunk, expected }: { chunk: string; expected: Partial }) => { const keyHandler = vi.fn(); - const testWrapper = ({ children }: { children: React.ReactNode }) => ( - {children} - ); - const { result } = renderHook(() => useKeypressContext(), { - wrapper: testWrapper, - }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); act(() => stdin.write(chunk)); @@ -1042,7 +1017,7 @@ describe('KeypressContext', () => { it('should timeout and flush incomplete kitty sequences after 50ms', async () => { const keyHandler = vi.fn(); - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); @@ -1077,7 +1052,7 @@ describe('KeypressContext', () => { it('should immediately flush non-kitty CSI sequences', async () => { const keyHandler = vi.fn(); - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); @@ -1099,7 +1074,7 @@ describe('KeypressContext', () => { it('should parse valid kitty sequences immediately when complete', async () => { const keyHandler = vi.fn(); - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); @@ -1117,7 +1092,7 @@ describe('KeypressContext', () => { it('should handle batched kitty sequences correctly', async () => { const keyHandler = vi.fn(); - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); @@ -1144,7 +1119,7 @@ describe('KeypressContext', () => { it('should handle mixed valid and invalid sequences', async () => { const keyHandler = vi.fn(); - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); @@ -1172,7 +1147,7 @@ describe('KeypressContext', () => { 'should handle sequences arriving character by character with %s ms delay', async (delay) => { const keyHandler = vi.fn(); - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); @@ -1196,7 +1171,7 @@ describe('KeypressContext', () => { it('should reset timeout when new input arrives', async () => { const keyHandler = vi.fn(); - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); @@ -1231,7 +1206,7 @@ describe('KeypressContext', () => { describe('SGR Mouse Handling', () => { it('should ignore SGR mouse sequences', async () => { const keyHandler = vi.fn(); - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); @@ -1249,7 +1224,7 @@ describe('KeypressContext', () => { it('should handle mixed SGR mouse and key sequences', async () => { const keyHandler = vi.fn(); - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); @@ -1275,7 +1250,7 @@ describe('KeypressContext', () => { it('should ignore X11 mouse sequences', async () => { const keyHandler = vi.fn(); - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); @@ -1291,7 +1266,7 @@ describe('KeypressContext', () => { it('should not flush slow SGR mouse sequences as garbage', async () => { const keyHandler = vi.fn(); - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); @@ -1311,7 +1286,7 @@ describe('KeypressContext', () => { it('should ignore specific SGR mouse sequence sandwiched between keystrokes', async () => { const keyHandler = vi.fn(); - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); @@ -1342,12 +1317,7 @@ describe('KeypressContext', () => { { name: 'another mouse', sequence: '\u001b[<0;29;19m' }, ])('should ignore $name sequence', async ({ sequence }) => { const keyHandler = vi.fn(); - const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} - ); - const { result } = renderHook(() => useKeypressContext(), { - wrapper, - }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); for (const char of sequence) { @@ -1372,10 +1342,7 @@ describe('KeypressContext', () => { it('should handle F12', async () => { const keyHandler = vi.fn(); - const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} - ); - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); act(() => { @@ -1404,7 +1371,7 @@ describe('KeypressContext', () => { 'A你B好C', // Mixed characters ])('should correctly handle string "%s"', async (inputString) => { const keyHandler = vi.fn(); - const { result } = renderHook(() => useKeypressContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useKeypressContext()); act(() => result.current.subscribe(keyHandler)); act(() => stdin.write(inputString)); diff --git a/packages/cli/src/ui/contexts/KeypressContext.tsx b/packages/cli/src/ui/contexts/KeypressContext.tsx index cdd6da7feb..3189172792 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.tsx @@ -13,6 +13,7 @@ import { useCallback, useContext, useEffect, + useMemo, useRef, } from 'react'; @@ -21,6 +22,7 @@ import { parseMouseEvent } from '../utils/mouse.js'; import { FOCUS_IN, FOCUS_OUT } from '../hooks/useFocus.js'; import { appEvents, AppEvent } from '../../utils/events.js'; import { terminalCapabilityManager } from '../utils/terminalCapabilityManager.js'; +import { useSettingsStore } from './SettingsContext.js'; export const BACKSLASH_ENTER_TIMEOUT = 5; export const ESC_TIMEOUT = 50; @@ -766,12 +768,13 @@ export function useKeypressContext() { export function KeypressProvider({ children, config, - debugKeystrokeLogging, }: { children: React.ReactNode; config?: Config; - debugKeystrokeLogging?: boolean; }) { + const { settings } = useSettingsStore(); + const debugKeystrokeLogging = settings.merged.general.debugKeystrokeLogging; + const { stdin, setRawMode } = useStdin(); const subscribersToPriority = useRef>( @@ -828,6 +831,9 @@ export function KeypressProvider({ const broadcast = useCallback( (key: Key) => { + if (debugKeystrokeLogging) { + debugLogger.log('[DEBUG] Keystroke:', JSON.stringify(key)); + } // Use cached sorted priorities to avoid sorting on every keypress for (const p of sortedPriorities.current) { const set = subscribers.get(p); @@ -842,7 +848,7 @@ export function KeypressProvider({ } } }, - [subscribers], + [subscribers, debugKeystrokeLogging], ); useEffect(() => { @@ -882,8 +888,13 @@ export function KeypressProvider({ }; }, [stdin, setRawMode, config, debugKeystrokeLogging, broadcast]); + const contextValue = useMemo( + () => ({ subscribe, unsubscribe }), + [subscribe, unsubscribe], + ); + return ( - + {children} ); diff --git a/packages/cli/src/ui/contexts/MouseContext.test.tsx b/packages/cli/src/ui/contexts/MouseContext.test.tsx index c6288ab4ef..d35c57c863 100644 --- a/packages/cli/src/ui/contexts/MouseContext.test.tsx +++ b/packages/cli/src/ui/contexts/MouseContext.test.tsx @@ -4,10 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { renderHook } from '../../test-utils/render.js'; -import type React from 'react'; +import { renderHookWithProviders } from '../../test-utils/render.js'; import { act } from 'react'; -import { MouseProvider, useMouseContext, useMouse } from './MouseContext.js'; +import { useMouseContext, useMouse } from './MouseContext.js'; import { vi, type Mock } from 'vitest'; import { useStdin } from 'ink'; import { EventEmitter } from 'node:events'; @@ -49,7 +48,6 @@ class MockStdin extends EventEmitter { describe('MouseContext', () => { let stdin: MockStdin; - let wrapper: React.FC<{ children: React.ReactNode }>; beforeEach(() => { stdin = new MockStdin(); @@ -57,9 +55,6 @@ describe('MouseContext', () => { stdin, setRawMode: vi.fn(), }); - wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} - ); vi.mocked(appEvents.emit).mockClear(); }); @@ -69,7 +64,9 @@ describe('MouseContext', () => { it('should subscribe and unsubscribe a handler', () => { const handler = vi.fn(); - const { result } = renderHook(() => useMouseContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useMouseContext(), { + mouseEventsEnabled: true, + }); act(() => { result.current.subscribe(handler); @@ -94,8 +91,8 @@ describe('MouseContext', () => { it('should not call handler if not active', () => { const handler = vi.fn(); - renderHook(() => useMouse(handler, { isActive: false }), { - wrapper, + renderHookWithProviders(() => useMouse(handler, { isActive: false }), { + mouseEventsEnabled: true, }); act(() => { @@ -106,7 +103,9 @@ describe('MouseContext', () => { }); it('should emit SelectionWarning when move event is unhandled and has coordinates', () => { - renderHook(() => useMouseContext(), { wrapper }); + renderHookWithProviders(() => useMouseContext(), { + mouseEventsEnabled: true, + }); act(() => { // Move event (32) at 10, 20 @@ -118,7 +117,9 @@ describe('MouseContext', () => { it('should not emit SelectionWarning when move event is handled', () => { const handler = vi.fn().mockReturnValue(true); - const { result } = renderHook(() => useMouseContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useMouseContext(), { + mouseEventsEnabled: true, + }); act(() => { result.current.subscribe(handler); @@ -218,7 +219,9 @@ describe('MouseContext', () => { 'should recognize sequence "$sequence" as $expected.name', ({ sequence, expected }) => { const mouseHandler = vi.fn(); - const { result } = renderHook(() => useMouseContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useMouseContext(), { + mouseEventsEnabled: true, + }); act(() => result.current.subscribe(mouseHandler)); act(() => stdin.write(sequence)); @@ -232,7 +235,9 @@ describe('MouseContext', () => { it('should emit a double-click event when two left-presses occur quickly at the same position', () => { const handler = vi.fn(); - const { result } = renderHook(() => useMouseContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useMouseContext(), { + mouseEventsEnabled: true, + }); act(() => { result.current.subscribe(handler); @@ -262,7 +267,9 @@ describe('MouseContext', () => { it('should NOT emit a double-click event if clicks are too far apart', () => { const handler = vi.fn(); - const { result } = renderHook(() => useMouseContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useMouseContext(), { + mouseEventsEnabled: true, + }); act(() => { result.current.subscribe(handler); @@ -287,7 +294,9 @@ describe('MouseContext', () => { it('should NOT emit a double-click event if too much time passes', async () => { vi.useFakeTimers(); const handler = vi.fn(); - const { result } = renderHook(() => useMouseContext(), { wrapper }); + const { result } = renderHookWithProviders(() => useMouseContext(), { + mouseEventsEnabled: true, + }); act(() => { result.current.subscribe(handler); diff --git a/packages/cli/src/ui/contexts/MouseContext.tsx b/packages/cli/src/ui/contexts/MouseContext.tsx index d36867bdbf..15ebd33ff8 100644 --- a/packages/cli/src/ui/contexts/MouseContext.tsx +++ b/packages/cli/src/ui/contexts/MouseContext.tsx @@ -11,6 +11,7 @@ import { useCallback, useContext, useEffect, + useMemo, useRef, } from 'react'; import { ESC } from '../utils/input.js'; @@ -25,6 +26,7 @@ import { DOUBLE_CLICK_THRESHOLD_MS, DOUBLE_CLICK_DISTANCE_TOLERANCE, } from '../utils/mouse.js'; +import { useSettingsStore } from './SettingsContext.js'; export type { MouseEvent, MouseEventName, MouseHandler }; @@ -61,12 +63,13 @@ export function useMouse(handler: MouseHandler, { isActive = true } = {}) { export function MouseProvider({ children, mouseEventsEnabled, - debugKeystrokeLogging, }: { children: React.ReactNode; mouseEventsEnabled?: boolean; - debugKeystrokeLogging?: boolean; }) { + const { settings } = useSettingsStore(); + const debugKeystrokeLogging = settings.merged.general.debugKeystrokeLogging; + const { stdin } = useStdin(); const subscribers = useRef>(new Set()).current; const lastClickRef = useRef<{ @@ -189,8 +192,13 @@ export function MouseProvider({ }; }, [stdin, mouseEventsEnabled, subscribers, debugKeystrokeLogging]); + const contextValue = useMemo( + () => ({ subscribe, unsubscribe }), + [subscribe, unsubscribe], + ); + return ( - + {children} ); diff --git a/packages/cli/src/ui/hooks/useFocus.test.tsx b/packages/cli/src/ui/hooks/useFocus.test.tsx index 86484cc1b9..dacac1aea6 100644 --- a/packages/cli/src/ui/hooks/useFocus.test.tsx +++ b/packages/cli/src/ui/hooks/useFocus.test.tsx @@ -4,12 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { render } from '../../test-utils/render.js'; +import { renderWithProviders } from '../../test-utils/render.js'; import { EventEmitter } from 'node:events'; import { useFocus } from './useFocus.js'; import { vi, type Mock } from 'vitest'; import { useStdin, useStdout } from 'ink'; -import { KeypressProvider } from '../contexts/KeypressContext.js'; import { act } from 'react'; // Mock the ink hooks @@ -54,11 +53,7 @@ describe('useFocus', () => { hookResult = useFocus(); return null; } - const { unmount } = render( - - - , - ); + const { unmount } = renderWithProviders(); return { result: { get current() { diff --git a/packages/cli/src/ui/hooks/useKeypress.test.tsx b/packages/cli/src/ui/hooks/useKeypress.test.tsx index 0ebfb76f8b..9a986c2c4c 100644 --- a/packages/cli/src/ui/hooks/useKeypress.test.tsx +++ b/packages/cli/src/ui/hooks/useKeypress.test.tsx @@ -5,9 +5,8 @@ */ import { act } from 'react'; -import { render } from '../../test-utils/render.js'; +import { renderHookWithProviders } from '../../test-utils/render.js'; import { useKeypress } from './useKeypress.js'; -import { KeypressProvider } from '../contexts/KeypressContext.js'; import { useStdin } from 'ink'; import { EventEmitter } from 'node:events'; import type { Mock } from 'vitest'; @@ -44,17 +43,8 @@ describe(`useKeypress`, () => { const onKeypress = vi.fn(); let originalNodeVersion: string; - const renderKeypressHook = (isActive = true) => { - function TestComponent() { - useKeypress(onKeypress, { isActive }); - return null; - } - return render( - - - , - ); - }; + const renderKeypressHook = (isActive = true) => + renderHookWithProviders(() => useKeypress(onKeypress, { isActive })); beforeEach(() => { vi.clearAllMocks(); diff --git a/packages/cli/src/ui/hooks/useMouse.test.ts b/packages/cli/src/ui/hooks/useMouse.test.ts index 2dea0ee16c..28439f6850 100644 --- a/packages/cli/src/ui/hooks/useMouse.test.ts +++ b/packages/cli/src/ui/hooks/useMouse.test.ts @@ -7,7 +7,7 @@ import { vi } from 'vitest'; import { renderHook } from '../../test-utils/render.js'; import { useMouse } from './useMouse.js'; -import { MouseProvider, useMouseContext } from '../contexts/MouseContext.js'; +import { useMouseContext } from '../contexts/MouseContext.js'; vi.mock('../contexts/MouseContext.js', async (importOriginal) => { const actual = @@ -16,10 +16,10 @@ vi.mock('../contexts/MouseContext.js', async (importOriginal) => { const unsubscribe = vi.fn(); return { ...actual, - useMouseContext: () => ({ + useMouseContext: vi.fn(() => ({ subscribe, unsubscribe, - }), + })), }; }); @@ -31,27 +31,22 @@ describe('useMouse', () => { }); it('should not subscribe when isActive is false', () => { - renderHook(() => useMouse(mockOnMouseEvent, { isActive: false }), { - wrapper: MouseProvider, - }); + renderHook(() => useMouse(mockOnMouseEvent, { isActive: false })); const { subscribe } = useMouseContext(); expect(subscribe).not.toHaveBeenCalled(); }); it('should subscribe when isActive is true', () => { - renderHook(() => useMouse(mockOnMouseEvent, { isActive: true }), { - wrapper: MouseProvider, - }); + renderHook(() => useMouse(mockOnMouseEvent, { isActive: true })); const { subscribe } = useMouseContext(); expect(subscribe).toHaveBeenCalledWith(mockOnMouseEvent); }); it('should unsubscribe on unmount', () => { - const { unmount } = renderHook( - () => useMouse(mockOnMouseEvent, { isActive: true }), - { wrapper: MouseProvider }, + const { unmount } = renderHook(() => + useMouse(mockOnMouseEvent, { isActive: true }), ); const { unsubscribe } = useMouseContext(); @@ -65,7 +60,6 @@ describe('useMouse', () => { useMouse(mockOnMouseEvent, { isActive }), { initialProps: { isActive: true }, - wrapper: MouseProvider, }, ); diff --git a/packages/cli/src/ui/utils/borderStyles.test.tsx b/packages/cli/src/ui/utils/borderStyles.test.tsx index 1852a0cb82..fa8cee693b 100644 --- a/packages/cli/src/ui/utils/borderStyles.test.tsx +++ b/packages/cli/src/ui/utils/borderStyles.test.tsx @@ -6,10 +6,11 @@ import { describe, expect, it, vi } from 'vitest'; import { getToolGroupBorderAppearance } from './borderStyles.js'; -import { CoreToolCallStatus } from '@google/gemini-cli-core'; +import { CoreToolCallStatus, makeFakeConfig } from '@google/gemini-cli-core'; import { theme } from '../semantic-colors.js'; import type { IndividualToolCallDisplay } from '../types.js'; import { renderWithProviders } from '../../test-utils/render.js'; +import { createMockSettings } from '../../test-utils/settings.js'; import { MainContent } from '../components/MainContent.js'; import { Text } from 'ink'; @@ -17,6 +18,13 @@ vi.mock('../components/CliSpinner.js', () => ({ CliSpinner: () => , })); +const altBufferOptions = { + config: makeFakeConfig({ useAlternateBuffer: true }), + settings: createMockSettings({ + merged: { ui: { useAlternateBuffer: true } }, + }), +}; + describe('getToolGroupBorderAppearance', () => { it('should use warning color for pending non-shell tools', () => { const item = { @@ -105,6 +113,7 @@ describe('getToolGroupBorderAppearance', () => { describe('MainContent tool group border SVG snapshots', () => { it('should render SVG snapshot for a pending search dialog (google_web_search)', async () => { const renderResult = renderWithProviders(, { + ...altBufferOptions, uiState: { history: [], pendingHistoryItems: [ @@ -129,6 +138,7 @@ describe('MainContent tool group border SVG snapshots', () => { it('should render SVG snapshot for an empty slice following a search tool', async () => { const renderResult = renderWithProviders(, { + ...altBufferOptions, uiState: { history: [], pendingHistoryItems: [ @@ -157,6 +167,7 @@ describe('MainContent tool group border SVG snapshots', () => { it('should render SVG snapshot for a shell tool', async () => { const renderResult = renderWithProviders(, { + ...altBufferOptions, uiState: { history: [], pendingHistoryItems: [