Skip to content

Commit e205a46

Browse files
authored
Improve test coverage for cli/src/ui/components (#13598)
1 parent bdf80ea commit e205a46

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+2897
-51
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { render } from '../../test-utils/render.js';
8+
import { AboutBox } from './AboutBox.js';
9+
import { describe, it, expect, vi } from 'vitest';
10+
11+
// Mock GIT_COMMIT_INFO
12+
vi.mock('../../generated/git-commit.js', () => ({
13+
GIT_COMMIT_INFO: 'mock-commit-hash',
14+
}));
15+
16+
describe('AboutBox', () => {
17+
const defaultProps = {
18+
cliVersion: '1.0.0',
19+
osVersion: 'macOS',
20+
sandboxEnv: 'default',
21+
modelVersion: 'gemini-pro',
22+
selectedAuthType: 'oauth',
23+
gcpProject: '',
24+
ideClient: '',
25+
};
26+
27+
it('renders with required props', () => {
28+
const { lastFrame } = render(<AboutBox {...defaultProps} />);
29+
const output = lastFrame();
30+
expect(output).toContain('About Gemini CLI');
31+
expect(output).toContain('1.0.0');
32+
expect(output).toContain('mock-commit-hash');
33+
expect(output).toContain('gemini-pro');
34+
expect(output).toContain('default');
35+
expect(output).toContain('macOS');
36+
expect(output).toContain('OAuth');
37+
});
38+
39+
it.each([
40+
['userEmail', '[email protected]', 'User Email'],
41+
['gcpProject', 'my-project', 'GCP Project'],
42+
['ideClient', 'vscode', 'IDE Client'],
43+
])('renders optional prop %s', (prop, value, label) => {
44+
const props = { ...defaultProps, [prop]: value };
45+
const { lastFrame } = render(<AboutBox {...props} />);
46+
const output = lastFrame();
47+
expect(output).toContain(label);
48+
expect(output).toContain(value);
49+
});
50+
51+
it('renders Auth Method correctly when not oauth', () => {
52+
const props = { ...defaultProps, selectedAuthType: 'api-key' };
53+
const { lastFrame } = render(<AboutBox {...props} />);
54+
const output = lastFrame();
55+
expect(output).toContain('api-key');
56+
});
57+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { render } from '../../test-utils/render.js';
8+
import { AutoAcceptIndicator } from './AutoAcceptIndicator.js';
9+
import { describe, it, expect } from 'vitest';
10+
import { ApprovalMode } from '@google/gemini-cli-core';
11+
12+
describe('AutoAcceptIndicator', () => {
13+
it('renders correctly for AUTO_EDIT mode', () => {
14+
const { lastFrame } = render(
15+
<AutoAcceptIndicator approvalMode={ApprovalMode.AUTO_EDIT} />,
16+
);
17+
const output = lastFrame();
18+
expect(output).toContain('accepting edits');
19+
expect(output).toContain('(shift + tab to toggle)');
20+
});
21+
22+
it('renders correctly for YOLO mode', () => {
23+
const { lastFrame } = render(
24+
<AutoAcceptIndicator approvalMode={ApprovalMode.YOLO} />,
25+
);
26+
const output = lastFrame();
27+
expect(output).toContain('YOLO mode');
28+
expect(output).toContain('(ctrl + y to toggle)');
29+
});
30+
31+
it('renders nothing for DEFAULT mode', () => {
32+
const { lastFrame } = render(
33+
<AutoAcceptIndicator approvalMode={ApprovalMode.DEFAULT} />,
34+
);
35+
const output = lastFrame();
36+
expect(output).not.toContain('accepting edits');
37+
expect(output).not.toContain('YOLO mode');
38+
});
39+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { render } from '../../test-utils/render.js';
8+
import { Banner } from './Banner.js';
9+
import { describe, it, expect } from 'vitest';
10+
11+
describe('Banner', () => {
12+
it.each([
13+
['warning mode', true, 'Warning Message'],
14+
['info mode', false, 'Info Message'],
15+
])('renders in %s', (_, isWarning, text) => {
16+
const { lastFrame } = render(
17+
<Banner bannerText={text} isWarning={isWarning} width={80} />,
18+
);
19+
expect(lastFrame()).toMatchSnapshot();
20+
});
21+
22+
it('handles newlines in text', () => {
23+
const text = 'Line 1\\nLine 2';
24+
const { lastFrame } = render(
25+
<Banner bannerText={text} isWarning={false} width={80} />,
26+
);
27+
expect(lastFrame()).toMatchSnapshot();
28+
});
29+
});
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { act } from 'react';
8+
import { render } from '../../test-utils/render.js';
9+
import { ConfigInitDisplay } from './ConfigInitDisplay.js';
10+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
11+
import { AppEvent } from '../../utils/events.js';
12+
import { MCPServerStatus, type McpClient } from '@google/gemini-cli-core';
13+
import { Text } from 'ink';
14+
15+
// Mock GeminiSpinner
16+
vi.mock('./GeminiRespondingSpinner.js', () => ({
17+
GeminiSpinner: () => <Text>Spinner</Text>,
18+
}));
19+
20+
// Mock appEvents
21+
const { mockOn, mockOff, mockEmit } = vi.hoisted(() => ({
22+
mockOn: vi.fn(),
23+
mockOff: vi.fn(),
24+
mockEmit: vi.fn(),
25+
}));
26+
27+
vi.mock('../../utils/events.js', async (importOriginal) => {
28+
const actual = await importOriginal<typeof import('../../utils/events.js')>();
29+
return {
30+
...actual,
31+
appEvents: {
32+
on: mockOn,
33+
off: mockOff,
34+
emit: mockEmit,
35+
},
36+
};
37+
});
38+
39+
describe('ConfigInitDisplay', () => {
40+
beforeEach(() => {
41+
mockOn.mockClear();
42+
mockOff.mockClear();
43+
mockEmit.mockClear();
44+
});
45+
46+
afterEach(() => {
47+
vi.restoreAllMocks();
48+
});
49+
50+
it('renders initial state', () => {
51+
const { lastFrame } = render(<ConfigInitDisplay />);
52+
expect(lastFrame()).toMatchSnapshot();
53+
});
54+
55+
it('updates message on McpClientUpdate event', async () => {
56+
let listener: ((clients?: Map<string, McpClient>) => void) | undefined;
57+
mockOn.mockImplementation((event, fn) => {
58+
if (event === AppEvent.McpClientUpdate) {
59+
listener = fn;
60+
}
61+
});
62+
63+
const { lastFrame } = render(<ConfigInitDisplay />);
64+
65+
// Wait for listener to be registered
66+
await vi.waitFor(() => {
67+
if (!listener) throw new Error('Listener not registered yet');
68+
});
69+
70+
const mockClient1 = {
71+
getStatus: () => MCPServerStatus.CONNECTED,
72+
} as McpClient;
73+
const mockClient2 = {
74+
getStatus: () => MCPServerStatus.CONNECTING,
75+
} as McpClient;
76+
const clients = new Map<string, McpClient>([
77+
['server1', mockClient1],
78+
['server2', mockClient2],
79+
]);
80+
81+
// Trigger the listener manually since we mocked the event emitter
82+
act(() => {
83+
listener!(clients);
84+
});
85+
86+
// Wait for the UI to update
87+
await vi.waitFor(() => {
88+
expect(lastFrame()).toMatchSnapshot();
89+
});
90+
});
91+
92+
it('handles empty clients map', async () => {
93+
let listener: ((clients?: Map<string, McpClient>) => void) | undefined;
94+
mockOn.mockImplementation((event, fn) => {
95+
if (event === AppEvent.McpClientUpdate) {
96+
listener = fn;
97+
}
98+
});
99+
100+
const { lastFrame } = render(<ConfigInitDisplay />);
101+
102+
await vi.waitFor(() => {
103+
if (!listener) throw new Error('Listener not registered yet');
104+
});
105+
106+
if (listener) {
107+
const safeListener = listener;
108+
act(() => {
109+
safeListener(new Map());
110+
});
111+
}
112+
113+
await vi.waitFor(() => {
114+
expect(lastFrame()).toMatchSnapshot();
115+
});
116+
});
117+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { render } from '../../test-utils/render.js';
8+
import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
9+
import { describe, it, expect } from 'vitest';
10+
11+
describe('ConsoleSummaryDisplay', () => {
12+
it('renders nothing when errorCount is 0', () => {
13+
const { lastFrame } = render(<ConsoleSummaryDisplay errorCount={0} />);
14+
expect(lastFrame()).toBe('');
15+
});
16+
17+
it.each([
18+
[1, '1 error'],
19+
[5, '5 errors'],
20+
])('renders correct message for %i errors', (count, expectedText) => {
21+
const { lastFrame } = render(<ConsoleSummaryDisplay errorCount={count} />);
22+
const output = lastFrame();
23+
expect(output).toContain(expectedText);
24+
expect(output).toContain('✖');
25+
expect(output).toContain('(F12 for details)');
26+
});
27+
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { render } from '../../test-utils/render.js';
8+
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
9+
import { describe, it, expect, vi } from 'vitest';
10+
11+
vi.mock('@google/gemini-cli-core', () => ({
12+
tokenLimit: () => 10000,
13+
}));
14+
15+
vi.mock('../../config/settings.js', () => ({
16+
DEFAULT_MODEL_CONFIGS: {},
17+
LoadedSettings: class {
18+
constructor() {
19+
// this.merged = {};
20+
}
21+
},
22+
}));
23+
24+
describe('ContextUsageDisplay', () => {
25+
it('renders correct percentage left', () => {
26+
const { lastFrame } = render(
27+
<ContextUsageDisplay
28+
promptTokenCount={5000}
29+
model="gemini-pro"
30+
terminalWidth={120}
31+
/>,
32+
);
33+
const output = lastFrame();
34+
expect(output).toContain('50% context left');
35+
});
36+
37+
it('renders short label when terminal width is small', () => {
38+
const { lastFrame } = render(
39+
<ContextUsageDisplay
40+
promptTokenCount={2000}
41+
model="gemini-pro"
42+
terminalWidth={80}
43+
/>,
44+
);
45+
const output = lastFrame();
46+
expect(output).toContain('80%');
47+
expect(output).not.toContain('context left');
48+
});
49+
50+
it('renders 0% when full', () => {
51+
const { lastFrame } = render(
52+
<ContextUsageDisplay
53+
promptTokenCount={10000}
54+
model="gemini-pro"
55+
terminalWidth={120}
56+
/>,
57+
);
58+
const output = lastFrame();
59+
expect(output).toContain('0% context left');
60+
});
61+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { render } from '../../test-utils/render.js';
8+
import { CopyModeWarning } from './CopyModeWarning.js';
9+
import { describe, it, expect, vi, beforeEach } from 'vitest';
10+
import { useUIState, type UIState } from '../contexts/UIStateContext.js';
11+
12+
vi.mock('../contexts/UIStateContext.js');
13+
14+
describe('CopyModeWarning', () => {
15+
const mockUseUIState = vi.mocked(useUIState);
16+
17+
beforeEach(() => {
18+
vi.clearAllMocks();
19+
});
20+
21+
it('renders nothing when copy mode is disabled', () => {
22+
mockUseUIState.mockReturnValue({
23+
copyModeEnabled: false,
24+
} as unknown as UIState);
25+
const { lastFrame } = render(<CopyModeWarning />);
26+
expect(lastFrame()).toBe('');
27+
});
28+
29+
it('renders warning when copy mode is enabled', () => {
30+
mockUseUIState.mockReturnValue({
31+
copyModeEnabled: true,
32+
} as unknown as UIState);
33+
const { lastFrame } = render(<CopyModeWarning />);
34+
expect(lastFrame()).toContain('In Copy Mode');
35+
expect(lastFrame()).toContain('Press any key to exit');
36+
});
37+
});

0 commit comments

Comments
 (0)