refactor(cli): simplify keypress and mouse providers and update tests (#22853)

This commit is contained in:
Tommaso Sciortino
2026-03-18 16:38:56 +00:00
committed by GitHub
parent 81a97e78f1
commit d7dfcf7f99
40 changed files with 923 additions and 863 deletions

View File

@@ -101,18 +101,8 @@ export async function startInteractiveUI(
return (
<SettingsContext.Provider value={settings}>
<KeyMatchersProvider value={matchers}>
<KeypressProvider
config={config}
debugKeystrokeLogging={
settings.merged.general.debugKeystrokeLogging
}
>
<MouseProvider
mouseEventsEnabled={mouseEventsEnabled}
debugKeystrokeLogging={
settings.merged.general.debugKeystrokeLogging
}
>
<KeypressProvider config={config}>
<MouseProvider mouseEventsEnabled={mouseEventsEnabled}>
<TerminalProvider>
<ScrollProvider>
<OverflowProvider>

View File

@@ -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,
},

View File

@@ -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<string>(),
// 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);
};

View File

@@ -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<UIActions>;
persistentState?: {
get?: typeof persistentStateMock.get;
@@ -685,20 +673,17 @@ export const renderWithProviders = (
button?: 0 | 1 | 2,
) => Promise<void>;
} => {
// 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) => (
<AppContext.Provider value={appState}>
<ConfigContext.Provider value={finalConfig}>
<SettingsContext.Provider value={finalSettings}>
@@ -803,7 +765,7 @@ export const renderWithProviders = (
flexGrow={0}
flexDirection="column"
>
{component}
{comp}
</Box>
</ContextCapture>
</ScrollProvider>
@@ -821,12 +783,16 @@ export const renderWithProviders = (
</UIStateContext.Provider>
</SettingsContext.Provider>
</ConfigContext.Provider>
</AppContext.Provider>,
terminalWidth,
</AppContext.Provider>
);
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<Result, Props>(
waitUntilReady: () => Promise<void>;
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<Result, Props>(
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<Result, Props>(
width?: number;
mouseEventsEnabled?: boolean;
config?: Config;
useAlternateBuffer?: boolean;
} = {},
): {
result: { current: Result };
@@ -920,7 +883,6 @@ export function renderHookWithProviders<Result, Props>(
waitUntilReady: () => Promise<void>;
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<Result, Props>(
act(() => {
renderResult = renderWithProviders(
<Wrapper>
{/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion */}
{}
<TestComponent initialProps={options.initialProps as Props} />
</Wrapper>,
options,
@@ -952,7 +914,6 @@ export function renderHookWithProviders<Result, Props>(
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();

View File

@@ -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];
}
}

View File

@@ -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', () => {
<App />,
{
uiState: mockUIState,
useAlternateBuffer: false,
config: makeFakeConfig({ useAlternateBuffer: false }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer: false } },
}),
},
);
await waitUntilReady();
@@ -118,7 +122,10 @@ describe('App', () => {
<App />,
{
uiState: quittingUIState,
useAlternateBuffer: false,
config: makeFakeConfig({ useAlternateBuffer: false }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer: false } },
}),
},
);
await waitUntilReady();
@@ -139,7 +146,10 @@ describe('App', () => {
<App />,
{
uiState: quittingUIState,
useAlternateBuffer: true,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer: true } },
}),
},
);
await waitUntilReady();
@@ -159,6 +169,10 @@ describe('App', () => {
<App />,
{
uiState: dialogUIState,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer: true } },
}),
},
);
await waitUntilReady();
@@ -185,6 +199,10 @@ describe('App', () => {
<App />,
{
uiState,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer: true } },
}),
},
);
await waitUntilReady();
@@ -201,6 +219,10 @@ describe('App', () => {
<App />,
{
uiState: mockUIState,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer: true } },
}),
},
);
await waitUntilReady();
@@ -219,6 +241,10 @@ describe('App', () => {
<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', () => {
<App />,
{
uiState: mockUIState,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer: true } },
}),
},
);
await waitUntilReady();
@@ -306,6 +339,10 @@ describe('App', () => {
<App />,
{
uiState: mockUIState,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer: true } },
}),
},
);
await waitUntilReady();
@@ -322,6 +359,10 @@ describe('App', () => {
<App />,
{
uiState: dialogUIState,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer: true } },
}),
},
);
await waitUntilReady();

View File

@@ -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);

View File

@@ -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,

View File

@@ -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(
<KeypressProvider>
<IdeIntegrationNudge {...defaultProps} />
</KeypressProvider>,
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<IdeIntegrationNudge {...defaultProps} />,
);
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(
<KeypressProvider>
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />
</KeypressProvider>,
const { stdin, waitUntilReady, unmount } = renderWithProviders(
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
);
await waitUntilReady();
@@ -94,10 +89,8 @@ describe('IdeIntegrationNudge', () => {
it('handles "No" selection', async () => {
const onComplete = vi.fn();
const { stdin, waitUntilReady, unmount } = render(
<KeypressProvider>
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />
</KeypressProvider>,
const { stdin, waitUntilReady, unmount } = renderWithProviders(
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
);
await waitUntilReady();
@@ -122,10 +115,8 @@ describe('IdeIntegrationNudge', () => {
it('handles "Dismiss" selection', async () => {
const onComplete = vi.fn();
const { stdin, waitUntilReady, unmount } = render(
<KeypressProvider>
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />
</KeypressProvider>,
const { stdin, waitUntilReady, unmount } = renderWithProviders(
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
);
await waitUntilReady();
@@ -155,10 +146,8 @@ describe('IdeIntegrationNudge', () => {
it('handles Escape key press', async () => {
const onComplete = vi.fn();
const { stdin, waitUntilReady, unmount } = render(
<KeypressProvider>
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />
</KeypressProvider>,
const { stdin, waitUntilReady, unmount } = renderWithProviders(
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
);
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(
<KeypressProvider>
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />
</KeypressProvider>,
const { lastFrame, stdin, waitUntilReady, unmount } = renderWithProviders(
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
);
await waitUntilReady();

View File

@@ -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(
<KeypressProvider>
<AgentConfigDialog
agentName="test-agent"
displayName="Test Agent"
definition={definition}
settings={settings}
onClose={mockOnClose}
onSave={mockOnSave}
/>
</KeypressProvider>,
const result = renderWithProviders(
<AgentConfigDialog
agentName="test-agent"
displayName="Test Agent"
definition={definition}
settings={settings}
onClose={mockOnClose}
onSave={mockOnSave}
/>,
{ 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(
<KeypressProvider>
<AgentConfigDialog
agentName="test-agent"
displayName="Test Agent"
definition={createMockAgentDefinition()}
settings={settings}
onClose={mockOnClose}
onSave={mockOnSave}
availableTerminalHeight={20}
/>
</KeypressProvider>,
const { lastFrame, unmount } = renderWithProviders(
<AgentConfigDialog
agentName="test-agent"
displayName="Test Agent"
definition={createMockAgentDefinition()}
settings={settings}
onClose={mockOnClose}
onSave={mockOnSave}
availableTerminalHeight={20}
/>,
{ settings, uiState: { mainAreaWidth: 100 } },
);
await waitFor(() =>
expect(lastFrame()).toContain('Configure: Test Agent'),

View File

@@ -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}
/>
</UIStateContext.Provider>,
{ 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
/>
</UIStateContext.Provider>,
{ useAlternateBuffer: true },
{
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer: true } },
}),
},
);
// Should NOT contain the truncation message

View File

@@ -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(<KeypressProvider>{ui}</KeypressProvider>);
const renderWithProvider = (ui: React.ReactElement) =>
renderWithProviders(ui);
it('renders correctly', async () => {
const { lastFrame, waitUntilReady } = renderWithProvider(

View File

@@ -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(
<ExitPlanModeDialog
planPath={mockPlanFullPath}
onApprove={onApprove}
@@ -163,10 +165,14 @@ Implement a comprehensive authentication system with multiple providers.
readTextFile: vi.fn(),
writeTextFile: vi.fn(),
}),
getUseAlternateBuffer: () => 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.
/>
</BubbleListener>,
{
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 },
},
}),
},
);

View File

@@ -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 },
},
);

View File

@@ -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('<HistoryItemDisplay />', () => {
};
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<HistoryItemDisplay {...baseItem} item={item} />,
{ useAlternateBuffer },
{
config: makeFakeConfig({ useAlternateBuffer }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer } },
}),
},
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
@@ -352,7 +358,12 @@ describe('<HistoryItemDisplay />', () => {
terminalWidth={80}
availableTerminalHeight={10}
/>,
{ useAlternateBuffer },
{
config: makeFakeConfig({ useAlternateBuffer }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer } },
}),
},
);
await waitUntilReady();
@@ -374,7 +385,12 @@ describe('<HistoryItemDisplay />', () => {
availableTerminalHeight={10}
availableTerminalHeightGemini={Number.MAX_SAFE_INTEGER}
/>,
{ useAlternateBuffer },
{
config: makeFakeConfig({ useAlternateBuffer }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer } },
}),
},
);
await waitUntilReady();
@@ -395,7 +411,12 @@ describe('<HistoryItemDisplay />', () => {
terminalWidth={80}
availableTerminalHeight={10}
/>,
{ useAlternateBuffer },
{
config: makeFakeConfig({ useAlternateBuffer }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer } },
}),
},
);
await waitUntilReady();
@@ -417,7 +438,12 @@ describe('<HistoryItemDisplay />', () => {
availableTerminalHeight={10}
availableTerminalHeightGemini={Number.MAX_SAFE_INTEGER}
/>,
{ useAlternateBuffer },
{
config: makeFakeConfig({ useAlternateBuffer }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer } },
}),
},
);
await waitUntilReady();

View File

@@ -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', () => {
<TestWrapper />,
{
mouseEventsEnabled: true,
useAlternateBuffer: true,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer: true } },
}),
uiActions,
},
);
@@ -3603,7 +3607,10 @@ describe('InputPrompt', () => {
<TestWrapper />,
{
mouseEventsEnabled: true,
useAlternateBuffer: true,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer: true } },
}),
uiActions,
},
);

View File

@@ -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', () => {
<MainContent />,
{
uiState: uiState as Partial<UIState>,
useAlternateBuffer: true,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer: true } },
}),
},
);
@@ -509,7 +513,10 @@ describe('MainContent', () => {
<MainContent />,
{
uiState: uiState as unknown as Partial<UIState>,
useAlternateBuffer: true,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer: true } },
}),
},
);
@@ -733,7 +740,10 @@ describe('MainContent', () => {
<MainContent />,
{
uiState: uiState as Partial<UIState>,
useAlternateBuffer: isAlternateBuffer,
config: makeFakeConfig({ useAlternateBuffer: isAlternateBuffer }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer: isAlternateBuffer } },
}),
},
);
await waitUntilReady();

View File

@@ -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(
<SettingsContext.Provider value={settings}>
<KeypressProvider>
<SettingsDialog
onSelect={onSelect}
onRestartRequest={options?.onRestartRequest}
availableTerminalHeight={options?.availableTerminalHeight}
/>
</KeypressProvider>
</SettingsContext.Provider>,
renderWithProviders(
<SettingsDialog
onSelect={onSelect}
onRestartRequest={options?.onRestartRequest}
availableTerminalHeight={options?.availableTerminalHeight}
/>,
{
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(
<SettingsContext.Provider value={settings}>
<KeypressProvider>
<SettingsDialog onSelect={onSelect} />
</KeypressProvider>
</SettingsContext.Provider>,
const { stdin, unmount, waitUntilReady } = renderWithProviders(
<SettingsDialog onSelect={onSelect} />,
{ 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(
<SettingsContext.Provider value={settings}>
<KeypressProvider>
<SettingsDialog onSelect={onSelect} />
</KeypressProvider>
</SettingsContext.Provider>,
);
await waitUntilReady();
// Press Escape to exit
await act(async () => {

View File

@@ -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', () => {
/>
</Box>,
{
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,

View File

@@ -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}
/>
</OverflowProvider>,
{ useAlternateBuffer },
{
config: makeFakeConfig({ useAlternateBuffer }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer } },
}),
},
);
await waitFor(() =>
expect(mockColorizeCode).toHaveBeenCalledWith({
@@ -74,7 +81,12 @@ index 0000000..e69de29
terminalWidth={80}
/>
</OverflowProvider>,
{ useAlternateBuffer },
{
config: makeFakeConfig({ useAlternateBuffer }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer } },
}),
},
);
await waitFor(() =>
expect(mockColorizeCode).toHaveBeenCalledWith({
@@ -102,7 +114,12 @@ index 0000000..e69de29
<OverflowProvider>
<DiffRenderer diffContent={newFileDiffContent} terminalWidth={80} />
</OverflowProvider>,
{ useAlternateBuffer },
{
config: makeFakeConfig({ useAlternateBuffer }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer } },
}),
},
);
await waitFor(() =>
expect(mockColorizeCode).toHaveBeenCalledWith({
@@ -135,7 +152,12 @@ index 0000001..0000002 100644
terminalWidth={80}
/>
</OverflowProvider>,
{ 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}
/>
</OverflowProvider>,
{ 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
<OverflowProvider>
<DiffRenderer diffContent="" terminalWidth={80} />
</OverflowProvider>,
{ 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}
/>
</OverflowProvider>,
{ 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}
/>
</OverflowProvider>,
{ 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}
/>
</OverflowProvider>,
{ 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}
/>
</OverflowProvider>,
{ 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}
/>
</OverflowProvider>,
{ useAlternateBuffer },
{
config: makeFakeConfig({ useAlternateBuffer }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer } },
}),
},
);
await waitFor(() => expect(lastFrame()).toContain('RUN npm run build'));
expect(lastFrame()).toMatchSnapshot();

View File

@@ -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('<ShellToolMessage />', () => {
setEmbeddedShellFocused: mockSetEmbeddedShellFocused,
};
const renderShell = (
props: Partial<ShellToolMessageProps> = {},
options: Parameters<typeof renderWithProviders>[1] = {},
) =>
renderWithProviders(<ShellToolMessage {...baseProps} {...props} />, {
uiActions,
...options,
});
beforeEach(() => {
vi.clearAllMocks();
});
@@ -65,9 +59,9 @@ describe('<ShellToolMessage />', () => {
['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(
<ShellToolMessage {...baseProps} name={name} />,
{ uiActions, mouseEventsEnabled: true },
);
await waitFor(() => {
@@ -152,7 +146,10 @@ describe('<ShellToolMessage />', () => {
ptyId: 1,
},
{
useAlternateBuffer: true,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer: true } },
}),
uiState: {
embeddedShellFocused: true,
activePtyId: 1,
@@ -166,7 +163,10 @@ describe('<ShellToolMessage />', () => {
ptyId: 1,
},
{
useAlternateBuffer: true,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer: true } },
}),
uiState: {
embeddedShellFocused: false,
activePtyId: 1,
@@ -174,9 +174,9 @@ describe('<ShellToolMessage />', () => {
},
],
])('%s', async (_, props, options) => {
const { lastFrame, waitUntilReady, unmount } = renderShell(
props,
options,
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<ShellToolMessage {...baseProps} {...props} />,
{ uiActions, ...options },
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
@@ -223,16 +223,21 @@ describe('<ShellToolMessage />', () => {
focused,
constrainHeight,
) => {
const { lastFrame, waitUntilReady, unmount } = renderShell(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<ShellToolMessage
{...baseProps}
resultDisplay={LONG_OUTPUT}
renderOutputAsMarkdown={false}
availableTerminalHeight={availableTerminalHeight}
ptyId={1}
status={CoreToolCallStatus.Executing}
/>,
{
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('<ShellToolMessage />', () => {
);
it('fully expands in standard mode when availableTerminalHeight is undefined', async () => {
const { lastFrame, unmount } = renderShell(
const { lastFrame, unmount } = renderWithProviders(
<ShellToolMessage
{...baseProps}
resultDisplay={LONG_OUTPUT}
renderOutputAsMarkdown={false}
availableTerminalHeight={undefined}
status={CoreToolCallStatus.Executing}
/>,
{
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('<ShellToolMessage />', () => {
});
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(
<ShellToolMessage
{...baseProps}
resultDisplay={LONG_OUTPUT}
renderOutputAsMarkdown={false}
availableTerminalHeight={undefined}
status={CoreToolCallStatus.Success}
isExpandable={true}
/>,
{
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('<ShellToolMessage />', () => {
});
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(
<ShellToolMessage
{...baseProps}
resultDisplay={LONG_OUTPUT}
renderOutputAsMarkdown={false}
availableTerminalHeight={undefined}
status={CoreToolCallStatus.Success}
isExpandable={false}
/>,
{
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,
},

View File

@@ -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('<SubagentGroupDisplay />', () => {
const renderSubagentGroup = (
toolCallsToRender: IndividualToolCallDisplay[],
height?: number,
) => (
<OverflowProvider>
<KeypressProvider>
<SubagentGroupDisplay
toolCalls={toolCallsToRender}
terminalWidth={80}
availableTerminalHeight={height}
isExpandable={true}
/>
</KeypressProvider>
</OverflowProvider>
);
) =>
renderWithProviders(
<SubagentGroupDisplay
toolCalls={toolCallsToRender}
terminalWidth={80}
availableTerminalHeight={height}
isExpandable={true}
/>,
);
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('<SubagentGroupDisplay />', () => {
});
// Expand view
rerender(renderSubagentGroup(mockToolCalls, undefined));
rerender(
<SubagentGroupDisplay
toolCalls={mockToolCalls}
terminalWidth={80}
availableTerminalHeight={undefined}
isExpandable={true}
/>,
);
await waitFor(() => {
expect(lastFrame()).toContain('(ctrl+o to collapse)');
});
// Collapse view
rerender(renderSubagentGroup(mockToolCalls, 40));
rerender(
<SubagentGroupDisplay
toolCalls={mockToolCalls}
terminalWidth={80}
availableTerminalHeight={40}
isExpandable={true}
/>,
);
await waitFor(() => {
expect(lastFrame()).toContain('(ctrl+o to expand)');
});

View File

@@ -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('<ToolMessage />', () => {
constrainHeight: true,
},
width: 80,
useAlternateBuffer: false,
config: makeFakeConfig({ useAlternateBuffer: false }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer: false } },
}),
},
);
await waitUntilReady();
@@ -495,7 +500,10 @@ describe('<ToolMessage />', () => {
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('<ToolMessage />', () => {
uiActions,
uiState: { streamingState: StreamingState.Idle },
width: 80,
useAlternateBuffer: false,
config: makeFakeConfig({ useAlternateBuffer: false }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer: false } },
}),
},
);
await waitUntilReady();

View File

@@ -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('<ToolMessage /> - Raw Markdown Display Snapshots', () => {
const baseProps: ToolMessageProps = {
@@ -72,7 +73,10 @@ describe('<ToolMessage /> - Raw Markdown Display Snapshots', () => {
</StreamingContext.Provider>,
{
uiState: { renderMarkdown, streamingState: StreamingState.Idle },
useAlternateBuffer,
config: makeFakeConfig({ useAlternateBuffer }),
settings: createMockSettings({
merged: { ui: { useAlternateBuffer } },
}),
},
);
await waitUntilReady();

View File

@@ -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 } },
}),
},
);

View File

@@ -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(
<ToolResultDisplay resultDisplay="**Some result**" terminalWidth={80} />,
{ 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 },
},
);

View File

@@ -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 },
},
);

View File

@@ -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(
<KeypressProvider>
<BaseSettingsDialog {...defaultProps} />
</KeypressProvider>,
const result = renderWithProviders(
<BaseSettingsDialog {...defaultProps} />,
);
await result.waitUntilReady();
return result;
@@ -331,22 +322,18 @@ describe('BaseSettingsDialog', () => {
const filteredItems = [items[0], items[2], items[4]];
await act(async () => {
rerender(
<KeypressProvider>
<BaseSettingsDialog
title="Test Settings"
items={filteredItems}
selectedScope={SettingScope.User}
maxItemsToShow={5}
onItemToggle={mockOnItemToggle}
onEditCommit={mockOnEditCommit}
onItemClear={mockOnItemClear}
onClose={mockOnClose}
/>
</KeypressProvider>,
<BaseSettingsDialog
title="Test Settings"
items={filteredItems}
selectedScope={SettingScope.User}
maxItemsToShow={5}
onItemToggle={mockOnItemToggle}
onEditCommit={mockOnEditCommit}
onItemClear={mockOnItemClear}
onClose={mockOnClose}
/>,
);
});
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(
<KeypressProvider>
<BaseSettingsDialog
title="Test Settings"
items={filteredItems}
selectedScope={SettingScope.User}
maxItemsToShow={5}
onItemToggle={mockOnItemToggle}
onEditCommit={mockOnEditCommit}
onItemClear={mockOnItemClear}
onClose={mockOnClose}
/>
</KeypressProvider>,
<BaseSettingsDialog
title="Test Settings"
items={filteredItems}
selectedScope={SettingScope.User}
maxItemsToShow={5}
onItemToggle={mockOnItemToggle}
onEditCommit={mockOnEditCommit}
onItemClear={mockOnItemClear}
onClose={mockOnClose}
/>,
);
});
await waitUntilReady();
await waitFor(() => {
const frame = lastFrame();
expect(frame).toContain('Boolean Setting');

View File

@@ -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<typeof import('ink')>();
@@ -85,51 +76,45 @@ const TestComponent = ({
}, [onRef]);
return (
<MouseProvider mouseEventsEnabled={false}>
<KeypressProvider>
<ScrollProvider>
<Box flexDirection="column" width={80} height={24} padding={1}>
<Box flexGrow={1} borderStyle="round" borderColor="cyan">
<ScrollableList
ref={listRef}
data={items}
renderItem={({ item, index }) => (
<Box flexDirection="column" paddingBottom={2}>
<Box flexDirection="column" width={80} height={24} padding={1}>
<Box flexGrow={1} borderStyle="round" borderColor="cyan">
<ScrollableList
ref={listRef}
data={items}
renderItem={({ item, index }) => (
<Box flexDirection="column" paddingBottom={2}>
<Box
sticky
flexDirection="column"
width={78}
opaque
stickyChildren={
<Box flexDirection="column" width={78} opaque>
<Text>{item.title}</Text>
<Box
sticky
flexDirection="column"
width={78}
opaque
stickyChildren={
<Box flexDirection="column" width={78} opaque>
<Text>{item.title}</Text>
<Box
borderStyle="single"
borderTop={true}
borderBottom={false}
borderLeft={false}
borderRight={false}
borderColor="gray"
/>
</Box>
}
>
<Text>{item.title}</Text>
</Box>
<Text color="gray">{getLorem(index)}</Text>
borderStyle="single"
borderTop={true}
borderBottom={false}
borderLeft={false}
borderRight={false}
borderColor="gray"
/>
</Box>
)}
estimatedItemHeight={() => 14}
keyExtractor={(item) => item.id}
hasFocus={true}
initialScrollIndex={Number.MAX_SAFE_INTEGER}
/>
}
>
<Text>{item.title}</Text>
</Box>
<Text color="gray">{getLorem(index)}</Text>
</Box>
<Text>Count: {items.length}</Text>
</Box>
</ScrollProvider>
</KeypressProvider>
</MouseProvider>
)}
estimatedItemHeight={() => 14}
keyExtractor={(item) => item.id}
hasFocus={true}
initialScrollIndex={Number.MAX_SAFE_INTEGER}
/>
</Box>
<Text>Count: {items.length}</Text>
</Box>
);
};
describe('ScrollableList Demo Behavior', () => {
@@ -147,10 +132,10 @@ describe('ScrollableList Demo Behavior', () => {
let lastFrame: (options?: { allowEmpty?: boolean }) => string | undefined;
let waitUntilReady: () => Promise<void>;
let result: ReturnType<typeof render>;
let result: ReturnType<typeof renderWithProviders>;
await act(async () => {
result = render(
result = renderWithProviders(
<TestComponent
onAddItem={(add) => {
addItem = add;
@@ -230,45 +215,39 @@ describe('ScrollableList Demo Behavior', () => {
}, []);
return (
<MouseProvider mouseEventsEnabled={false}>
<KeypressProvider>
<ScrollProvider>
<Box flexDirection="column" width={80} height={10}>
<ScrollableList
ref={ref}
data={items}
renderItem={({ item, index }) => (
<Box flexDirection="column" height={3}>
{index === 0 ? (
<Box
sticky
stickyChildren={<Text>[STICKY] {item.title}</Text>}
>
<Text>[Normal] {item.title}</Text>
</Box>
) : (
<Text>[Normal] {item.title}</Text>
)}
<Text>Content for {item.title}</Text>
<Text>More content for {item.title}</Text>
</Box>
)}
estimatedItemHeight={() => 3}
keyExtractor={(item) => item.id}
hasFocus={true}
/>
<Box flexDirection="column" width={80} height={10}>
<ScrollableList
ref={ref}
data={items}
renderItem={({ item, index }) => (
<Box flexDirection="column" height={3}>
{index === 0 ? (
<Box
sticky
stickyChildren={<Text>[STICKY] {item.title}</Text>}
>
<Text>[Normal] {item.title}</Text>
</Box>
) : (
<Text>[Normal] {item.title}</Text>
)}
<Text>Content for {item.title}</Text>
<Text>More content for {item.title}</Text>
</Box>
</ScrollProvider>
</KeypressProvider>
</MouseProvider>
)}
estimatedItemHeight={() => 3}
keyExtractor={(item) => item.id}
hasFocus={true}
/>
</Box>
);
};
let lastFrame: () => string | undefined;
let waitUntilReady: () => Promise<void>;
let result: ReturnType<typeof render>;
let result: ReturnType<typeof renderWithProviders>;
await act(async () => {
result = render(<StickyTestComponent />);
result = renderWithProviders(<StickyTestComponent />);
lastFrame = result.lastFrame;
waitUntilReady = result.waitUntilReady;
});
@@ -334,27 +313,21 @@ describe('ScrollableList Demo Behavior', () => {
title: `Item ${i}`,
}));
let result: ReturnType<typeof render>;
let result: ReturnType<typeof renderWithProviders>;
await act(async () => {
result = render(
<MouseProvider mouseEventsEnabled={false}>
<KeypressProvider>
<ScrollProvider>
<Box flexDirection="column" width={80} height={10}>
<ScrollableList
ref={(ref) => {
listRef = ref;
}}
data={items}
renderItem={({ item }) => <Text>{item.title}</Text>}
estimatedItemHeight={() => 1}
keyExtractor={(item) => item.id}
hasFocus={true}
/>
</Box>
</ScrollProvider>
</KeypressProvider>
</MouseProvider>,
result = renderWithProviders(
<Box flexDirection="column" width={80} height={10}>
<ScrollableList
ref={(ref) => {
listRef = ref;
}}
data={items}
renderItem={({ item }) => <Text>{item.title}</Text>}
estimatedItemHeight={() => 1}
keyExtractor={(item) => item.id}
hasFocus={true}
/>
</Box>,
);
lastFrame = result.lastFrame;
stdin = result.stdin;
@@ -444,25 +417,19 @@ describe('ScrollableList Demo Behavior', () => {
let lastFrame: (options?: { allowEmpty?: boolean }) => string | undefined;
let waitUntilReady: () => Promise<void>;
let result: ReturnType<typeof render>;
let result: ReturnType<typeof renderWithProviders>;
await act(async () => {
result = render(
<MouseProvider mouseEventsEnabled={false}>
<KeypressProvider>
<ScrollProvider>
<Box width={100} height={20}>
<ScrollableList
data={items}
renderItem={({ item }) => <Text>{item.title}</Text>}
estimatedItemHeight={() => 1}
keyExtractor={(item) => item.id}
hasFocus={true}
width={50}
/>
</Box>
</ScrollProvider>
</KeypressProvider>
</MouseProvider>,
result = renderWithProviders(
<Box width={100} height={20}>
<ScrollableList
data={items}
renderItem={({ item }) => <Text>{item.title}</Text>}
estimatedItemHeight={() => 1}
keyExtractor={(item) => item.id}
hasFocus={true}
width={50}
/>
</Box>,
);
lastFrame = result.lastFrame;
waitUntilReady = result.waitUntilReady;
@@ -497,31 +464,25 @@ describe('ScrollableList Demo Behavior', () => {
}, []);
return (
<MouseProvider mouseEventsEnabled={false}>
<KeypressProvider>
<ScrollProvider>
<Box flexDirection="column" width={80} height={5}>
<ScrollableList
ref={(ref) => {
listRef = ref;
}}
data={items}
renderItem={({ item }) => <Text>{item.title}</Text>}
estimatedItemHeight={() => 1}
keyExtractor={(item) => item.id}
hasFocus={true}
initialScrollIndex={Number.MAX_SAFE_INTEGER}
/>
</Box>
</ScrollProvider>
</KeypressProvider>
</MouseProvider>
<Box flexDirection="column" width={80} height={5}>
<ScrollableList
ref={(ref) => {
listRef = ref;
}}
data={items}
renderItem={({ item }) => <Text>{item.title}</Text>}
estimatedItemHeight={() => 1}
keyExtractor={(item) => item.id}
hasFocus={true}
initialScrollIndex={Number.MAX_SAFE_INTEGER}
/>
</Box>
);
};
let result: ReturnType<typeof render>;
let result: ReturnType<typeof renderWithProviders>;
await act(async () => {
result = render(<TestComp />);
result = renderWithProviders(<TestComp />);
});
await result!.waitUntilReady();
@@ -622,33 +583,27 @@ describe('ScrollableList Demo Behavior', () => {
);
return (
<MouseProvider mouseEventsEnabled={false}>
<KeypressProvider>
<ScrollProvider>
<Box flexDirection="column" width={80} height={4}>
<ScrollableList
ref={(ref) => {
listRef = ref;
}}
data={items}
renderItem={({ item, index }) => (
<ItemWithState item={item} isLast={index === 4} />
)}
estimatedItemHeight={() => 1}
keyExtractor={(item) => item.id}
hasFocus={true}
initialScrollIndex={Number.MAX_SAFE_INTEGER}
/>
</Box>
</ScrollProvider>
</KeypressProvider>
</MouseProvider>
<Box flexDirection="column" width={80} height={4}>
<ScrollableList
ref={(ref) => {
listRef = ref;
}}
data={items}
renderItem={({ item, index }) => (
<ItemWithState item={item} isLast={index === 4} />
)}
estimatedItemHeight={() => 1}
keyExtractor={(item) => item.id}
hasFocus={true}
initialScrollIndex={Number.MAX_SAFE_INTEGER}
/>
</Box>
);
};
let result: ReturnType<typeof render>;
let result: ReturnType<typeof renderWithProviders>;
await act(async () => {
result = render(<TestComp />);
result = renderWithProviders(<TestComp />);
});
await result!.waitUntilReady();
@@ -696,35 +651,29 @@ describe('ScrollableList Demo Behavior', () => {
}, []);
return (
<MouseProvider mouseEventsEnabled={false}>
<KeypressProvider>
<ScrollProvider>
<Box flexDirection="column" width={80} height={10}>
<ScrollableList
ref={(ref) => {
listRef = ref;
}}
data={items}
renderItem={({ item }) => (
<Box height={item.id === '1' ? 10 : 2}>
<Text>{item.title}</Text>
</Box>
)}
estimatedItemHeight={() => 2}
keyExtractor={(item) => item.id}
hasFocus={true}
initialScrollIndex={Number.MAX_SAFE_INTEGER}
/>
<Box flexDirection="column" width={80} height={10}>
<ScrollableList
ref={(ref) => {
listRef = ref;
}}
data={items}
renderItem={({ item }) => (
<Box height={item.id === '1' ? 10 : 2}>
<Text>{item.title}</Text>
</Box>
</ScrollProvider>
</KeypressProvider>
</MouseProvider>
)}
estimatedItemHeight={() => 2}
keyExtractor={(item) => item.id}
hasFocus={true}
initialScrollIndex={Number.MAX_SAFE_INTEGER}
/>
</Box>
);
};
let result: ReturnType<typeof render>;
let result: ReturnType<typeof renderWithProviders>;
await act(async () => {
result = render(<TestComp />);
result = renderWithProviders(<TestComp />);
});
await result!.waitUntilReady();

View File

@@ -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(
<KeypressProvider>
<SearchableList {...defaultProps} />
</KeypressProvider>,
);
return renderWithProviders(<SearchableList {...defaultProps} />);
};
it('should render all items initially', async () => {

View File

@@ -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(
<KeypressProvider>
<ExtensionDetails
extension={mockExtension}
onBack={mockOnBack}
onInstall={mockOnInstall}
isInstalled={isInstalled}
/>
</KeypressProvider>,
renderWithProviders(
<ExtensionDetails
extension={mockExtension}
onBack={mockOnBack}
onInstall={mockOnInstall}
isInstalled={isInstalled}
/>,
);
it('should render extension details correctly', async () => {

View File

@@ -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<GenericListItem>,
);
vi.mocked(useUIState).mockReturnValue({
mainAreaWidth: 100,
terminalHeight: 40,
staticExtraHeight: 5,
} as unknown as ReturnType<typeof useUIState>);
vi.mocked(useConfig).mockReturnValue({
getEnableExtensionReloading: vi.fn().mockReturnValue(false),
getExtensionRegistryURI: vi
.fn()
.mockReturnValue('https://geminicli.com/extensions.json'),
} as unknown as ReturnType<typeof useConfig>);
});
const renderView = () =>
render(
<KeypressProvider>
<ExtensionRegistryView
extensionManager={mockExtensionManager}
onSelect={mockOnSelect}
onClose={mockOnClose}
/>
</KeypressProvider>,
renderWithProviders(
<ExtensionRegistryView
extensionManager={mockExtensionManager}
onSelect={mockOnSelect}
onClose={mockOnClose}
/>,
{
uiState: {
staticExtraHeight: 5,
terminalHeight: 40,
} as Partial<UIState>,
},
);
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');

View File

@@ -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 }) => (
<KeypressProvider>{children}</KeypressProvider>
);
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 }) => (
<KeypressProvider>{children}</KeypressProvider>
);
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 }) => (
<KeypressProvider>{children}</KeypressProvider>
);
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 }) => (
<KeypressProvider>{children}</KeypressProvider>
);
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 }) => (
<KeypressProvider debugKeystrokeLogging={false}>
{children}
</KeypressProvider>
);
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 }) => (
<KeypressProvider debugKeystrokeLogging={true}>
{children}
</KeypressProvider>
);
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 }) => (
<KeypressProvider debugKeystrokeLogging={true}>
{children}
</KeypressProvider>
);
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<Key> }) => {
const keyHandler = vi.fn();
const testWrapper = ({ children }: { children: React.ReactNode }) => (
<KeypressProvider>{children}</KeypressProvider>
);
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 }) => (
<KeypressProvider>{children}</KeypressProvider>
);
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 }) => (
<KeypressProvider>{children}</KeypressProvider>
);
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));

View File

@@ -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<Map<KeypressHandler, number>>(
@@ -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 (
<KeypressContext.Provider value={{ subscribe, unsubscribe }}>
<KeypressContext.Provider value={contextValue}>
{children}
</KeypressContext.Provider>
);

View File

@@ -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 }) => (
<MouseProvider mouseEventsEnabled={true}>{children}</MouseProvider>
);
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);

View File

@@ -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<Set<MouseHandler>>(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 (
<MouseContext.Provider value={{ subscribe, unsubscribe }}>
<MouseContext.Provider value={contextValue}>
{children}
</MouseContext.Provider>
);

View File

@@ -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(
<KeypressProvider>
<TestComponent />
</KeypressProvider>,
);
const { unmount } = renderWithProviders(<TestComponent />);
return {
result: {
get current() {

View File

@@ -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(
<KeypressProvider>
<TestComponent />
</KeypressProvider>,
);
};
const renderKeypressHook = (isActive = true) =>
renderHookWithProviders(() => useKeypress(onKeypress, { isActive }));
beforeEach(() => {
vi.clearAllMocks();

View File

@@ -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,
},
);

View File

@@ -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: () => <Text></Text>,
}));
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(<MainContent />, {
...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(<MainContent />, {
...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(<MainContent />, {
...altBufferOptions,
uiState: {
history: [],
pendingHistoryItems: [